Environment variables and .env
What environment variables are, how processes set and inherit them, and why app config lives in the environment.
What environment variables are, how processes inherit them, and why app config lives in the environment.
Why this matters
The same app runs on your laptop, on a test environment, and in prod. The code is the same, but the database, the port and the log level differ everywhere. Almost always, those settings travel through environment variables: in Docker, in Kubernetes, in CI, in systemd.
That's where the usual problems come from. A script "can't see" a variable you just set. A container crashes over a config value that makes no sense. A database password ends up in git. This lesson covers where each of these comes from.
What an environment variable is
An environment variable is a name-value pair that a process gets from its parent at startup and sees for its whole lifetime. Apps get their configuration through these variables: which database to connect to, which port to listen on, which mode to run in.
See the current shell's environment:
printenv | head
PATH=/usr/local/bin:/usr/bin:/bin
HOME=/home/deploy
LANG=en_US.UTF-8
...
You can print a single variable with printenv HOME or echo "$HOME":
echo "$HOME"
/home/deploy
You refer to a variable as $NAME. By convention, environment variable names are uppercase:
DB_HOST, LOG_LEVEL.
Shell variable vs environment variable
A shell has two kinds of variables:
NAME=value # shell variable, visible only in this shell
export NAME=value # environment variable, child processes get it
The difference shows up as soon as the shell starts another process. Here sh -c starts a child
shell:
X=1; export Y=2; sh -c 'echo "X=[$X] Y=[$Y]"'
X=[] Y=[2]
X stayed inside the parent shell, Y made it to the child. You can also export later: set
X=1, then export X. To remove a variable entirely: unset X.
Because of this, echo and printenv sometimes give different answers. echo "$X" shows the
shell variable, while printenv X prints nothing: it's not in the environment.
How inheritance works
When a process starts another one, the child gets a copy of the parent's environment. It really is a copy: each one then changes its own, and the changes never travel back.
Two things follow from this. First: a child can't change its parent's environment. A script that
does export DB_HOST=... internally only changes its own environment, and once it finishes,
nothing changes in your terminal:
sh -c 'export DB_HOST=db.local'; echo "DB_HOST=[$DB_HOST]"
DB_HOST=[]
Second: the environment of a process already running doesn't change from the outside. Fixed a
setting? Restart the process, or it keeps working with the old values. On Linux, the environment
a process started with is visible in /proc/<PID>/environ (values are separated by a null byte,
so you turn them into lines with tr):
tr '\0' '\n' < /proc/1234/environ | grep APP_
Swap 1234 for your process's PID (how to find it was covered in the lesson on processes).
Set a variable for a single command without changing your own environment:
PORT=9090 ./app # app sees PORT=9090, your shell doesn't
The .env file
When there are a lot of variables, they live in an .env file:
# .env
DB_HOST=localhost
DB_PORT=5432
DB_PASSWORD=lab-only-password
LOG_LEVEL=info
These are KEY=value pairs, one per line. Nobody reads .env on its own: it's loaded by the app
(through something like dotenv), by docker compose (env_file:), by docker run --env-file, or
by hand in a shell:
set -a; . ./.env; set +a
. ./.env runs the file's lines in the current shell, which means it sets shell variables.
set -a turns on a mode where every variable you set gets marked for export right away, and
set +a turns it off. Without set -a, the variables from the file stay in the shell and never
reach the programs you launch.
There's no single standard for the .env format. A shell reads it by shell rules, docker compose
by its own, docker run --env-file by its own. Quotes, spaces around =, and trailing comments
are each handled differently. So when an app behaves strangely, don't look at the file, look at
what actually reached the process: printenv inside it, docker inspect for a container.
.env and git
The main rule: don't commit .env with secrets. Add it to .gitignore, and keep
.env.example next to it: the same keys with no values, a template of what to fill in.
# .env.example
DB_HOST=
DB_PORT=
DB_PASSWORD=
LOG_LEVEL=
.gitignore only works on files git isn't already tracking. If .env has already made it into
a commit, a line in .gitignore won't remove it: git keeps tracking changes to it. And a secret
from an old commit stays in history, readable by anyone with a copy of the repository. A password
like that counts as leaked, and you rotate it.
Why config lives in the environment
Keeping config in environment variables has been standard practice for a long time; it's one of the points in the twelve-factor app approach. The reasons:
- the same image runs in dev and prod, differing only in variables, no rebuild needed;
- secrets don't live in code, they're supplied at startup;
- changing a setting means restarting with a different environment, no need to edit files inside.
That's why containers are configured through -e KEY=value, --env-file and env_file: in
compose, rather than through config baked into the image:
docker run --rm -e LOG_LEVEL=debug busybox:1.36.1 printenv LOG_LEVEL
debug
If the same variable comes from both --env-file and -e, -e wins.
The environment isn't a vault. A container's variables are visible in docker inspect to anyone
with access to Docker, and a process's environment can be read from /proc by its owner and by
root. Serious secrets have their own mechanisms (Docker secrets, Kubernetes Secret). We'll cover
those in their own tracks.
Common mistakes
- Set it without
export. The variable exists in the shell (echoshows it), but the program doesn't see it. - Expecting a script to change the terminal's environment.
sh setup-env.shwithexportinside it changes nothing in your shell: it ran in a child process. To keep the variables for yourself, load the file into the current shell:. ./setup-env.sh. - Fixed
.envand didn't restart. A running process still lives with the old environment. - A file with Windows line endings. If
.envwas saved with CRLF, an invisible\rsneaks onto the end of every value, and8080turns into8080\r. This happens a lot on WSL; fix it in your editor's settings or withdos2unix. - Added
.envto.gitignoreafter committing it. The file is already tracked,.gitignoredoesn't apply to it, and the secret stays in history.
What you'll need for the tasks
Lab 1 and lab 3 run in your regular terminal (Linux, macOS, WSL2), lab 2 in Docker. You'll need:
printenvandecho "$NAME", the difference between a shell variable and an environment variable;export,set -aand. filefrom the sections "How inheritance works" and "The .env file";docker logs <container>anddocker inspect <container>, to see which variables a container got;git status,git add,git commit, and the section ".env and git".
What to remember
An environment variable is a name-value pair that a process gets as a copy from its parent. That's how config gets passed to an app.
export marks a variable for handing down to children; without it, a child process won't see
it. VAR=value command sets a variable for one run. A child can't change its parent's
environment, and a running process doesn't see fixes until it's restarted.
Config lives in the environment (.env, -e, --env-file, env_file:), so the same image works
everywhere and secrets don't sit in code. Don't commit an .env with secrets to git; keep an
.env.example next to it.
Next lesson: disk space, where it goes and how to find what's eating it.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 3 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson linux-admin-05-env-vars
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress