Linux Administration Lesson 17 – | Dataplexa
Section II — User, Process & Package Management

Environment Variables

In this lesson

What variables are export and scope Persistence files System-wide vs user Variables in services

Environment variables are named key-value pairs that the operating system makes available to every running process, shaping how programs find executables, where they write data, what language they display, and how they connect to other services. They are the invisible configuration layer that sits between the OS and every application — and understanding how they flow through a system is essential for both shell scripting and service administration.

How Variables Work and Flow Through Processes

Every process on Linux inherits a copy of its parent's environment. When you open a terminal, your shell receives the environment from the login process. When you run a script, that script receives a copy of the shell's environment. Crucially, this is a one-way copy — a child process can never modify the environment of its parent.

login / PAM reads /etc/environment, /etc/profile inherits copy bash (interactive shell) reads ~/.bashrc, ~/.bash_profile shell variable (not exported) MY_VAR=hello visible in this shell only child processes cannot see it environment variable (exported) export MY_VAR=hello visible in this shell AND all child processes it spawns Child processes receive a copy — changes in a child never propagate back to the parent

Fig 1 — Environment variables flow downward to child processes; unexported shell variables stay local to the current shell

# View all current environment variables
env
printenv

# View a single variable
printenv HOME
echo $HOME
echo $PATH

# List all shell variables (includes both shell-local and environment variables)
set | head -30

# Check whether a variable is in the environment (exported) or just shell-local
export -p | grep MY_VAR

What just happened? env printed only exported environment variables — those that child processes will inherit. set would show far more, including shell-local variables and functions. The four variables filtered — HOME, LANG, SHELL, USER — are set by the login process and automatically present in every interactive session.

Setting Variables and the export Command

Assigning a variable in bash creates a shell variable — local to the current shell only. Adding export promotes it to an environment variable, placing it in the environment block that every child process inherits. These two steps — assignment and export — are the fundamental operations, and understanding the difference between them prevents one of the most common shell scripting bugs.

Shell variable — local only

Exists only in the current shell session. Scripts and child processes you launch cannot see it. Lost when the shell exits.

MY_VAR="hello" echo $MY_VAR # works here bash -c 'echo $MY_VAR' # empty!

Environment variable — inherited

Placed in the environment block. Every child process, script, and command run from this shell receives a copy.

export MY_VAR="hello" echo $MY_VAR # works here bash -c 'echo $MY_VAR' # hello ✓
# Assign and export in one step (most common form)
export MY_VAR="hello"
export DB_HOST="db.prod.example.com"
export DB_PORT=5432

# Assign first, export later (equivalent)
MY_VAR="hello"
export MY_VAR

# Set a variable for a single command only — does not affect the current shell
DB_HOST=staging.example.com /opt/app/server --config /etc/app.conf

# Unset a variable (remove it entirely from the environment)
unset MY_VAR

# Make a variable read-only — cannot be changed or unset in this session
readonly API_KEY="abc123"
export API_KEY

# View all exported variables
export -p

What just happened? The per-command prefix syntax VAR=value command is a clean pattern for temporarily overriding a variable for exactly one command without touching the shell's environment at all. This is heavily used in deployment scripts to target a specific database host or configuration file without changing global state.

Essential Built-In Environment Variables

Linux and bash define a set of standard variables that are present in every session. These are not just conventions — programs actively depend on them to locate files, determine locale, find executables, and decide how to display output.

Standard Environment Variables
Variable Purpose and typical value
PATH Colon-separated list of directories the shell searches when you type a command name. The order matters — first match wins.
HOME The current user's home directory. Used by ~ expansion and by many programs to find config files.
USER The username of the current user. Set at login.
SHELL The path to the user's login shell, e.g. /bin/bash.
LANG Locale setting controlling language, character encoding, and date/number formats. e.g. en_US.UTF-8.
PWD The current working directory. Updated automatically by cd.
EDITOR The preferred text editor for interactive tools (crontab, git commit, visudo). e.g. vim or nano.
TERM Terminal emulator type. Programs use this to decide how to handle colour, cursor movement, and screen clearing.
PS1 The primary prompt string — controls what your shell prompt looks like. e.g. \u@\h:\w\$ shows alice@server:/etc$.
# Extend PATH to include a custom bin directory (prepend = searched first)
export PATH="$HOME/bin:$PATH"

# Append a directory to PATH (searched last)
export PATH="$PATH:/opt/custom/bin"

# Verify the change
echo $PATH

# Set preferred editor for all interactive tools
export EDITOR=nano
export VISUAL=nano     # some tools check VISUAL specifically

# Check PATH resolution — find which binary will actually run
which python3
type python3

What just happened? which finds the first matching binary in PATH. type is more complete — it also reveals aliases and shell functions, which which misses. The difference matters when debugging why a command behaves unexpectedly: the culprit might be an alias shadowing the real binary.

Persisting Variables — The Configuration File Hierarchy

Variables set with export at the command line last only until the shell exits. To make variables permanent, they must be written into a shell startup file or a system-wide configuration file. The right file depends on whether the variable should apply to one user or every user on the system.

/etc/environment — system-wide, all users

Not a shell script — simple KEY=value pairs, one per line. Read by PAM at login before any shell starts. The correct place for locale, system-wide PATH additions, and proxy settings.

LANG=en_US.UTF-8 PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

/etc/profile and /etc/profile.d/*.sh — system-wide, login shells

Shell scripts sourced for every user's login shell. Drop .sh files into /etc/profile.d/ rather than editing /etc/profile directly — cleaner and survives package upgrades.

echo 'export JAVA_HOME=/opt/jdk-21' | sudo tee /etc/profile.d/java.sh

~/.bash_profile or ~/.profile — per-user, login shells only

Sourced once when you log in (SSH, console login). The right place for user-specific PATH additions, API keys, and tool-specific variables. Typically sources ~/.bashrc at the end.

~/.bashrc — per-user, interactive non-login shells

Sourced every time a new interactive terminal is opened. The most commonly edited file for personal environment customisation — aliases, prompt settings, and variable exports that should apply to every terminal window.

# Add a variable permanently for the current user — append to ~/.bashrc
echo 'export MY_APP_ENV=production' >> ~/.bashrc

# Add a PATH extension permanently for the current user
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc

# Apply changes to the current session without opening a new terminal
source ~/.bashrc
# or equivalently:
. ~/.bashrc

# Add a system-wide variable (all users) via /etc/profile.d/
echo 'export JAVA_HOME=/opt/jdk-21' | sudo tee /etc/profile.d/java.sh
sudo chmod +x /etc/profile.d/java.sh

# Verify it loads for a new login shell
bash --login -c 'echo $JAVA_HOME'

What just happened? source ~/.bashrc re-read the file in the current shell — unlike running it as a script (which would start a child process and leave the parent unchanged). The bash --login -c test confirmed that JAVA_HOME loads automatically for any new login shell on the system, simulating what a newly connecting SSH user would see.

Environment Variables in systemd Services

systemd services do not load ~/.bashrc, ~/.profile, or most of /etc/profile. They run in a clean, minimal environment. If your service needs environment variables — database URLs, API keys, feature flags — they must be provided explicitly through the unit file using Environment= or EnvironmentFile=.

# /etc/systemd/system/myapp.service
# Passing environment variables to a systemd service

[Unit]
Description=My Application

[Service]
Type=simple
User=myapp
ExecStart=/opt/myapp/bin/server

# Method 1 — inline variables (fine for non-sensitive config)
Environment=APP_ENV=production
Environment=APP_PORT=8080
Environment=LOG_LEVEL=info

# Method 2 — external file (preferred for secrets and long lists)
# Variables in the file use KEY=value format, one per line
# File should be owned root, mode 0600 — never commit to git
EnvironmentFile=/etc/myapp/env

[Install]
WantedBy=multi-user.target
# Create the environment file for the service
sudo mkdir -p /etc/myapp
sudo tee /etc/myapp/env <<'EOF'
DB_HOST=db.prod.internal
DB_PORT=5432
DB_NAME=myapp_prod
DB_PASSWORD=s3cr3t-password
API_KEY=xk-prod-abc123
EOF

# Lock down permissions — only root should read secrets
sudo chmod 600 /etc/myapp/env
sudo chown root:root /etc/myapp/env

# After editing the unit file, reload and restart
sudo systemctl daemon-reload
sudo systemctl restart myapp

# Inspect what environment variables a running service actually sees
sudo systemctl show myapp --property=Environment
# Or from the process directly:
sudo cat /proc/$(pgrep -f myapp)/environ | tr '\0' '\n'

What just happened? Reading /proc/PID/environ reveals the exact environment the running process actually received — including variables from both Environment= lines and the EnvironmentFile=. Notice that the service PATH is the minimal systemd default — not the rich PATH from a user's shell profile — which is why services must use full paths in ExecStart=.

Practical Patterns and Common Pitfalls

Environment variables are simple in concept but generate a disproportionate share of debugging time in practice. The issues almost always stem from scope confusion, quoting mistakes, or forgetting that shells and services have different environments.

Spaces around =
Variable assignment cannot have spaces around the equals sign

MY_VAR = "hello" is a syntax error — bash interprets it as running the command MY_VAR with arguments. Always write MY_VAR="hello" with no spaces.

Subshell scope
Variables set inside a subshell do not affect the parent

Running a script as ./myscript.sh starts a subshell — any exports inside it are invisible to your current shell. To make them visible, use source ./myscript.sh or . ./myscript.sh instead.

Quote expansion
Single quotes prevent expansion; double quotes allow it

export PATH='$HOME/bin:$PATH' stores the literal string $HOME/bin:$PATH. Use double quotes: export PATH="$HOME/bin:$PATH".

Services ≠ shells
systemd services do not load shell profile files

A variable in ~/.bashrc will never reach a systemd service. Use Environment= or EnvironmentFile= in the unit file.

source needed
Editing ~/.bashrc does not affect the current session

After writing to ~/.bashrc, you must run source ~/.bashrc or open a new terminal for the change to take effect.

Analogy: An environment variable is like a sticky note attached to your desk. When you send someone to do a task for you (spawn a child process), you hand them a copy of your sticky note. If they write on their copy, your original is unchanged. If you want the note to follow you between sessions, you have to stick it on the wall (write it to a config file) — otherwise it falls off when you leave the desk (shell exits).

Never Store Secrets in /etc/environment or ~/.bashrc

Files like /etc/environment and ~/.bashrc are readable by anyone with access to that user account and are routinely captured in system backups, log bundles, and config management exports. API keys, database passwords, and tokens that live in these files are effectively world-readable on any shared system. For secrets, use a dedicated EnvironmentFile= with chmod 600, a secrets manager, or a tool like vault.

Lesson Checklist

I understand the difference between a shell variable and an environment variable, and know that export is what makes a variable visible to child processes
I know which file to edit for user-persistent variables (~/.bashrc), system-wide variables (/etc/environment), and per-tool system additions (/etc/profile.d/)
I always use source ~/.bashrc (not a new subshell) to apply changes to the current session, and I use double quotes when referencing variables in assignments
I can pass environment variables to systemd services using Environment= inline or EnvironmentFile= for secrets, and I verify what a service actually received via /proc/PID/environ
I store secrets in chmod 600 environment files, never in world-readable files like /etc/environment or shell profiles

Teacher's Note

The most productive debugging habit when a service "can't find" something that works fine in your terminal is to immediately read /proc/PID/environ for the running service. Nine times out of ten the variable is simply missing — because it was only in a shell profile, not in the unit file — and seeing the actual environment the process has makes the fix obvious in seconds.

Practice Questions

1. A developer sets export DB_HOST=staging.internal in their terminal, then runs ./deploy.sh and expects the script to use that value. The script calls a Python application that reads os.environ['DB_HOST']. Will this work? After the script exits, if they open a new terminal tab, will DB_HOST still be set?

2. You need to make JAVA_HOME=/opt/jdk-21 available system-wide so that every user who logs in — and also a systemd service called build-agent.service — has access to it. Describe where you would set it for each case and explain why the same file cannot serve both purposes.

3. A colleague added export PATH='/root/.local/bin:/home/claude/.npm-global/bin:/home/claude/.local/bin:/root/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' (single quotes) to their ~/.bashrc and reports that the new tools in ~/.local/bin are not found. What is wrong, and what is the correct line?

Lesson Quiz

1. You run MY_VAR=hello in your shell (without export), then run a script that contains echo . What does the script print?

2. Which file is the correct place to add a system-wide environment variable that applies to every user's login session on a Debian/Ubuntu system, without writing a shell script?

3. A systemd service is failing because it cannot find a binary in /usr/local/bin. You confirm the PATH in your own shell includes it. What is the most likely cause?

Up Next

Lesson 18 — Disk Partitioning and Mounting

Partitioning disks with fdisk and parted, formatting filesystems, and mounting them persistently with fstab