Environment variables and PATH
Where configuration lives outside your code, why PATH decides which java runs, and how a container inherits none of it.
An environment variable is a named value that a process is given when it starts. Not read from a file, not passed as an argument — handed to it by whatever launched it, and inherited by anything it launches in turn.
They are how configuration lives outside code, and how one machine ends up running a different Java than another without anybody noticing.
Reading and setting them
echo $JAVA_HOME # print one
printenv # print all of them
export API_URL=https://… # set one, for this shell and its childrenThat last word matters: for this shell and its children. export in one terminal does not affect another terminal, and it does not survive closing the window. A value you want permanently goes in a shell startup file; a value you want for one command goes in front of it:
JAVA_HOME=/usr/lib/jvm/temurin-21 ./build.shPATH: the one that decides which java runs
PATH is a list of directories, separated by : (; on Windows). When you type a command, the shell walks that list in order and runs the first match it finds.
PATH=/usr/local/bin:/usr/bin:/binType java, and the shell looks in /usr/local/bin first, then /usr/bin, then /bin, and stops at the first java it sees. Everything after that point is invisible.
you type java. The shell has a name and no location. It does not search your disk — that would be slow and unpredictable. It searches one list, in order, and that list is PATH.
/usr/local/bin. First entry, and there is a java here — version 17, installed months ago and forgotten. The search is over at the first hit; the shell does not look for a better one.
/usr/bin. Not examined. Not empty, not missing, not broken — simply never reached, because a match was already found.
/opt/java21/bin. Java 21 is here. It is installed, it works, and typing `java` will never run it while something earlier in the list answers first. This is the whole of "I installed 21 and it says 17".
Two directories in that list were never opened, and the version you installed is sitting in one of them. Reorder the list and nothing else, and the answer changes:
$ PATH=/opt/java17/bin:/opt/java21/bin
$ which java
/opt/java17/bin/java
$ java -version
openjdk version "17.0.9"
$ PATH=/opt/java21/bin:/opt/java17/bin # the only change
$ which java
/opt/java21/bin/java
$ java -version
openjdk version "21.0.1"Same machine, same two installations, same command. The order of one variable decided which one ran.
That single fact explains a category of confusion:
- "I installed Java 21 but
java -versionsays 17." Both are installed, and 17 is in a directory that comes earlier inPATH. - "It works for me but not for my colleague." Different
PATH, different first match. - "It works in my terminal but not in the CI pipeline." CI starts a fresh shell with a different environment — usually a much emptier one.
The command that ends the argument tells you which one you are running:
which java # the path of the java that would run
java -version # which version that one actually isRun both. They answer different questions, and people routinely run the second when they needed the first.
JAVA_HOME, and why two variables
PATH tells the shell where to find the java command. JAVA_HOME tells other programs where the whole JDK is installed — Maven, Gradle, application servers, and scripts all read it, because they need more than the launcher: they need javac, the libraries, the tools directory.
The two can disagree, and that is a genuinely confusing afternoon: your shell runs Java 21 because of PATH, while Maven compiles with Java 17 because of JAVA_HOME. Set both, and set them to the same JDK.
Configuration belongs here, not in code
The rule that follows is one of the most durable in backend work:
Anything that differs between environments goes in the environment. Database URLs, credentials, feature flags, the log level, which region you are in. The same built artifact then runs in test and in production, and only its environment differs.
The alternative — if (isProduction()) in the code — means your test environment is running different code from production, which defeats the point of testing it.
A container inherits none of it
This catches everybody once. A container starts with an environment you gave it and nothing else — not your shell's variables, not your ~/.bashrc, not JAVA_HOME from the host.
So a program that works on your laptop because a variable happened to be set will fail in a container with a confusing error, usually about something unrelated. If your program needs a variable, it should say so loudly at startup rather than failing later:
FATAL: DATABASE_URL is not setSix words at boot beat a NullPointerException twenty minutes into the first request.
Try it yourself
Find out what you are actually running
- Print your
PATH, and read it as the ordered list it is. - Find out which
javawould run, and what version it is. Are you sure they match what you installed? - Set a variable, print it, open a new terminal, and print it again.
- Run a command with a variable set only for that command, then print the variable afterwards.
What each one shows
echo $PATH | tr ':' '\n' # one directory per line — much easier to read
which java && java -version # where, and which
export FOO=bar && echo $FOO # bar. New terminal: empty — export is per-shell
FOO=once printenv FOO && echo "after: $FOO" # once, then emptyStep 3 is the one that corrects a wrong model. export feels permanent and is not; it lives as long as that shell. Step 4 shows the opposite extreme — a value that exists for exactly one command — and that form is how you test a configuration change without changing anything.
Misconceptions
- "Environment variables are global." They are per-process and inherited downward. Setting one does not reach a process that is already running.
- "
exportmakes it permanent." It makes it available to children of that shell. Closing the terminal ends it. - "If it is not in the code, it is configuration." Only if the code reads it from the environment. A hard-coded value in a file you plan to edit is still hard-coded.