GICT knowledge base
Environment Variables in Shell: What $PATH Controls and How to Set It Properly
Published
Every time you type python3 or nginx without specifying the full path, the shell has to find that executable somewhere. That somewhere is PATH. Understanding how PATH works is the difference between a script that runs everywhere and one that fails with “command not found” on every server except yours.
What PATH actually is
PATH is an environment variable. It holds a list of directory paths separated by colons. When you type a command, the shell searches these directories in order until it finds an executable with that name.
echo $PATH
On a typical Ubuntu VPS, this looks something like:
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
The shell checks each directory from left to right. It stops at the first match. If it reaches the end without finding the command, you get “command not found” and exit code 127.
How the shell resolves a command step by step
When you type nginx, the shell goes through this sequence:
- Check if
nginxis a shell builtin (likecdorexport). If yes, run it directly. - Check if
nginxis a function defined in the current shell session. - Search each directory in PATH, in order, for an executable file named
nginx. - First match wins. If no match is found, print “command not found”.
This matters when you have two programs with the same name in different PATH directories. The one in the earlier directory wins, regardless of which is “better” or more recent.
To see which version of a command the shell would run:
which python3
type python3
command -v python3
All three tell you the same thing — the full path the shell resolves for that command. Use these when debugging why your script runs a different version than you expect.
Viewing and modifying PATH
View the current PATH:
echo $PATH
Add a directory to the end of PATH:
export PATH="$PATH:/opt/myapp/bin"
Add it to the beginning to give it priority:
export PATH=/opt/myapp/bin:$PATH
The order matters. If /opt/myapp/bin/nginx and /usr/sbin/nginx both exist, putting the new directory first means your version runs instead of the system one.
These changes are temporary — they disappear when you close the terminal. To make them permanent, you need to know where to put them, which brings you to the next section.
Where PATH changes do not apply
This is the part most tutorials skip. Changes made in your shell profile (.bashrc, .profile) do not apply to systemd services, cron jobs, or any process that does not start from your interactive shell session.
Here is why. When you log in or open a terminal, the shell reads your profile scripts and sets environment variables including PATH. But when systemd starts a service, it does not read .bashrc. It starts the process directly with the environment it was given, which comes from the service file or the system-wide environment.
The same problem affects cron. Cron jobs run in a minimal environment that does not include your user is profile. PATH in cron defaults to whatever cron is configured to use, which is usually just /usr/bin:/bin.
Three ways to handle this:
- For systemd services — set the full path in
ExecStart, or setEnvironment=PATH=...in the service file. - For cron jobs — set PATH at the top of your crontab:
PATH=/usr/local/bin:/usr/bin:/bin:/opt/myapp/bin
0 * * * * /opt/myapp/bin/backup.sh
- For scripts run by other programs — always use absolute paths in
ExecStartor source a file that sets the environment before running the script.
Environment variables vs shell variables
There are two kinds of variables in bash.
A shell variable exists only in the current shell process:
MYVAR="hello"
echo $MYVAR # prints hello
bash -c 'echo $MYVAR' # prints nothing -- child shell does not see it
An environment variable is inherited by every child process:
export MYVAR="hello"
bash -c 'echo $MYVAR' # prints hello -- child shell inherits it
PATH is an environment variable because every command the shell runs is a child process that needs to find executables. Shell variables like myvar=hello are local to that shell session.
View all environment variables:
env
View all variables including shell-local ones:
set | head
Where to set PATH permanently
There are several places to put a PATH change, and the right one depends on who needs to see it.
/etc/environment — system-wide, applies to every user at login. Use this for paths that should be available to all users and services. Edit it directly:
sudo nano /etc/environment
Add a line like:
PATH="/opt/myapp/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
~/.profile — user-specific, read at login for non-interactive shells. Good for things that need to be available to scripts running outside an interactive session.
~/.bashrc — user-specific, read for interactive bash sessions. This is where you put aliases, functions, and PATH additions for your own terminal work. It is not read by login shells or systemd services.
The practical rule: put system-wide paths in /etc/environment, put user-specific interactive settings in ~/.bashrc, and always use absolute paths in systemd service files.
Debugging “command not found” errors
When a script fails with “command not found”, the fix is almost always one of three things:
- The command is not in PATH. Use
whichortypeto check if the shell can find it. If not, either add its directory to PATH or use the full path. - PATH differs between your terminal and the script is execution environment. The script might run under a different user or through cron, both of which may have different PATH values. Use absolute paths to eliminate this ambiguity.
- The executable is not marked as executable. Check with
ls -l /full/path/to/command. If it lacks execute permission, runchmod +x /full/path/to/command.
To see exactly what PATH a running process sees:
cat /proc/$(pgrep -f myservice)/environ | tr '\0' '\n' | grep PATH
Replace myservice with the name of your process. This shows the environment as it actually exists inside that process.
A practical workflow for adding a command to PATH
You have installed a tool in /opt/mytool/bin/ and want it available everywhere:
# 1. Verify the command works with the full path
/opt/mytool/bin/mytool --version
# 2. Add it to /etc/environment for system-wide access
sudo nano /etc/environment
# Add /opt/mytool/bin to the front of PATH
# 3. Reload the environment
source /etc/environment
# 4. Verify it is now found without the full path
which mytool
# 5. For systemd services, also set the path in the service file
# Environment=PATH=/opt/mytool/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Related guides
Plan a Safe Redis-to-Valkey Migration
Plan a Redis-to-Valkey migration: check version compatibility, rehearse recovery, stop writers for cutover and protect against lost writes during rollback.
Linux AdministrationHow to Restrict an SSH Key to One Command
Restrict an SSH key to one command with authorized_keys options. Protect the command and check that unwanted shell and forwarding access are denied.
Linux AdministrationHow to Schedule Tasks with systemd Timers
Schedule tasks with systemd timers. Verify service results separately from timer status, test missed-run handling and use an explicit UTC schedule.
Linux AdministrationHow to Restrict a Linux Service with systemd Security Settings
Restrict a Linux service with systemd drop-ins, a dedicated account, read-only paths, private temporary storage, and controlled writable directories.
