Linux Environment Variables: A Practical Guide to env, export, and /etc/environment

Linux environment variables are one of those things you interact with constantly without always realizing it. Every time you run a command, your shell checks PATH. Every time a script logs something, it might reference HOME or USER. Every time a web app reads a database password, it is almost certainly pulling from an environment variable.

Understanding how to set, inspect, and manage Linux environment variables properly is a genuine sysadmin skill. It saves you from broken deployments, confusing permission errors, and scripts that work in one shell but silently fail in another.

This guide covers the full picture: what environment variables are, how scoping works, and how to set them correctly depending on your goal.

What Is an Environment Variable?

Output of the printenv command showing common Linux environment variables including PATH, HOME, USER, and SHELL

An environment variable is a named value attached to a running process. When a process spawns a child process, the child inherits copies of the parent’s environment variables. Changes in the child do not propagate back to the parent.

That last point trips people up constantly. If you set a variable inside a script and wonder why the parent shell does not see it after the script exits, that is why.

At the shell level, there is a distinction worth knowing:

  • Shell variables exist only in the current shell. Other processes cannot see them.
  • Environment variables are exported and passed to child processes.

Viewing Current Environment Variables

Start by seeing what is already set in your environment.

Print all environment variables:

env

Or use printenv for the same result:

printenv

Print a specific variable:

printenv HOME
printenv PATH

Or the shell built-in approach:

echo $HOME
echo $PATH

To see all shell variables, including unexported ones, use:

set

set outputs a lot. Pipe it through less or grep if you are looking for something specific.

set | grep JAVA

Setting Variables: Shell vs. Export

Here is the distinction in practice.

Set a shell-only variable:

MY_VAR="hello"

This variable exists in your current shell session but will not be passed to any program you run from that shell.

Export it so child processes can see it:

export MY_VAR="hello"

You can also export a variable that was already set:

MY_VAR="hello"
export MY_VAR

Verify the export status with declare:

declare -p MY_VAR

Output will show -x if the variable is exported:

declare -x MY_VAR="hello"

Quoting and Special Characters

Values with spaces, quotes, or shell metacharacters need careful quoting. Get this wrong and you end up with truncated values or unexpected expansions.

Double quotes preserve spaces but still expand variables and command substitutions:

export GREETING="Hello $USER"
echo $GREETING
# Output: Hello hydn

Single quotes preserve everything literally, with no expansion:

export LITERAL='Hello $USER'
echo $LITERAL
# Output: Hello $USER

For values containing dollar signs, backticks, or quotes, single-quote them or escape the special characters:

export DB_PASS='p@ss$word!'
export REGEX="\\d+\\.\\d+"

When values contain both single and double quotes, concatenate the two quoting styles:

export MSG="It's "'"complicated"'  # mix single and double quotes

Empty values are valid and behave differently from unset variables:

export EMPTY=""
printenv EMPTY    # prints a blank line, exit 0
unset EMPTY
printenv EMPTY    # prints nothing, exit 1

Running a Command with a Temporary Variable

Sometimes you want to set a variable for a single command without affecting the current shell at all:

MY_VAR="hello" printenv MY_VAR

This sets MY_VAR only for that one command. The current shell is unchanged before and after.

The env command can do the same thing and gives you more control:

env MY_VAR="hello" printenv MY_VAR

The env Command in Depth

env is more useful than it first appears. Most people use it to print the environment, but it has practical uses for scripting and troubleshooting.

Run a Command in a Clean Environment

The -i flag strips the inherited environment entirely:

env -i PATH=/usr/bin:/bin bash --norc  # minimal PATH required for basic commands

This starts a bash shell with nothing in the environment except what you specify. Useful for testing whether a script depends on something it should not, or for reproducing minimal environments similar to what a systemd service sees.

Unset a Variable for a Single Command

env -u HOME printenv HOME

This runs printenv HOME with HOME removed from the environment. Prints nothing. The variable is still set in your shell after the command returns.

The #!/usr/bin/env Shebang

If you write scripts, you have seen this:

#!/usr/bin/env python3
#!/usr/bin/env bash

Using /usr/bin/env in the shebang tells the kernel to look up python3 or bash in the user’s PATH, rather than hardcoding the binary location. This makes scripts more portable across systems where Python or Bash might live in different locations.

sudo and Environment Preservation

By default, sudo strips most of the environment for security reasons. Variables you have exported in your shell will not be visible to the command running under sudo:

export MY_VAR="hello"
sudo printenv MY_VAR
# Output: nothing

Use sudo -E to preserve the existing environment:

sudo -E printenv MY_VAR
# Output: hello

Be aware that -E is restricted by the env_keep and env_check directives in /etc/sudoers. Sensitive variables like PATH and LD_LIBRARY_PATH are usually filtered regardless.

To allow specific variables permanently, edit sudoers with visudo and add an env_keep line:

Defaults env_keep += "MY_VAR JAVA_HOME"

SUDO_USER is set automatically by sudo and contains the name of the original user who invoked the command. This is useful in scripts that need to know who is actually running them.

Persistence: Where to Actually Set Variables

The most common point of confusion with environment variables is persistence. Variables set with export at the command line vanish when the shell session ends. For variables to survive a logout, a reboot, or a new SSH session, you need to put them somewhere permanent.

The right location depends on the scope you need.

Per-User Variables: ~/.bashrc and ~/.bash_profile

For a single user, add your exports to ~/.bashrc (interactive non-login shells) or ~/.bash_profile (login shells). On most distros, ~/.bash_profile sources ~/.bashrc, so putting everything in ~/.bashrc is usually fine for desktop use.

# ~/.bashrc
export EDITOR="vim"
export JAVA_HOME="/usr/lib/jvm/java-17-openjdk"
export PATH="$PATH:$HOME/.local/bin"

Reload the file in your current shell:

source ~/.bashrc

Or the shorthand:

. ~/.bashrc

Note: Sourcing only affects the current shell. Other shells you have open will not see the change until they are restarted or sourced again. If you use Zsh, the equivalent files are ~/.zshrc and ~/.zprofile.

System-Wide Variables: /etc/environment

/etc/environment is the right place for system-wide variables that should apply to every user and every session, including graphical logins via PAM.

The format is simple: one KEY=VALUE pair per line, no export keyword, no shell syntax:

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

This file is not a shell script. You cannot use variable expansion or command substitution here. Just plain key-value pairs.

Changes to /etc/environment take effect at the next login. PAM reads this file via the pam_env module, so it applies to SSH sessions, display managers, and everything else managed by PAM. On most modern distros (Debian, Ubuntu, Fedora, RHEL) this is enabled by default.

System-Wide Variables: /etc/profile and /etc/profile.d/

/etc/profile is a shell script sourced at login for all users. You can use shell syntax here, including conditionals and variable expansion. However, the cleaner approach is to drop individual files into /etc/profile.d/ rather than editing /etc/profile directly.

For example, create a file to set a custom variable system-wide:

sudo nano /etc/profile.d/myapp.sh
export MYAPP_HOME="/opt/myapp"
export PATH="$PATH:$MYAPP_HOME/bin"

Make it executable:

sudo chmod +x /etc/profile.d/myapp.sh

This is picked up automatically at login because /etc/profile sources everything in /etc/profile.d/. It is the cleanest way to add application-specific paths and variables system-wide without touching core config files.

Note: /etc/profile.d/ files are only sourced for login shells, not every new terminal window. If you need variables available in non-login interactive shells too, you still need /etc/environment or per-user ~/.bashrc entries.

Variables for systemd Services

This one catches people out. If you run an application as a systemd service and need to inject environment variables, neither ~/.bashrc nor /etc/environment is reliably picked up by systemd units in all configurations.

The correct approach is to use the Environment= or EnvironmentFile= directives in the service unit file.

Inline in the unit file:

[Service]
Environment="DB_HOST=localhost"
Environment="DB_PORT=5432"

Or point to a separate file:

[Service]
EnvironmentFile=/etc/myapp/myapp.env

The .env file uses the same KEY=VALUE format, one per line. No export needed. Permissions on this file should be tight if it contains secrets:

sudo chmod 600 /etc/myapp/myapp.env
sudo chown root:myapp /etc/myapp/myapp.env

After editing, reload the unit and restart the service:

sudo systemctl daemon-reload
sudo systemctl restart myapp

Check what environment the service actually sees:

sudo systemctl show myapp | grep Environment

Values set with Environment= are visible to any user via systemctl show and process inspection. For secrets, use EnvironmentFile= with restricted permissions instead.

Or inspect the full process environment using the service’s main PID:

sudo cat /proc/$(systemctl show -p MainPID --value myapp)/environ | tr '\0' '\n'

Using MainPID from systemctl is more reliable than pidof, which can return multiple PIDs for services that run worker processes.

Inspecting a Process Environment

Every running process on Linux has its environment stored in /proc/PID/environ. The values are null-byte separated, so pipe through tr to make it readable.

cat /proc/1234/environ | tr '\0' '\n'

Replace 1234 with the actual PID. This is invaluable for confirming what a running service actually sees, rather than guessing based on what you think you configured.

Find a PID quickly with pidof or pgrep:

pidof nginx
pgrep -f myapp

Common Environment Variables Worth Knowing

Here are the ones you will encounter most often:

  • PATH: Colon-separated list of directories searched for executables.
  • HOME: The current user’s home directory.
  • USER / LOGNAME: The current username.
  • SHELL: Path to the current user’s default shell.
  • EDITOR / VISUAL: Preferred text editor for command-line tools.
  • LANG / LC_ALL: Locale and language settings.
  • TERM: Terminal type, used by ncurses applications.
  • DISPLAY: X11 display server address, required for GUI apps.
  • SUDO_USER: The original user when running under sudo.
  • XDG_CONFIG_HOME: Base directory for user config files (defaults to ~/.config).

Modifying PATH Correctly

Appending to PATH without clobbering it is a common source of mistakes. Always include the existing $PATH in your new value:

export PATH="$PATH:/opt/myapp/bin"

Prepending instead, so your binary takes priority over a system version:

export PATH="/opt/myapp/bin:$PATH"

Verify the result:

echo $PATH

Or check which binary a command resolves to:

which python3
type python3

Environment Variables in Docker Containers

Containers are one of the most common modern places where environment variables get used, especially for secrets and per-environment config. Docker provides a few ways to inject them.

Pass a single variable on the command line:

docker run -e DB_HOST=localhost -e DB_PORT=5432 myapp:latest

Pass an entire file with one KEY=VALUE pair per line:

docker run --env-file ./myapp.env myapp:latest

The --env-file format is the same as the EnvironmentFile= format used by systemd. No export, no shell syntax, no quotes around values unless you actually want the quotes to be part of the value.

In Docker Compose, the equivalent is the environment or env_file key:

services:
  myapp:
    image: myapp:latest
    env_file:
      - ./myapp.env
    environment:
      - LOG_LEVEL=debug

Inside a running container, inspect the environment exactly the same way as on the host:

docker exec myapp_container printenv

Or read it from /proc/1/environ inside the container, since the application typically runs as PID 1.

Unsetting Variables

Remove a variable from the current shell environment:

unset MY_VAR

This works for both shell variables and exported environment variables. After unset, the variable no longer exists in the current session. Child processes spawned after this point will not see it either.

A Quick Decision Guide: Where to Set Variables

  • Current session only, testing something: export VAR=value at the prompt.
  • Single command only: VAR=value command or env VAR=value command.
  • Persistent for your user account: Add to ~/.bashrc or ~/.bash_profile.
  • Persistent for all users, all session types: Add to /etc/environment.
  • Persistent for all users, login shells only, with shell logic: Drop a file in /etc/profile.d/.
  • For a systemd service: Use Environment= in the unit file or EnvironmentFile= pointing to a secured file.
  • For a Docker container: Use -e, --env-file, or the Compose environment / env_file keys.

Getting this wrong is how variables end up working in your terminal session but breaking in cron jobs, systemd services, or remote SSH commands. The scoping rules are consistent once you internalize them.

Note: If you manage scheduled tasks, keep in mind that cron runs with a minimal environment. Variables set in ~/.bashrc are not available to cron jobs. Either set them in the crontab directly, or source your profile at the top of the script.

Practical Example: Setting Up a Java Environment

A real-world scenario. You have installed Java and need JAVA_HOME set system-wide.

Find the Java installation path on Debian or Ubuntu:

update-java-alternatives -l

On RHEL, Fedora, or AlmaLinux, use:

alternatives --display java

Or a portable approach that works anywhere:

dirname $(dirname $(readlink -f $(which java)))

Create a profile.d file:

sudo nano /etc/profile.d/java.sh
export JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64"
export PATH="$PATH:$JAVA_HOME/bin"
sudo chmod +x /etc/profile.d/java.sh

Source it to test without logging out:

source /etc/profile.d/java.sh
echo $JAVA_HOME

For a service that needs JAVA_HOME, add it to the unit file directly using Environment= rather than relying on the profile script being sourced.

Conclusion

printenv command output on a Linux server showing common environment variables

Linux environment variables are simple in concept but genuinely important to get right in practice. Most breakage comes from setting a variable in the wrong place: it works in your interactive shell but not in a service, a cron job, or another user’s session.

The short version: use export for the current session, ~/.bashrc for your user, /etc/environment for system-wide settings, the EnvironmentFile= directive for systemd services, and --env-file for Docker containers. Know what /proc/PID/environ shows you when something is not behaving as expected.

Once the scoping model clicks, a whole category of confusing Linux behavior starts making sense.

Tags: , , ,

Ready to optimize your server performance?

Get expert Linux consulting or stay updated with our latest insights.

Contact me   Subscribe
Top ↑