Environment Variables on Linux: System Wide vs. User Scope
AI generated
$
/etc
Linux · Environment Variables · Scope
Environment variables: system wide vs. user scope
who sees a variable, and who does not

An environment variable that works fine in the administrator's shell but is simply missing inside the PHP-FPM process of a Magento application is one of the most common puzzles in daily server operations. The reason is almost always scope: environment variables are only inherited by child processes, never backward and never across independent process trees, and system wide scope works fundamentally differently from user specific scope.

14 min read Process Inheritance · /etc/environment · systemd Environment= Linux · PHP-FPM · systemd · Docker

1. Why scope is decisive for environment variables

An environment variable is a simple key value entry that a process inherits from its parent process and that is available to it at runtime. The scope of a variable, meaning the range in which it is actually visible, depends entirely on where it was defined and through which process tree it gets passed on. This exact relationship is the cause of one of the most common operational problems: a variable that works in the interactive shell is missing in the concurrently running web server process, because both processes descend from completely different parent processes.

Anyone using environment variables in production, for example for database credentials, API keys or PHP configuration, must therefore know exactly which process tree can even see which variable. System wide scope and user specific scope differ not only in where the definition is stored, but also in which processes ever get to see these definitions, a difference that quickly leads to hard to trace errors in production environments such as Magento hosting setups.

This basic rule applies regardless of whether a variable was set via export in a shell, via a configuration file, or via a directive in a unit file: the process tree decides, not the location of definition alone. Only the interplay between where a variable is defined and the actual parent process relationship determines its final scope.

An additional aspect influencing scope is the timing of evaluation: some variables are read only once when a process starts, while others get re evaluated on every function call. A change to /etc/environment, for example, only affects new sessions, never processes already running, even if those would theoretically be entitled to read the file. This detail often leads in practice to the mistaken assumption that a configuration change had no effect, when in fact the affected service simply had not yet been restarted.

2. Process inheritance: how variables pass to child processes

The central mechanism behind every scope is process inheritance: when a process creates a new child process via fork() and exec(), that child process by default receives a copy of the parent process's entire environment. This inheritance only works in one direction, from top to bottom in the process tree, never backward. If a child process changes an inherited variable, that only affects its own copy and its own later child processes, never the parent process itself.

This one way street explains a common misconception: setting a variable with export in a running terminal and then starting a script, the script sees this variable because it is a child process of the shell. Restarting an already running program via systemctl restart, on the other hand, the new process does not inherit the environment of the interactive shell, but the environment of systemd itself as its actual parent process, which is why variables set in the shell fundamentally never arrive there.


# Demonstrate one-directional inheritance
export DEMO_VAR="parent value"

bash -c '
  echo "Child sees inherited variable: $DEMO_VAR"
  export DEMO_VAR="child value"
  echo "Child changed its own copy: $DEMO_VAR"
'

# Back in the parent shell, the original value is untouched
echo "Parent still has: $DEMO_VAR"   # still "parent value"

# Show the full environment a process would receive
env | sort | head -20

3. System wide scope: /etc/environment and /etc/profile.d

For variables meant to apply to every user and every login session on a server, /etc/environment is the central place. This file contains plain KEY=value assignments without shell syntax, is not interpreted by the shell but read directly by pam_env.so at login, and therefore applies regardless of the shell used, whether Bash, Zsh or Dash. This is exactly what makes it the most robust place for system wide definitions, because it does not depend on Bash specific syntax.

For more complex system wide logic, such as conditional assignments or variables coming from installed packages, /etc/profile.d/ is the better place, because real shell scripts with full syntax can live there. The decisive difference: files in /etc/profile.d/ are only sourced from /etc/profile and therefore only apply to login shells, while /etc/environment applies system wide through PAM and thereby also reaches graphical desktop sessions and some non shell contexts.


# /etc/environment — plain KEY=value pairs, no shell syntax, read by PAM
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
PHP_INI_SCAN_DIR="/etc/php/8.4/custom.d"
TZ="Europe/Berlin"

4. User specific scope: bashrc, profile and bash_profile

User specific scope lives in the startup files inside the home directory of the respective user: ~/.bashrc, ~/.profile or ~/.bash_profile, depending on whether it is a login or non login shell. Variables defined here are visible exclusively to this one user and do not affect other system accounts, even if they run the same service. This is deliberately designed this way, because different users on the same server may well need different tool versions, API keys or debug settings.

In practice, this user specific scope often causes confusion in deployment processes: if a deployment script runs under the user deploy, but the actual PHP-FPM process runs under the user www-data, defining a variable in the .bashrc of deploy does not help at all, because www-data never starts an interactive shell reading that file. For variables that need to apply across user boundaries, the system wide scope or an explicit configuration at the level of the respective service is fundamentally the right choice.

5. export versus local shell variable: the decisive difference

Within a single shell session there is another, often overlooked scope level: the difference between a simple shell variable and an exported environment variable. An assignment like VARIABLE=value without export only creates a local shell variable, visible to the current shell itself, but never passed on to child processes. Only export VARIABLE=value marks the variable to become part of the environment inherited by every future child process of that shell.

This difference explains a classic mistake in deployment scripts: a script sets DB_PASSWORD=secret without export and then calls a PHP script that tries to access the variable via getenv('DB_PASSWORD'). The PHP script receives an empty value, because the variable never made it into the environment of the child process, even though it was easily visible in the calling shell with echo $DB_PASSWORD. This trap is one of the most common reasons for seemingly randomly missing configuration values in production scripts.


# Local shell variable — NOT passed to child processes
DB_PASSWORD="secret"
php -r 'var_dump(getenv("DB_PASSWORD"));'   # bool(false)

# Exported variable — IS passed to child processes
export DB_PASSWORD="secret"
php -r 'var_dump(getenv("DB_PASSWORD"));'   # string(6) "secret"

# List only exported variables of the current shell
export -p | grep DB_PASSWORD

6. systemd services and their own environment scope

systemd services have a completely own scope, entirely independent of any shell configuration. A systemd service inherits neither /etc/environment automatically nor any Bash startup file, but sources its environment exclusively from the directives Environment= for individual assignments and EnvironmentFile= for an external file with multiple assignments, each directly inside the service's unit file. This strict separation is intentional: it ensures a service reproducibly receives the same environment, regardless of which user happens to restart the service at any given moment.

For PHP-FPM pools an additional layer comes into play: the env[VARIABLE] directive in the pool configuration defines variables passed exclusively to PHP worker processes of that specific pool, independent of the systemd scope of the PHP-FPM main process itself. This nested scope structure, systemd unit, then FPM pool, then PHP worker, is the reason a variable needed by PHP could often be configured in three different places, but only one of them actually reaches the application code.


# /etc/systemd/system/php8.4-fpm.service.d/override.conf
[Service]
Environment="PHP_INI_SCAN_DIR=/etc/php/8.4/custom.d"
EnvironmentFile=/etc/default/magento-env

# /etc/default/magento-env — external file with KEY=value pairs
MAGENTO_MODE=production
MAGE_MODE=production

# /etc/php/8.4/fpm/pool.d/www.conf — pool-specific variables for workers
env[MAGENTO_MODE] = production
env[PATH] = /usr/local/bin:/usr/bin:/bin

7. Container scope: why Docker has its own rules

Containers add another, clearly delimited scope level: a Docker container by default inherits none of the host's environment variables, neither system wide nor user specific, but starts with a minimal environment dictated by the container image. Variables are brought into the container scope exclusively via -e VARIABLE=value on docker run, via the environment section in a Compose file, or via ENV instructions inside the Dockerfile itself.

This complete isolation is a deliberate security and reproducibility feature: a container should always receive the same, explicitly defined environment regardless of which host it runs on and which environment variables happen to be set there. For production Magento setups with Docker this means all PHP and database variables must be consistently maintained through the Compose file or a separate .env file, instead of relying on any host configuration.

8. Practical example: scoping PATH and PHP variables for Magento correctly

A typical practical problem in Magento hosting setups is a PATH that correctly points to a specific PHP version in the administrator's interactive shell but suddenly uses the wrong, system wide PHP version in a bin/magento call started via cron. The cause is almost always the same scope mistake: the PATH addition was only set in the interactive user's .bashrc, while the cron job starts as a non interactive, non login shell that does not read that file at all.

The robust solution is to either set the needed PATH addition directly in the crontab line itself, or use a central file included via BASH_ENV that reaches both interactive and non interactive contexts. For systemd controlled cron alternatives like timer units, the variable belongs directly in the unit file via Environment=, keeping the scope consistent regardless of the executing user account.

Scope Level Defined In Visible To Reaches Cron/systemd
System wide (PAM) /etc/environment All users, all logins No
System wide (login shell) /etc/profile.d/*.sh All users, login shells only No
User specific ~/.bashrc, ~/.profile Only this user No
Non interactive BASH_ENV file Scripts, cron via BASH_ENV Yes, with BASH_ENV
systemd unit Environment=, EnvironmentFile= Only this service Yes, directly

9. Debugging and comparison table of scope levels

To check the actual scope of a variable in a specific process, one reads its environment directly from the kernel via cat /proc/PID/environ | tr '\0' '\n', which is especially more reliable than any assumption based on configuration files for running PHP-FPM workers or systemd services. For systemd units, systemctl show SERVICENAME -p Environment shows exactly the variables the service actually received at its last start, including all values loaded via EnvironmentFile=.

Mironsoft

Linux server administration and configuration management for PHP hosting

Configuration values that reliably reach where PHP needs them?

We bring order to scattered environment variables across shell, systemd and PHP-FPM pools and make sure Magento deployments and cron jobs see consistent configuration, regardless of which user starts them.

Scope Audit

Analysis of every definition location and its actual reach in production

systemd and FPM Configuration

Clean Environment= and env[] directives for reproducible deployments

Docker Migration

Moving scattered host variables into explicit Compose and .env files

10. Summary

Environment variables are inherited exclusively from parent to child processes, never backward and never across independent process trees. System wide scope via /etc/environment and /etc/profile.d/ differs fundamentally from user specific scope in .bashrc and .profile, because the former is PAM based and the latter shell based. Without export, an assignment stays a local shell variable that never gets passed on to child processes.

systemd services and Docker containers each have a completely own scope decoupled from the shell, filled exclusively via explicit directives like Environment= or -e. Anyone who understands these scope levels and consistently defines variables in the right place, instead of relying on shell configuration that happens to work, avoids the majority of configuration mistakes in production PHP and Magento environments.

Environment Variable Scope: The Key Facts at a Glance

Inheritance

Only from parent to child processes, never backward. Changes in the child process stay local.

System Wide vs. User

/etc/environment via PAM reaches more contexts than /etc/profile.d/, which only affects login shells.

Don't Forget export

Without export, an assignment stays a local shell variable, invisible to every child process.

systemd & Docker

Own scope decoupled from the shell via Environment=, EnvironmentFile= and -e.

11. FAQ: Environment Variable Scope

1What does scope mean here?
The range in which a variable is visible, depending on where it is defined and the process tree.
2PHP-FPM doesn't see shell variable?
PHP-FPM is not a child process of the shell, it is started by systemd.
3/etc/environment vs. profile.d?
environment via PAM is shell independent, profile.d only for login shells via /etc/profile.
4getenv() returns nothing?
Likely export is missing, the variable stays local to the shell.
5Set a variable for a systemd service?
Via Environment= or EnvironmentFile= directly in the unit file.
6FPM workers inherit automatically?
Not fully, additionally use env[VARIABLE] in the pool configuration.
7Docker doesn't see host variables?
Containers are deliberately isolated, variables must be set explicitly via -e or Compose.
8Find the real process environment?
cat /proc/PID/environ reads the real environment directly from the kernel.
9PATH wrong in cron job?
Cron doesn't read .bashrc, set PATH directly in the crontab or via BASH_ENV.
10Check a service's environment?
systemctl show SERVICENAME -p Environment shows the actually set values.