Sooner or later, every program needs to be told things: where to find a file, which database to talk to, what mode to run in, what the secret password is. You could write all of that directly into the code — but the moment you do, you’ve welded your program to one specific setup. Move it to another machine and it breaks. Environment variables are the standard answer to this problem, and on a Linux server you’ll meet them constantly.
The idea is small and the payoff is large. An environment variable is just a named value that lives around a running program rather than inside its source code. Once you understand how they’re set, where they come from, and how a process actually reads them, a huge amount of server configuration stops being mysterious.
What an environment variable actually is
An environment variable is a NAME=value pair that the operating system keeps available to running programs. Every process on a Linux system carries a little dictionary of these pairs with it — its environment. When a program starts, it can look up any name in that dictionary and read the value.
That’s the whole concept. A name, a value, and a process that can read it:
ENVIRONMENT (a set of name=value pairs)
┌─────────────────────────────────────┐
│ HOME=/home/jane │
│ PATH=/usr/local/bin:/usr/bin:/bin │
│ LANG=en_US.UTF-8 │
│ APP_ENV=production │
│ DATABASE_URL=postgres://... │
└─────────────────────────────────────┘
│
▼
a running program
reads the values it cares about
By convention, environment variable names are written in UPPER_SNAKE_CASE — all capitals, words joined by underscores. That’s not a rule the system enforces, but everyone follows it, so a name like DATABASE_URL instantly reads as “this is an environment variable.”
The value is always a string. There are no numbers, booleans, or lists as far as the environment is concerned — just text. A program that wants a number reads the string "8080" and converts it itself.
Why they exist: separating config from code
The real reason environment variables matter is a principle worth saying out loud: configuration should live outside your code. The same program, byte for byte, should be able to run on your laptop, on a test server, and in production — and behave correctly in each place — without editing a single line. What changes between those places is the configuration, and the environment is where that configuration goes.
Think about a web app that needs a database connection. On your laptop it talks to a local database; in production it talks to a real one with a real password. If the connection string is baked into the code, you’d have to keep two different versions of the program, which is a recipe for shipping the wrong one. Instead, the code reads a variable:
one program, everywhere
─────────────────────────────────────────
laptop DATABASE_URL = postgres://localhost/dev
test DATABASE_URL = postgres://test-db/app
production DATABASE_URL = postgres://prod-db/app
Same code, three environments, zero edits. That’s the payoff. It also keeps secrets — passwords, API keys — out of your source files, which is where they absolutely should not be. A secret committed into code is a secret leaked to everyone who can read the code.
Environment variables are not encrypted
A common misunderstanding: putting a password in an environment variable does not encrypt or hide it. The value is plain text, readable by the process and often by anyone who can inspect that process or read the file you stored it in. Environment variables keep secrets out of your codebase and version control — which is genuinely valuable — but on the server itself you still protect them with file permissions and limited access, the same way you’d protect any other sensitive file.
How a process gets its environment
Here’s the part that ties everything together: a process inherits its environment from whatever started it. When one program launches another, the child receives a copy of the parent’s environment. That copy is independent — the child can change its own variables without affecting the parent.
This inheritance chain is why your shell matters so much. When you log in, a shell starts with an environment. Every command you run from that shell is a child process that inherits a copy of it. So the variables you see in your shell are the variables your commands will see too.
init / system
│ (sets a base environment)
▼
your login shell
│ (inherits it, may add more)
▼
the command you run ← gets a copy
│
▼
any program it launches ← gets a copy of THAT
Because each level gets a copy, a change made deep down never travels back up. A script that sets a variable for itself doesn’t pollute the shell that ran it. This one-way flow is exactly what you want — it keeps things predictable.
Shell variables vs. exported variables
This is the single most common point of confusion, so let’s be precise. Inside a shell there are actually two kinds of variables, and the difference is whether child processes can see them.
A plain shell variable exists only inside the current shell. You set one like this:
GREETING="hello"
echo "$GREETING" # prints: hello
That works, but GREETING is private to this shell. If you run another program, it won’t see GREETING at all — it was never added to the environment that gets copied to children.
To make a variable part of the environment — so child processes inherit it — you export it:
export GREETING="hello"
Now GREETING is a true environment variable. Any command you run afterward will find it. You can also do it in two steps, or export an already-existing variable:
APP_ENV="production" # shell variable for now
export APP_ENV # now it's in the environment
The mental model: a shell variable is a sticky note on your own desk; exporting it pins the note to the wall where everyone walking past can read it.
Set a variable for just one command
You don’t always need a permanent change. You can prefix a single command with a variable, and it applies only to that one run:
APP_ENV=production node server.jsHere APP_ENV is set in the environment of that node process only. The moment the command finishes, it’s gone — your shell never had APP_ENV at all. This is perfect for one-off tasks where you don’t want to leave settings lying around.
Reading and inspecting variables
To read a single variable in the shell, put a $ in front of its name. To see everything currently in your environment, a few commands help:
echo "$HOME" # print one variable's value
printenv PATH # print one variable, no $ needed
printenv # list every exported variable
env # same idea — dump the whole environment
printenv and env only show exported variables — the real environment. If you set a plain shell variable and it doesn’t appear in printenv, that’s your clue it wasn’t exported and child programs can’t see it. The set command (in bash) shows shell variables too, which is handy when you’re debugging why something “isn’t there.”
Quoting matters when you read a value. Always wrap $VAR in double quotes — "$VAR" — when you use it. If the value contains spaces and you forget the quotes, the shell splits it into multiple pieces and you get baffling errors. This is one of those habits that saves hours.
The variables you’ll meet everywhere
Linux ships with a set of standard environment variables that almost everything relies on. A few worth recognizing on sight:
PATH— a colon-separated list of directories the shell searches when you type a command. When you rungit, the shell walks through each directory inPATHuntil it finds a program namedgit. If a command is “not found,” a missingPATHentry is often why.HOME— the path to your home directory, like/home/jane. Lots of programs store their config and data relative toHOME.USER— the name of the current user.SHELL— the path to your login shell.LANG/LC_*— language and locale settings (date formats, character encoding, sorting order).PWD— the current working directory, kept up to date as you move around.TZ— the time zone, when set.
PATH is the one you’ll touch most. When you install a tool and the shell can’t find it, the fix is usually to add its directory to PATH:
export PATH="/opt/myapp/bin:$PATH"
Notice the trick: we put the new directory first, then append the old PATH with $PATH. That preserves everything that was already there and just adds to the front.
Making variables stick: temporary vs. permanent
Everything we’ve done so far lasts only as long as the current shell. Close the terminal or log out, and those exports are gone. That’s often fine — sometimes you want a setting to be temporary. But for configuration a server needs every time it starts, you need it to persist.
The traditional way is to put your export lines in a shell startup file that runs each time a shell begins. For an interactive login shell, that’s commonly ~/.bashrc or ~/.profile (the exact file depends on the shell and how it’s launched). Add a line like:
export EDITOR="nano"
…and it’ll be set in every new shell from then on. This is great for your own interactive settings.
For a long-running server process, though, relying on a personal startup file is fragile. A service started by the system at boot doesn’t read your ~/.bashrc — it isn’t a login shell, and it may not even run as you. The cleaner pattern for application configuration is to give the process its environment explicitly: define the variables in the service definition that launches it, or load them from a dedicated config file when the process starts. The principle is the same; you’re just choosing where the NAME=value pairs come from based on who starts the program.
Don't commit your secrets file
A popular convention is to keep configuration in a file (often named .env) listing KEY=value lines, then load it into the environment when the app starts. It’s a fine pattern — with one iron rule: never commit that file to version control. It almost always contains secrets. Add it to your ignore list, keep a safe .env.example with dummy values for documentation, and store the real file only on the machines that need it, protected by tight file permissions. Plenty of breaches trace back to a secrets file accidentally pushed to a public repository.
A concrete walk-through
Let’s tie it together with a tiny session. Imagine Jane is setting up a server process by hand to test it:
# nothing set yet — empty output
printenv APP_ENV
# set it, but only as a shell variable (not exported)
APP_ENV="staging"
printenv APP_ENV # still empty — children can't see it!
# now export it
export APP_ENV
printenv APP_ENV # staging
# the app, launched from this shell, inherits APP_ENV
node server.js # inside the app, APP_ENV reads "staging"
# override it for just one run, without changing the shell
APP_ENV=production node server.js # this run sees "production"
printenv APP_ENV # back to "staging" — the override was local
Every line here is one of the ideas from above doing its job: the export boundary, inheritance into the child process, and the one-command override that leaves the shell untouched. Once this little dance feels natural, configuration on a server stops being guesswork.
Why this matters on a server
On your own laptop you might get away with hard-coded settings, because there’s only one of you and one machine. On a server, environment variables become essential. They’re how the same deployable artifact runs in staging and production, how secrets stay out of your codebase, how a process running as a service gets exactly the configuration it needs without anyone editing files by hand. Almost every modern deployment workflow — from a single VPS to a fleet of containers — leans on this one simple NAME=value mechanism.
If you’ve already gotten comfortable in the Linux command line, environment variables are the next layer of control: not just running commands, but shaping the world those commands run in. And because each user on a server gets their own environment, the same variable can hold different values for different users — another reason this mechanism scales so neatly.
Wrapping up
Here’s the whole picture in one place:
- An environment variable is a
NAME=valuepair the OS keeps available to running programs; the value is always a string, and names are written inUPPER_SNAKE_CASEby convention. - They exist to separate configuration from code — the same program runs everywhere, with only its environment changing — and to keep secrets out of your source files.
- A process inherits a copy of its parent’s environment; changes flow one way, parent to child, never back.
- A plain shell variable is private to the current shell; you
exportit to make it part of the environment that children inherit. - Read variables with
$NAME,printenv, andenv;PATH,HOME, andUSERare standard ones you’ll see constantly. - Set them temporarily in a shell or per command, or permanently via a startup file or a service’s own configuration — and never commit a secrets file to version control.
Next, it’s worth looking at how you actually reach a server in the first place to set any of this up — securely connecting from your machine to a remote one over SSH, the tool every server administrator lives in.