Guides

How to set an environment variable in Linux, and make it permanent

Set an environment variable in Linux with export NAME=value. Make it permanent in ~/.bashrc or /etc/profile.d, and pass it to systemd, cron and sudo.

Published ~11 min read
On this page

Short answer: export NAME=value sets an environment variable for the current shell and every program you start from it, until that shell closes. To make it permanent for your user, put the same line in ~/.bashrc (or ~/.profile) and open a new shell; for every user, put it in a file under /etc/profile.d/. systemd services, cron jobs and sudo read none of those files, and each has its own place, covered below.

bash · any Linux
export APP_ENV=production                        # this shell and everything started from it
APP_ENV=staging ./run.sh                          # this one command only
echo 'export APP_ENV=production' >> ~/.bashrc     # every new shell from now on
printenv APP_ENV                                  # check the value

The shell behaviour and the example output on this page were checked on Debian 12 with bash 5.2.15 and GNU coreutils 9.1. The systemd, sudo and PAM details follow their man pages as shipped with Debian 12 (systemd.exec(5), sudoers(5), pam_env.conf(5)); Ubuntu uses the same programs.

Where to set a variable, and who sees it

WhereWho gets itWhen it applies
export NAME=valueThe current shell and the programs it startsImmediately, until the shell exits
NAME=value commandThat one commandFor that run only
~/.bashrcYour interactive bash shellsNew terminal, or source ~/.bashrc
~/.profileYour login sessions: SSH, console, desktop loginNext login
/etc/profile.d/*.shEvery user’s bash and sh login shellsNext login
/etc/environmentEvery session that starts through PAM: SSH, console, su, desktop loginNext login
Environment=, EnvironmentFile=One systemd serviceAfter restarting the service
Top of a crontabThe jobs in that crontabFrom the next run
sudo -E, env_keepCommands run through sudoPer command, or permanently in sudoers

export: set a variable for this shell

Without export, NAME=value creates a shell variable: the shell can use it, but the programs it starts do not get it. export puts it into the environment, which every child process receives a copy of:

bash · Debian 12
$ GREETING=hello
$ bash -c 'echo "child sees: [$GREETING]"'
child sees: []
$ export GREETING
$ bash -c 'echo "child sees: [$GREETING]"'
child sees: [hello]
  • No spaces around the equals sign. export NAME = value fails with bash: export: `=': not a valid identifier.
  • Quote values with spaces or special characters: export GREETING="hello world". Single quotes keep a dollar sign literal: export PRICE_TEXT='costs $5'.
  • Names use letters, digits and underscores and cannot start with a digit. Environment variables are upper case by convention; do not reuse PATH, HOME, USER or LANG unless you mean to change them.
  • A child gets a copy. Nothing a child process changes flows back to its parent, which is why a script cannot set a variable in your terminal (see scripts and .env files below).

Set a variable for one command only

Put the assignment in front of the command, with no export. Only that command sees it, and the shell keeps its old value:

bash · Debian 12
$ TZ=Asia/Tokyo date +%Z
JST
$ date +%Z
UTC

One trap catches almost everybody once:

bash · Debian 12
$ NAME=world
$ NAME=mars echo "hello $NAME"
hello world
$ NAME=mars bash -c 'echo "hello $NAME"'
hello mars

The shell expands $NAME on the command line before the temporary assignment takes effect, so echo receives the old value. Only a program that reads the variable from its environment, like the inner bash, sees the new one. The env command does the same from outside the shell’s syntax: env NAME=value command sets one, env -u NAME command runs a command without it, and env -i command starts it with an empty environment.

Check, list and unset variables

bash
printenv APP_ENV     # the value; prints nothing and returns 1 if it is not set
echo "$APP_ENV"      # also works for shell variables that are not exported
env | sort           # the whole environment
declare -p APP_ENV   # "declare -x" means exported, "declare --" means shell only

A running program got its environment when it started, and nothing you export later reaches it. /proc shows what a process actually has:

bash · Debian 12
$ MYAPP_MODE=staging sleep 300 &
[1] 48213
$ tr '\0' '\n' < /proc/$!/environ | grep MYAPP
MYAPP_MODE=staging
$ export MYAPP_MODE=production
$ tr '\0' '\n' < /proc/$!/environ | grep MYAPP
MYAPP_MODE=staging

Restart the program to pick up a change. For another user’s process, read the file as root: sudo cat /proc/PID/environ | tr '\0' '\n'. To remove a variable:

bash
unset APP_ENV        # gone from this shell and from programs started after this
export -n APP_ENV    # keep it as a shell variable, stop passing it on

If the variable comes from a startup file, delete the line there too, or it is back in the next session.

Make it permanent for your user: ~/.bashrc or ~/.profile

Every new shell starts from scratch and rebuilds its environment from startup files. bash reads ~/.bashrc for an interactive shell that is not a login (a terminal window, or typing bash), and the first of ~/.bash_profile, ~/.bash_login and ~/.profile for a login shell (an SSH login, the console). The default ~/.profile on Debian and Ubuntu loads ~/.bashrc itself, so on those systems one line in ~/.bashrc reaches both kinds of shell, as long as you have not created a ~/.bash_profile, which makes bash skip ~/.profile entirely.

bash
echo 'export APP_ENV=production' >> ~/.bashrc
source ~/.bashrc     # apply it to the shell you are in as well

When ~/.profile is the better file

Programs you start from a desktop menu never run bash, so ~/.bashrc does not reach them. They inherit the environment of the graphical login, which reads ~/.profile; Ubuntu’s community documentation recommends ~/.profile for exactly that reason. It is also the file that sh-compatible login shells other than bash read. Log out and back in after changing it.

The ssh host command trap

ssh server 'printenv APP_ENV' runs a shell that is neither interactive nor a login shell. bash still reads ~/.bashrc when it notices it was started by sshd, but the stock ~/.bashrc on Debian and Ubuntu begins by returning straight away in a non-interactive shell, so anything at the bottom of the file never runs there. If deploy scripts or other remote commands need the variable, put the line above that block:

~/.bashrc
export APP_ENV=production      # above the check: also seen by "ssh host command"

# If not running interactively, don't do anything
case $- in
    *i*) ;;
      *) return;;
esac

zsh and fish

zsh reads ~/.zshenv in every instance, scripts and remote commands included, and ~/.zshrc only in interactive shells; put exports that everything needs in ~/.zshenv. fish has no export line: set -gx APP_ENV production sets one for the session, and set -Ux APP_ENV production makes it universal and persistent without touching any file yourself.

Set it for every user: /etc/profile.d or /etc/environment

/etc/profile runs every *.sh file in /etc/profile.d/ for every bash or sh login shell. A file there is an ordinary shell script, so it can use other variables and conditions:

every user's login shells
sudo tee /etc/profile.d/app.sh > /dev/null <<'EOF'
export APP_ENV=production
EOF

/etc/environment is not a script. The PAM module pam_env reads it when a session starts: Debian 12 loads it for SSH logins, console logins and su, and the usual desktop login managers do the same. Per pam_env.conf(5) it must consist of simple NAME=VALUE pairs on separate lines. No shell ever runs it, so there is no $VARIABLE expansion and no commands.

/etc/environment
APP_ENV=production
HTTPS_PROXY=http://proxy.example.net:3128

Changing PATH without breaking the shell

PATH is the variable people break most, because one wrong assignment takes every command with it:

bash · Debian 12
$ export PATH=/opt/mytool/bin
$ ls
bash: ls: command not found

Always build on the existing value. Prepend to win over system copies, append to be a fallback:

bash
export PATH="/opt/mytool/bin:$PATH"     # searched first
export PATH="$PATH:/opt/mytool/bin"     # searched last

If a broken line is already in ~/.bashrc, put the standard list back for the current shell and open the file with the editor’s full path:

recover a broken PATH
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
/usr/bin/nano ~/.bashrc
  • Never add . or an empty entry (a leading or trailing colon, or ::). An empty entry means the current directory, so a file called ls in whatever folder you are in would run instead of the real one.
  • Keep directories other users can write to out of PATH, and never in front of the system ones.
  • Guard against duplicates when a file can be sourced more than once. The guarded version, with the zsh and fish equivalents, is in command not found: adding a directory to PATH permanently.

Scripts and .env files: why an export in a script vanishes

bash script.sh or ./script.sh runs in a child shell, and its exports die with it. source (or its short form, a single dot) runs the file in your current shell instead:

bash · Debian 12
$ cat setvars.sh
export FROM_SCRIPT=yes
$ bash setvars.sh; echo "[$FROM_SCRIPT]"
[]
$ source setvars.sh; echo "[$FROM_SCRIPT]"
[yes]

A .env file of plain KEY=value lines has no export in it, so sourcing it only creates shell variables. set -a exports everything assigned while it is on:

load a .env file into the environment
set -a
. ./.env
set +a

Sourcing runs the file as shell code: $(...) and backticks in it execute, and unquoted spaces split values. Only source files you trust, and quote values that contain spaces. The popular one-liner export $(cat .env | xargs) breaks on exactly those values.

systemd services: Environment= and EnvironmentFile=

A service started by systemd does not run a login shell, so ~/.bashrc, ~/.profile, /etc/profile.d/ and /etc/environment do not apply to it. Set its variables in the unit, best in a drop-in file so a package update never overwrites them:

sudo systemctl edit myapp
[Service]
Environment="APP_ENV=production" "LOG_LEVEL=info"
EnvironmentFile=/etc/myapp/myapp.env
apply and check
sudo systemctl restart myapp
systemctl show myapp -p Environment
sudo cat /proc/$(systemctl show -p MainPID --value myapp)/environ | tr '\0' '\n'

systemctl edit reloads systemd’s configuration by itself; after editing a unit file directly, run sudo systemctl daemon-reload before the restart. systemd does not expand variables in Environment= lines, so Environment=PATH=$PATH:/opt/mytool/bin is stored literally: write the full list. systemctl show lists only Environment= lines; the /proc line shows what the process really received, file included.

Environment= values are visible to any user through systemctl show, so tokens and passwords belong in an EnvironmentFile that only root can read, or in systemd’s credentials (LoadCredential=), which systemd.exec(5) recommends for secrets. The full setup, with permissions, is in creating a systemd service. Services you run as a user with systemctl --user also read ~/.config/environment.d/*.conf.

cron jobs

cron starts jobs with a minimal environment: SHELL=/bin/sh, HOME and LOGNAME from /etc/passwd, and on Debian and Ubuntu a PATH of just /usr/bin:/bin. It reads none of your startup files. Set what the jobs need at the top of the crontab, as literal values (cron does not expand $VARIABLES there), or load a file inside the job line:

crontab -e
APP_ENV=production
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

15 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
0 6 * * * set -a; . /etc/myapp/myapp.env; set +a; /usr/local/bin/report.sh

More on those header lines is in crontab’s own environment, and cron job not running shows how to run a command with exactly cron’s environment to test it.

sudo: why your variable disappears

Debian and Ubuntu configure sudo with env_reset and secure_path in /etc/sudoers: the command starts with a fresh environment, HOME and USER belong to the target user, and PATH is replaced by a fixed list. An export HTTPS_PROXY=... in your shell therefore never reaches sudo apt update. Pass it on explicitly:

bash
sudo HTTPS_PROXY=http://proxy.example.net:3128 apt update   # set it for this command
sudo --preserve-env=HTTPS_PROXY,NO_PROXY apt update          # keep only these from your shell
sudo -E ./deploy.sh                                          # keep your whole environment

Per sudoers(5), both the assignment and -E are allowed when your rule is ALL, the usual admin entry, because ALL implies the SETENV tag. PATH is the exception: secure_path replaces it even with -E, so call your own programs by full path (sudo "$(command -v mytool)"), as command not found under sudo explains. To keep a variable every time, add it to env_keep with visudo, which checks the syntax before saving:

sudo visudo -f /etc/sudoers.d/keep-proxy
Defaults env_keep += "HTTPS_PROXY NO_PROXY"

Two more things to know. sudo echo "$HOME" prints your own home, because your shell expands $HOME before sudo starts; sudo printenv HOME shows what the command really gets. And Ubuntu 26.04 ships sudo-rs, a rewrite that reads the same sudoers files but supports only part of their options, so test an env_keep line there before you rely on it.

On a server: keep the variable next to the program

On a VPS, a variable an application needs belongs with that application, usually in an EnvironmentFile under /etc/<app>/ with chmod 600, not in root’s ~/.bashrc. A service never reads that file, so the app only “works” while you start it by hand, and every root shell carries the secret around. Set it in one place, restart the service, confirm it through /proc/PID/environ, and reboot once to prove it survives. On a VMHeaven Linux VPS you have full root, so every option on this page is open to you, system-wide files included.

Frequently asked

How do I set an environment variable permanently in Linux?

For your own user, add export NAME=value to ~/.bashrc (or ~/.profile, which Debian and Ubuntu read at login and which loads ~/.bashrc there) and open a new shell. For every user, create a file such as /etc/profile.d/app.sh with the same line, or add NAME=value to /etc/environment. systemd services and cron jobs read none of these files and need the variable in their own configuration.

What is the difference between export and setting a variable without it?

NAME=value on its own creates a shell variable that only the current shell can use. export NAME=value also puts it into the environment, which every program started from that shell inherits. Written in front of a command, NAME=value command sets it for that one command only.

Why does sudo not see my environment variable?

Debian and Ubuntu configure sudo with env_reset, so commands start with a fresh environment, and secure_path replaces PATH. Pass a variable for one command with sudo NAME=value command, keep yours with sudo -E or sudo --preserve-env=NAME, or keep it every time with Defaults env_keep += "NAME" in a file under /etc/sudoers.d edited with visudo. PATH is replaced even with -E.

How do I set an environment variable for a systemd service?

Add Environment=NAME=value or EnvironmentFile=/path/to/file under [Service], ideally with sudo systemctl edit SERVICE, then restart the service. Services do not read ~/.bashrc, /etc/profile.d or /etc/environment. Keep secrets in an EnvironmentFile that only root can read, because Environment= values are visible to any user through systemctl show.

Related articles

All guides