Environment variables and PATH

Where configuration lives outside your code, why PATH decides which java runs, and how a container inherits none of it.

5 min read🧭 Programming Foundations

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

bash
echo $JAVA_HOME              # print one
printenv                     # print all of them
export API_URL=https://…     # set one, for this shell and its children

That 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:

bash
JAVA_HOME=/usr/lib/jvm/temurin-21 ./build.sh

PATH: 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.

plaintext
PATH=/usr/local/bin:/usr/bin:/bin

Type 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/usr/local/bin/usr/bin/opt/java21/bin
searchingnot startedfirst matchnone yetis Java 21 reachable?we do not know yet

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.

1 / 4

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:

plaintext
$ 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 -version says 17." Both are installed, and 17 is in a directory that comes earlier in PATH.
  • "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:

bash
which java          # the path of the java that would run
java -version       # which version that one actually is

Run 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:

plaintext
FATAL: DATABASE_URL is not set

Six words at boot beat a NullPointerException twenty minutes into the first request.

Try it yourself

Find out what you are actually running

  1. Print your PATH, and read it as the ordered list it is.
  2. Find out which java would run, and what version it is. Are you sure they match what you installed?
  3. Set a variable, print it, open a new terminal, and print it again.
  4. Run a command with a variable set only for that command, then print the variable afterwards.
What each one shows
bash
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 empty

Step 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.
  • "export makes 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.
Progress is saved on this device and to your account when signed in.