Linux Environment Variables: Complete Guide

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.

Contents
  1. Prerequisites
  2. What Environment Variables Actually Are
  3. Viewing Variables
  4. Setting Variables Temporarily
  5. Setting Variables Permanently
  6. Shell Configuration Files — What Loads When
  7. The PATH Variable
  8. Environment Variables in Scripts
  9. Storing API Keys and Secrets
  10. Variables in systemd Services
  11. Environment Variables in Docker
  12. Troubleshooting
    1. Variable is set in terminal but not in cron job
    2. export in a script doesn't affect the parent shell
    3. Further Reading

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 | sort

The 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 variables

Viewing 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_VAR

Setting 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:
. ~/.bashrc

For 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/environment
EDITOR=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.sh

Shell 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.

FileLoaded byBest for
~/.bashrcEvery interactive non-login bash shellUser exports, aliases, functions
~/.bash_profileBash login shells only (SSH, tty)Should source ~/.bashrc; login-only setup
~/.profileLogin shells, sh-compatible (used by dash)Variables needed by GUI session and SSH
~/.zshrcEvery interactive zsh shellZsh exports, aliases, functions
~/.zshenvEvery zsh invocation (scripts too)Variables needed in non-interactive zsh
/etc/environmentPAM at login (all users, no shell)System-wide, static key=value pairs
/etc/profileAll login shellsSystem-wide login shell config
/etc/profile.d/*.shAll 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 ~/.bashrc

The 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 packages

Environment 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 .env
OPENAI_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 ~/.bashrc

Variables 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/environment
DB_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=Environment

Environment 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 env

Troubleshooting

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


Go up