from /etc/profile to your own .bashrc
Anyone who has set an environment variable in an SSH session only to find it missing inside a cron job knows this problem well. Bash loads a completely different set of startup files depending on context. The load order of profile, bash_profile and bashrc decides which configuration actually takes effect, and this exact detail is where most administrators get stuck.
Table of Contents
- 1. Why the load order causes so much confusion
- 2. Login shell versus non login shell: the key distinction
- 3. /etc/profile and /etc/profile.d: the system wide layer
- 4. The login chain: bash_profile, bash_login and profile
- 5. .bashrc: the interactive non login shell
- 6. Non interactive shells and BASH_ENV
- 7. SSH, cron and systemd in practice
- 8. Common mistakes: lost PATH entries and duplicate output
- 9. Debugging the load order and comparison table
- 10. Summary
- 11. FAQ
1. Why the load order causes so much confusion
Few topics on Linux create as much confusion as the load order of Bash startup files. An administrator sets an environment variable in .bashrc, opens a new terminal, and the variable is there. The same cron job that needs that same variable fails the next morning with a cryptic error. The reason is simple yet rarely documented: Bash decides which file to read based purely on the invocation context, and cron invokes the shell in a way that ignores .bashrc entirely.
The load order is not an arbitrary design choice, it follows a clear logic based on two questions: is the shell a login shell, and is it interactive. From the combination of these two properties follows exactly which files get read and in which order. Once this logic is internalized, configuration problems can be found in seconds instead of hours of guessing why a variable is sometimes there and sometimes not.
2. Login shell versus non login shell: the key distinction
A login shell is created when a user logs into the system fresh: from a physical console, via ssh user@server without a trailing command, or with bash --login. A non login shell is created instead when a new terminal is opened from within an already running session, for example inside a graphical desktop, or when bash is invoked without further options. This distinction is the most important lever for the entire load order, because login shells and non login shells read completely different sets of files.
A second, independent axis is whether a shell is interactive, meaning it presents a prompt and waits for input, or runs non interactively, as is the case for scripts, cron jobs and systemd services. Together, login status and interactivity produce four possible combinations, and for each of these four combinations Bash reads a different set of startup files. This is exactly the reason a script running via cron sees different environment variables than the same script started manually in a terminal.
# Determine whether the current shell is a login shell
shopt -q login_shell && echo "login shell" || echo "not a login shell"
# Determine whether the current shell is interactive
case $- in
*i*) echo "interactive" ;;
*) echo "not interactive" ;;
esac
# Combine both checks in a small diagnostic script
cat <<'EOF' > /tmp/shell-context.sh
#!/usr/bin/env bash
if shopt -q login_shell; then
login="login"
else
login="non-login"
fi
case $- in
*i*) interactive="interactive" ;;
*) interactive="non-interactive" ;;
esac
echo "Context: $login, $interactive shell (PID $$)"
EOF
chmod +x /tmp/shell-context.sh
3. /etc/profile and /etc/profile.d: the system wide layer
For every login shell, Bash first reads the system wide file /etc/profile before any user specific file gets a chance. This file is the right place for settings that should apply to all users of a server, such as a system wide PATH addition or a global umask. On most modern distributions, /etc/profile additionally sources every file in /etc/profile.d/*.sh, which greatly simplifies maintenance because each package can drop its own configuration file without touching the central one.
This modular structure explains why installing something like rvm or a new PHP version suddenly makes new environment variables available in every login shell: the installer simply drops a new file into /etc/profile.d/. It is important that /etc/profile is only read for login shells. Opening just a new terminal window inside a running desktop typically does not trigger this file again, because that new window is usually not a login shell.
4. The login chain: bash_profile, bash_login and profile
After /etc/profile, Bash searches for exactly one of the following three files in the user home directory for a login shell, in this fixed order: first ~/.bash_profile, then ~/.bash_login, and only if neither exists, ~/.profile. As soon as one of these files is found, Bash reads only that one and ignores the rest. In practice this often produces a trap: creating a ~/.bash_profile causes an already existing ~/.profile to be skipped entirely by Bash, silently disabling settings from that file without any error message.
The common practice on most systems is therefore to avoid a dedicated ~/.bash_profile altogether and work in ~/.profile instead, because that file is additionally read by POSIX compliant shells like dash. Anyone who needs Bash specific configuration in the login chain instead creates a minimal ~/.bash_profile that simply sources ~/.bashrc. That way, both login shell and non login shell end up loading the same central configuration file, and maintenance stays bundled in a single place.
# ~/.bash_profile — minimal pattern: delegate to .bashrc
# Ensures a login shell picks up the same interactive settings
if [ -f "$HOME/.bashrc" ]; then
source "$HOME/.bashrc"
fi
# Login-only additions still belong here, not in .bashrc
export PATH="$HOME/.local/bin:$PATH"
umask 022
5. .bashrc: the interactive non login shell
The file ~/.bashrc is used exclusively for interactive non login shells, meaning new terminal windows, tabs inside a terminal multiplexer, or sessions started with bash from within an already running shell. This is the right place for aliases, prompt configuration (PS1), history settings and shell options such as shopt, because these things only take effect in interactive sessions in the first place. Environment variables that also need to be available in non interactive contexts should not be defined exclusively inside .bashrc.
A common misconception is assuming that .bashrc is read on every single Bash session without exception. In reality, a pure login shell without the sourcing trick shown above does not load it automatically at all, which explains why aliases defined in .bashrc sometimes appear missing after a fresh SSH login but suddenly show up after opening an additional terminal.
6. Non interactive shells and BASH_ENV
For non interactive shells, meaning those executing a script without waiting for a prompt, Bash by default reads none of the files mentioned so far. Instead, Bash checks the environment variable BASH_ENV, and if it is set, the file it references is loaded before the actual script runs. This variable is the only official lever for giving non interactive shells a shared configuration, yet in practice it is rarely used because many administrators set their variables explicitly inside the script itself instead.
Important for troubleshooting: BASH_ENV is only evaluated by Bash itself, not by sh or other POSIX shells, and it only applies to non interactive shells, never to interactive login or non login shells. Anyone expecting a variable started via cron to also appear in the user interactive shell is confusing two entirely separate load paths.
# Set BASH_ENV so non-interactive scripts pick up shared config
export BASH_ENV="/etc/bash_env_common.sh"
# /etc/bash_env_common.sh — loaded before every non-interactive script
export PHP_INI_SCAN_DIR="/etc/php/8.4/custom.d"
export MAGENTO_MODE="production"
# Verify the effect from a non-interactive invocation
bash -c 'echo "MAGENTO_MODE inside non-interactive shell: $MAGENTO_MODE"'
7. SSH, cron and systemd in practice
An SSH session with ssh user@server without an attached command creates an interactive login shell, which therefore goes through /etc/profile, then the login chain, and depending on the sourcing trick also .bashrc. Running ssh user@server 'command' with a directly attached command instead creates a non interactive non login shell, which by default reads none of the mentioned files and only evaluates BASH_ENV. This explains why remote deployment scripts often struggle with an empty PATH, even though everything works fine in an interactive SSH session.
Cron jobs always run as non interactive, non login shells with a very minimal environment, usually just SHELL, PATH and HOME. systemd services are even stricter: they inherit no shell startup files at all, and instead take their environment exclusively from Environment= and EnvironmentFile= inside the unit file. Anyone needing PHP environment variables for a Magento cron job or a systemd timer must set them explicitly inside the job itself or the unit file, rather than relying on .bashrc.
8. Common mistakes: lost PATH entries and duplicate output
The most common mistake in practice is defining a needed environment variable exclusively in .bashrc and then assuming it will also be available in a cron job or an SSH one liner. The second most common source of errors is echo output in .bashrc or .bash_profile, for example a welcome banner: in non interactive contexts this causes a script parsing the output of an SSH command to suddenly receive unexpected text lines before the actual result, because the startup file got read along the way.
A third classic mistake involves duplicated appends to PATH. If ~/.bash_profile additionally sources ~/.bashrc, and both files append the same path to PATH, the variable grows by another duplicate entry with every new terminal. The robust solution checks whether the path is already present before appending it again, instead of concatenating blindly.
# Idempotent PATH extension — avoids duplicate entries on repeated sourcing
add_to_path() {
local new_path="$1"
case ":$PATH:" in
*":$new_path:"*) ;; # already present, do nothing
*) PATH="$new_path:$PATH" ;; # not present, prepend once
esac
}
add_to_path "$HOME/.local/bin"
add_to_path "/usr/local/php8.4/bin"
export PATH
9. Debugging the load order and comparison table
To trace the actually loaded order, calling bash -x -l -c 'exit' makes every read file visible through trace mode. Alternatively, adding a dedicated echo line with the file name to the top of every suspect startup file, starting a new session in the respective context and reading the order of output lines works just as well. For SSH commands, ssh user@server 'echo $0; echo $-' is useful for checking whether the resulting shell was interactive or not.
| Context | Login Shell | Interactive | Files Read |
|---|---|---|---|
| SSH without command | Yes | Yes | /etc/profile, bash_profile/bash_login/profile |
| SSH with command | No | No | only BASH_ENV, if set |
| New terminal window | No | Yes | ~/.bashrc |
| Cron job | No | No | only BASH_ENV, minimal environment |
| systemd service | No | No | none, only unit file environment |
This table makes clear why a central .bashrc sourced from .bash_profile, combined with a well maintained BASH_ENV file, is the most robust combination. Instead of maintaining variables redundantly in multiple places, they get defined once in a shared file and included through whichever mechanism fits the context.
Mironsoft
Linux server administration and shell environments for PHP hosting
Environment variables that reach where they are needed?
We audit your shell configuration across SSH, cron and systemd, fix inconsistent load orders, and make sure deployment scripts and cron jobs reliably see the same environment.
Configuration Audit
Analysis of every startup file and its actual load order in production
Cleanup
Consolidation of redundant PATH entries and conflicting variables
Deployment Hardening
Consistent environment for cron, systemd timers and CI/CD pipelines
10. Summary
The load order of Bash startup files follows two axes: login status and interactivity. Login shells first read /etc/profile, then exactly one of the files ~/.bash_profile, ~/.bash_login or ~/.profile. Interactive non login shells read exclusively ~/.bashrc. Non interactive shells by default read nothing except the file referenced by BASH_ENV. Cron jobs and systemd services follow their own, even stricter rules and do not receive their environment from the classic startup files.
Anyone wanting a robust, maintainable configuration should let ~/.bash_profile source ~/.bashrc, maintain shared environment variables centrally through BASH_ENV or /etc/profile.d/, and avoid output in startup files that could also be reached from non interactive contexts. These few rules eliminate most of the confusion around disappearing environment variables in everyday server operations.
Bash Load Order: The Key Facts at a Glance
Login Shell
/etc/profile, then exactly one of bash_profile, bash_login or profile. Only on fresh login or bash --login.
Interactive Non Login Shell
Exclusively ~/.bashrc. Applies to new terminal windows and nested interactive shells.
Non Interactive Shell
No file by default, only BASH_ENV if set. Applies to script execution and one line SSH commands.
Cron & systemd
Own, minimal environment. Cron uses no startup files, systemd units only Environment= and EnvironmentFile=.