Linux Environment Variables: Complete Guide

Tested on: Ubuntu 26.04 LTS · Debian 12 · Arch Linux · Zsh — Last updated: June 2026
Environment variables are named key=value pairs that every Linux process carries in its environment block. They're how programs get configuration without hardcoded values — PATH tells the shell where to find executables, EDITOR tells Git and sudoedit which editor to open, ANTHROPIC_API_KEY reaches your Python script without ever touching source code. Understanding how they're scoped, inherited, and persisted correctly saves hours of debugging.
- Prerequisites
- What Environment Variables Actually Are
- Viewing Variables
- Setting Variables Temporarily
- Setting Variables Permanently
- Shell Configuration Files — What Loads When
- The PATH Variable
- Environment Variables in Scripts
- Storing API Keys and Secrets
- Variables in systemd Services
- Environment Variables in Docker
- Troubleshooting
Prerequisites
- A Linux terminal with bash or zsh
- Basic familiarity with the command line (cd, ls, nano/vim)
- sudo access for system-wide changes
- For the Docker section: Docker Engine installed
What Environment Variables Actually Are
Every running process on Linux has an environment — a small memory region of null-terminated KEY=value strings. When a process forks and execs a child (which is what your shell does every time you run a command), the child receives a copy of the parent's environment. The child can modify its own copy, but those changes never propagate back up.
You can inspect a process's live environment directly through /proc:
# View your current shell's environment via /proc:
cat /proc/$$/environ | tr ' ' 'n'
# Compare with what env shows (should match exported variables):
env | sortThe distinction between a shell variable and an environment variable is critical and frequently misunderstood. A shell variable exists only inside the shell process. An environment variable has been marked for export — it's copied into the environment block that child processes inherit.
# Shell variable — invisible to child processes:
MY_VAR="hello"
bash -c 'echo "child sees: $MY_VAR"'
# child sees:
# Environment variable — inherited by children:
export MY_VAR="hello"
bash -c 'echo "child sees: $MY_VAR"'
# child sees: hello
# Verify what's exported vs shell-only:
set | grep MY_VAR # shows shell variables (includes local)
env | grep MY_VAR # shows only exported variablesViewing Variables
Several commands overlap here with subtle differences:
# env — print exported environment variables (POSIX, works in scripts):
env
# printenv — print specific variable or all exported vars:
printenv HOME
printenv PATH
printenv # all exported variables
# set — print all shell variables, functions, and exports (bash built-in):
set | less
# Inspect a single variable:
echo $HOME
echo "$PATH" # always quote to handle spaces safely
# Check if a variable is set (empty-string-safe):
echo ${MYVAR:-"default value"} # use default if unset or empty
echo ${MYVAR-"default value"} # use default only if unset (not if empty)
echo ${MYVAR:?"error: MYVAR must be set"} # exit with error if unset
# Check whether variable is set at all:
[[ -v MYVAR ]] && echo "set" || echo "not set"Setting Variables Temporarily
Temporary variables live only for the current shell session or a single command. They disappear when the terminal closes.
# Session-scoped (survives subshells, gone when terminal closes):
export API_KEY="sk-abc123"
export DEBUG=1
export DB_HOST="localhost" DB_PORT="5432" # multiple on one line
# Single-command scope — set for one process, never touches your shell:
API_KEY=sk-abc123 python3 script.py
DEBUG=1 VERBOSE=1 ./my-program
# Verify it didn't leak into the shell:
echo $API_KEY # empty, only existed for that command
# Unset a variable:
unset API_KEY
echo ${API_KEY:-"gone"} # gone
# Remove from export but keep as shell variable (rarely needed):
export -n MY_VARSetting Variables Permanently
Permanent variables are written to a shell config file that's sourced on every new session. The right file depends on scope and shell.
# For your user, bash — edit ~/.bashrc:
nano ~/.bashrc
# Add at the bottom:
export EDITOR="nvim"
export BROWSER="firefox"
export GOPATH="$HOME/go"
export PATH="$HOME/.local/bin:$GOPATH/bin:$PATH"
# Apply immediately without opening a new terminal:
source ~/.bashrc
# or the shorthand:
. ~/.bashrcFor system-wide variables affecting all users, you have two options:
# Option 1: /etc/environment — PAM reads this for every login (GUI and SSH).
# Format: KEY=value, no 'export', no variable expansion, no shell syntax.
sudo nano /etc/environmentEDITOR=vim
TZ=Europe/London
MY_SETTING=value# Option 2: Drop a file in /etc/profile.d/ — sourced for login shells.
# Supports full shell syntax including variable expansion:
sudo tee /etc/profile.d/myapp.sh << 'EOF'
export MYAPP_HOME="/opt/myapp"
export PATH="$MYAPP_HOME/bin:$PATH"
EOF
sudo chmod 644 /etc/profile.d/myapp.shShell Configuration Files — What Loads When
Getting variables into the wrong file is a common source of "it works in my terminal but not in cron/SSH/my service" bugs.
| File | Loaded by | Best for |
|---|---|---|
~/.bashrc | Every interactive non-login bash shell | User exports, aliases, functions |
~/.bash_profile | Bash login shells only (SSH, tty) | Should source ~/.bashrc; login-only setup |
~/.profile | Login shells, sh-compatible (used by dash) | Variables needed by GUI session and SSH |
~/.zshrc | Every interactive zsh shell | Zsh exports, aliases, functions |
~/.zshenv | Every zsh invocation (scripts too) | Variables needed in non-interactive zsh |
/etc/environment | PAM at login (all users, no shell) | System-wide, static key=value pairs |
/etc/profile | All login shells | System-wide login shell config |
/etc/profile.d/*.sh | All login shells (sourced by /etc/profile) | Modular system-wide additions |
A reliable pattern for bash: keep ~/.bash_profile minimal and have it source ~/.bashrc so your variables are consistent between login and interactive sessions:
# ~/.bash_profile — the entire file:
[[ -f ~/.bashrc ]] && source ~/.bashrcThe PATH Variable
PATH is just a colon-separated list of directories the shell searches left to right when you type a command name. Prepending puts your directory first (takes priority); appending puts it last (fallback).
# View current PATH, one directory per line:
echo "$PATH" | tr ':' 'n'
# /home/alice/.local/bin
# /usr/local/bin
# /usr/bin
# /bin
# Prepend — your bin dir takes priority over system dirs:
export PATH="$HOME/.local/bin:$PATH"
# Append — system dirs win, yours is fallback:
export PATH="$PATH:$HOME/scripts"
# Make it permanent in ~/.bashrc:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
# Find where a command resolves to:
which python3 # /usr/bin/python3
type -a python3 # shows all matches in PATH order
command -v python3 # POSIX-safe, works in scripts
# Common directories to add:
# $HOME/.local/bin — pip install --user tools
# $HOME/go/bin — go install binaries
# $HOME/.cargo/bin — cargo install binaries
# $HOME/.npm-global/bin — npm install -g packagesEnvironment Variables in Scripts
Scripts run in a subprocess and inherit the caller's exported environment. Never assume a variable is set — always validate or provide defaults.
#!/usr/bin/env bash
set -euo pipefail
# Use default if variable is unset or empty:
DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-5432}"
LOG_LEVEL="${LOG_LEVEL:-info}"
# Hard-fail if a required variable is missing:
: "${API_KEY:?ERROR: API_KEY must be set before running this script}"
: "${DB_PASSWORD:?ERROR: DB_PASSWORD must be set}"
# Load a .env file if present (common pattern):
if [[ -f ".env" ]]; then
set -o allexport # automatically export everything that follows
source .env
set +o allexport
fi
echo "Connecting to ${DB_HOST}:${DB_PORT}"Storing API Keys and Secrets
Shell profile files work for development secrets but are plaintext and world-readable by root. Use the right tool for the context.
# Development: use a .env file per project, never commit it:
cat .envOPENAI_API_KEY=sk-proj-abc123
ANTHROPIC_API_KEY=sk-ant-xyz789
DATABASE_URL=postgres://user:pass@localhost/mydb# Always gitignore it:
echo ".env" >> .gitignore
echo ".env.*" >> .gitignore
git rm --cached .env 2>/dev/null || true # remove if accidentally staged
# Load in shell:
export $(grep -v '^#' .env | grep -v '^$' | xargs)
# Load in Python (python-dotenv):
# pip install python-dotenv# python-dotenv usage:
python3 - << 'EOF'
from dotenv import load_dotenv
import os
load_dotenv()
print(os.getenv("OPENAI_API_KEY", "not set"))
EOF# Production: use systemd credentials (encrypted at rest, Linux 5.10+):
# In your service unit:
# LoadCredential=api_key:/etc/myapp/secrets/api_key
# Access at runtime:
cat /run/credentials/myapp.service/api_key
# Lock down your ~/.bashrc if you store dev keys there:
chmod 600 ~/.bashrcVariables in systemd Services
systemd services don't load .bashrc, ~/.profile, or /etc/environment by default. Set variables in the unit file itself.
[Unit]
Description=My Application
After=network.target
[Service]
Type=simple
User=myapp
WorkingDirectory=/opt/myapp
# Inline variables:
Environment="NODE_ENV=production"
Environment="PORT=3000"
# Or load from a file (no export keyword, KEY=value format):
EnvironmentFile=/etc/myapp/environment
EnvironmentFile=-%h/.config/myapp/env # optional (- prefix = ignore if missing)
ExecStart=/opt/myapp/bin/server
Restart=on-failure
[Install]
WantedBy=multi-user.target# The EnvironmentFile format:
cat /etc/myapp/environmentDB_HOST=localhost
DB_PORT=5432
DB_NAME=production
LOG_LEVEL=warn# After editing a unit file, always reload and restart:
sudo systemctl daemon-reload
sudo systemctl restart myapp
# Verify what environment a service actually sees:
sudo systemctl show myapp --property=EnvironmentEnvironment Variables in Docker
# Pass a specific value:
docker run -e NODE_ENV=production -e PORT=3000 myimage
# Inherit a variable from your current shell (no = means take from env):
docker run -e API_KEY myimage
# Load entire .env file:
docker run --env-file .env myimage
# Inspect a running container's environment:
docker exec mycontainer printenv | sort
# In a Dockerfile (baked into the image — never use for secrets):
# ENV NODE_ENV=production# docker-compose.yml:
services:
app:
image: myapp:latest
environment:
- NODE_ENV=production
- PORT=3000
env_file:
- .env # merged with environment: above, env_file wins on conflicts
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: "${DB_PASSWORD}" # interpolated from shell envTroubleshooting
Variable is set in terminal but not in cron job
Cron runs with a stripped environment — no ~/.bashrc, no ~/.profile, a minimal PATH of /usr/bin:/bin. Fix by declaring the variable in the crontab directly or sourcing your profile:
# Set in crontab (run: crontab -e):
MYVAR=value
PATH=/usr/local/bin:/usr/bin:/bin
0 * * * * /usr/local/bin/myscript.sh
# Or source inside the script itself:
#!/bin/bash
source /home/alice/.bashrc
# rest of script...export in a script doesn't affect the parent shell
A child process cannot modify its parent's environment. Running export FOO=bar inside a script sets it
