You set an environment variable. You’re sure you did it right. But then your app doesn’t see it. You log out, log back in. Still doesn’t see it. You set it in one place, but it’s not there in another. You curse Linux and move on.
Here’s the thing: there are five different places where environment variables can live. They’re loaded at different times, in different contexts, with different scopes. Understanding which place to use saves hours of debugging.
The Five Places
1. /etc/environment — System-Wide, All Sessions
This is read at login time by the PAM system. Every user gets these variables.
$ sudo cat /etc/environmentPATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"LANGUAGE="en_US.UTF-8"JAVA_HOME="/usr/lib/jvm/java-11-openjdk-amd64"Variables here are available to all shells and all users at login. They’re sourced before shell-specific files.
When to use: System-wide settings that apply to every user (e.g., JAVA_HOME, language settings).
Edit it:
$ sudo nano /etc/environment# Add a new lineMY_APP_HOME="/opt/myapp"# Save and log out/inGotcha: This file is not a shell script. Don’t use variable expansion or complex syntax. Just KEY=value on each line.
2. ~/.profile — Per-User, Login Shells
This is read when you open a login shell (SSH, terminal at boot, su -). Not read in non-login contexts.
$ cat ~/.profileif [ -n "$BASH_VERSION" ]; then if [ -f "$HOME/.bashrc" ]; then . "$HOME/.bashrc" fifiThe default ~/.profile usually just sources ~/.bashrc. But you can add variables here:
export MY_DATABASE_URL="postgres://localhost/mydb"export MY_API_KEY="secret123"When to use: Per-user settings in login shells. Variables for specific users only.
Gotcha: Not read in non-login shells. Run bash -l to test (the -l makes it a login shell).
3. ~/.bashrc — Per-User, Interactive Shells
This is read every time you open an interactive shell (including non-login shells). Used most often.
export PATH="$PATH:$HOME/.local/bin"export MY_EDITOR="vim"This runs every shell session, so it’s good for frequently-used variables.
When to use: Personal aliases, functions, path extensions, interactive shell preferences.
Gotcha: Runs every time, so keep it fast. Don’t do slow operations here.
4. ~/.bashrc vs ~/.profile — The Hierarchy
Here’s the actual order:
-
Login shell (SSH,
su -, terminal at boot)- Read
/etc/profile(system-wide) - Read
/etc/profile.d/*.sh(system-wide) - Read
~/.profile(your login script) - Read
~/.bashrc(if~/.profilesources it, which it usually does)
- Read
-
Non-login shell (
bashin an open terminal)- Read
~/.bashrconly
- Read
-
Non-interactive shell (script execution)
- Doesn’t read any of these files
- Only inherits exported variables from parent
This is why setting a variable in ~/.bashrc might not show up when running a script. Scripts don’t source .bashrc. They only inherit exported variables.
5. Systemd Services — Environment=
For services managed by systemd, use the service file, not shell files:
[Service]Type=simpleExecStart=/usr/bin/myappEnvironment="MY_APP_DEBUG=1"Environment="DATABASE_URL=postgresql://localhost/mydb"Or in a drop-in directory:
$ sudo mkdir -p /etc/systemd/system/myapp.service.d$ sudo nano /etc/systemd/system/myapp.service.d/env.conf[Service]Environment="MY_VARIABLE=value"Reload and restart:
$ sudo systemctl daemon-reload$ sudo systemctl restart myappWhen to use: Environment variables for systemd services only. This is the right way.
The Real-World Scenarios
Scenario 1: “My app doesn’t see the variable”
You set DATABASE_URL in your ~/.bashrc, then run your Node.js app:
$ source ~/.bashrc$ node app.js// app doesn't see DATABASE_URLWhy: Your shell sees it, but when node starts, it doesn’t automatically source ~/.bashrc. You need to explicitly export it:
# In ~/.bashrcexport DATABASE_URL="postgres://localhost/mydb"node app.jsNow node inherits the exported variable.
Scenario 2: “SSH works but cron doesn’t”
You set a variable in ~/.bashrc. Works in SSH. But your cron job doesn’t see it:
$ crontab -e# Add:* * * * * /usr/bin/myapp# myapp doesn't see the variableWhy: Cron doesn’t source ~/.bashrc or ~/.profile. It only inherits variables from the cron environment.
Fix: Set variables in /etc/environment (system-wide) or in the cron job itself:
$ crontab -e# Set the variable for this jobDATABASE_URL="postgres://localhost/mydb" * * * * * /usr/bin/myappScenario 3: “Systemd service doesn’t see the variable”
You set LOG_LEVEL in ~/.bashrc. The systemd service doesn’t see it:
[Service]ExecStart=/usr/bin/myapp# myapp doesn't see LOG_LEVELWhy: Systemd services don’t inherit the user’s shell environment. They start fresh.
Fix: Use Environment= in the service file:
[Service]ExecStart=/usr/bin/myappEnvironment="LOG_LEVEL=debug"The Troubleshooting Checklist
Variable not showing up? Follow this:
# 1. Is it exported?$ echo $MY_VARIABLE# If empty, it's not set. If set, continue to 2.
# 2. Where is it set?$ grep -r "MY_VARIABLE" ~/.bashrc ~/.profile /etc/environment
# 3. Did you source it after editing?$ source ~/.bashrc$ echo $MY_VARIABLE
# 4. Is it exported?$ export MY_VARIABLE="value"
# 5. For systemd services, use Environment=The Rules
For login shells (SSH, remote login):
Use /etc/environment (system-wide) or ~/.profile (user-specific).
For interactive shells:
Use ~/.bashrc if it’s user-specific and interactive-only.
For scripts run directly:
export MY_VAR="value" && /usr/bin/myscriptFor systemd services:
Use Environment= in the service file. Never rely on shell files.
For everything else:
/etc/environment is the safest default for system-wide settings.
Key Takeaway
Environment variables in Linux are layered. Different contexts load different files. SSH loads profile files. Scripts don’t. Systemd doesn’t care about shells. Understanding which file gets read in which context saves you from hours of “why doesn’t this work?” debugging.
Master this, and you’ll stop cursing environment variables and start debugging actual problems.