wer eine Variable sieht, und wer nicht
Eine Umgebungsvariable, die in der Shell des Administrators funktioniert, aber im PHP-FPM-Prozess einer Magento-Anwendung schlicht fehlt, ist eines der häufigsten Rätsel im Serveralltag. Der Grund liegt fast immer im Scope: Umgebungsvariablen werden nur an Kindprozesse vererbt, niemals rückwärts oder quer zu unabhängigen Prozessbäumen, und systemweiter Scope funktioniert grundlegend anders als benutzerspezifischer Scope.
Inhaltsverzeichnis
- 1. Warum Scope bei Umgebungsvariablen entscheidend ist
- 2. Prozessvererbung: wie Variablen an Kindprozesse weitergegeben werden
- 3. Systemweiter Scope: /etc/environment und /etc/profile.d
- 4. Benutzerspezifischer Scope: bashrc, profile und bash_profile
- 5. export vs. lokale Shell-Variable: der entscheidende Unterschied
- 6. systemd-Services und ihr eigener Umgebungs-Scope
- 7. Container-Scope: warum Docker eigene Regeln hat
- 8. Praxisbeispiel: PATH und PHP-Variablen für Magento richtig scopen
- 9. Debugging und Vergleichstabelle der Scope-Ebenen
- 10. Zusammenfassung
- 11. FAQ
1. Warum Scope bei Umgebungsvariablen entscheidend ist
Eine Umgebungsvariable ist ein einfacher Schlüssel-Wert-Eintrag, den ein Prozess von seinem Elternprozess erbt und der ihm zur Laufzeit zur Verfügung steht. Der Scope einer Variable, also der Bereich, in dem sie tatsächlich sichtbar ist, hängt vollständig davon ab, wo sie definiert wurde und über welchen Prozessbaum sie weitergegeben wird. Genau dieser Zusammenhang ist die Ursache für eines der häufigsten Betriebsprobleme: eine Variable, die in der interaktiven Shell funktioniert, fehlt im gleichzeitig laufenden Webserver-Prozess, weil beide Prozesse aus völlig unterschiedlichen Elternprozessen hervorgegangen sind.
Wer Umgebungsvariablen produktiv einsetzt, etwa für Datenbankzugänge, API-Schlüssel oder PHP-Konfiguration, muss deshalb genau wissen, welcher Prozessbaum welche Variable überhaupt sehen kann. Der systemweite Scope und der benutzerspezifische Scope unterscheiden sich dabei nicht nur im Speicherort der Definition, sondern auch darin, welche Prozesse diese Definitionen jemals zu Gesicht bekommen, ein Unterschied, der in produktiven Umgebungen wie Magento-Hosting-Setups schnell zu schwer nachvollziehbaren Fehlern führt.
Diese Grundregel gilt unabhängig davon, ob eine Variable über export in einer Shell, über eine Konfigurationsdatei oder über eine Direktive in einer Unit-Datei gesetzt wurde: Der Prozessbaum entscheidet, nicht der Definitionsort allein. Erst das Zusammenspiel aus Definitionsort und tatsächlicher Elternprozess-Beziehung legt den endgültigen Scope einer Variable fest.
Ein zusätzlicher Aspekt, der den Scope beeinflusst, ist der Zeitpunkt der Auswertung: Manche Variablen werden nur einmalig beim Start eines Prozesses gelesen, während andere bei jedem Aufruf einer Funktion neu ausgewertet werden. Eine Änderung an /etc/environment etwa wirkt sich erst auf neue Sitzungen aus, niemals auf bereits laufende Prozesse, selbst wenn diese theoretisch berechtigt wären, die Datei zu lesen. Dieses Detail führt in der Praxis oft zu der irrigen Annahme, eine Konfigurationsänderung sei wirkungslos geblieben, obwohl lediglich der betroffene Dienst noch nicht neu gestartet wurde.
2. Prozessvererbung: wie Variablen an Kindprozesse weitergegeben werden
Der zentrale Mechanismus hinter jedem Scope ist die Prozessvererbung: Wenn ein Prozess einen neuen Kindprozess über fork() und exec() erzeugt, erhält dieser Kindprozess standardmäßig eine Kopie der gesamten Umgebung des Elternprozesses. Diese Vererbung funktioniert ausschließlich in eine Richtung, von oben nach unten im Prozessbaum, niemals rückwärts. Ändert ein Kindprozess eine geerbte Variable, wirkt sich das ausschließlich auf seine eigene Kopie und auf seine eigenen späteren Kindprozesse aus, niemals auf den Elternprozess selbst.
Diese Einbahnstraße erklärt ein häufiges Missverständnis: Setzt man in einem laufenden Terminal eine Variable mit export und startet danach ein Skript, sieht das Skript diese Variable, weil es ein Kindprozess der Shell ist. Startet man dagegen ein bereits laufendes Programm neu über systemctl restart, erbt der neue Prozess nicht die Umgebung der interaktiven Shell, sondern die Umgebung von systemd selbst als seinem tatsächlichen Elternprozess, weshalb in der Shell gesetzte Variablen dort grundsätzlich nicht ankommen.
# 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. Systemweiter Scope: /etc/environment und /etc/profile.d
Für Variablen, die für jeden Benutzer und jede Login-Sitzung auf einem Server gelten sollen, ist /etc/environment der zentrale Ort. Diese Datei enthält einfache SCHLUESSEL=Wert-Zuweisungen ohne Shell-Syntax, wird nicht von der Shell interpretiert, sondern direkt von pam_env.so beim Login gelesen, und gilt deshalb unabhängig von der verwendeten Shell, ob Bash, Zsh oder Dash. Genau das macht sie zum robustesten Ort für systemweite Definitionen, weil sie nicht auf Bash-spezifische Syntax angewiesen ist.
Für komplexere systemweite Logik, etwa bedingte Zuweisungen oder Variablen, die von installierten Paketen stammen, ist /etc/profile.d/ der bessere Ort, weil dort echte Shell-Skripte mit voller Syntax liegen können. Der entscheidende Unterschied: Dateien in /etc/profile.d/ werden nur von /etc/profile eingebunden und greifen deshalb ausschließlich bei Login-Shells, während /etc/environment systemweit über PAM greift und damit auch grafische Desktop-Sitzungen sowie einige Nicht-Shell-Kontexte erreicht.
# /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. Benutzerspezifischer Scope: bashrc, profile und bash_profile
Der benutzerspezifische Scope lebt in den Startdateien im Home-Verzeichnis des jeweiligen Benutzers: ~/.bashrc, ~/.profile oder ~/.bash_profile, je nachdem ob es sich um eine Login- oder Non-Login-Shell handelt. Variablen, die hier definiert werden, sind ausschließlich für diesen einen Benutzer sichtbar und wirken sich nicht auf andere Systemkonten aus, selbst wenn diese denselben Dienst betreiben. Das ist bewusst so gestaltet, weil unterschiedliche Benutzer auf demselben Server durchaus unterschiedliche Werkzeugversionen, API-Schlüssel oder Debug-Einstellungen benötigen können.
In der Praxis führt dieser benutzerspezifische Scope häufig zu Verwirrung bei Deployment-Prozessen: Läuft ein Deployment-Skript unter dem Benutzer deploy, aber der eigentliche PHP-FPM-Prozess unter dem Benutzer www-data, hilft es nichts, eine Variable in der .bashrc von deploy zu definieren, weil www-data niemals eine interaktive Shell mit dieser Datei startet. Für Variablen, die über Benutzergrenzen hinweg gelten müssen, ist deshalb grundsätzlich der systemweite Scope oder eine explizite Konfiguration auf Ebene des jeweiligen Dienstes die richtige Wahl.
5. export vs. lokale Shell-Variable: der entscheidende Unterschied
Innerhalb einer einzelnen Shell-Sitzung gibt es eine weitere, oft übersehene Scope-Ebene: den Unterschied zwischen einer einfachen Shell-Variable und einer exportierten Umgebungsvariable. Eine Zuweisung wie VARIABLE=wert ohne export erzeugt lediglich eine lokale Shell-Variable, die für die aktuelle Shell selbst sichtbar ist, aber niemals an Kindprozesse weitergegeben wird. Erst export VARIABLE=wert markiert die Variable dafür, Teil der Umgebung zu werden, die an jeden künftigen Kindprozess dieser Shell vererbt wird.
Dieser Unterschied erklärt einen klassischen Fehler in Deployment-Skripten: Ein Skript setzt DB_PASSWORD=geheim ohne export und ruft anschließend ein PHP-Skript auf, das über getenv('DB_PASSWORD') auf die Variable zugreifen will. Das PHP-Skript erhält einen leeren Wert, weil die Variable niemals in die Umgebung des Kindprozesses gelangt ist, obwohl sie in der aufrufenden Shell mit echo $DB_PASSWORD problemlos sichtbar war. Diese Falle ist einer der häufigsten Gründe für scheinbar zufällig fehlende Konfigurationswerte in produktiven Skripten.
# 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 und ihr eigener Umgebungs-Scope
systemd-Dienste haben einen vollständig eigenen Scope, der komplett unabhängig von jeder Shell-Konfiguration ist. Ein systemd-Service erbt weder /etc/environment automatisch noch irgendeine Bash-Startdatei, sondern bezieht seine Umgebung ausschließlich aus den Direktiven Environment= für einzelne Zuweisungen und EnvironmentFile= für eine externe Datei mit mehreren Zuweisungen, jeweils direkt in der Unit-Datei des Dienstes. Diese strikte Trennung ist beabsichtigt: Sie sorgt dafür, dass ein Dienst reproduzierbar dieselbe Umgebung erhält, unabhängig davon, welcher Benutzer den Dienst gerade zufällig neu startet.
Für PHP-FPM-Pools kommt zusätzlich eine eigene Ebene hinzu: Die env[VARIABLE]-Direktive in der Pool-Konfiguration definiert Variablen, die ausschließlich an PHP-Worker-Prozesse dieses spezifischen Pools weitergegeben werden, unabhängig vom systemd-Scope des PHP-FPM-Hauptprozesses selbst. Diese verschachtelte Scope-Struktur, systemd-Unit, dann FPM-Pool, dann PHP-Worker, ist der Grund, warum eine für PHP benötigte Variable oft an drei verschiedenen Stellen konfiguriert werden könnte, aber nur eine davon tatsächlich beim Anwendungscode ankommt.
# /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: warum Docker eigene Regeln hat
Container fügen eine weitere, klar abgegrenzte Scope-Ebene hinzu: Ein Docker-Container erbt standardmäßig keine der Umgebungsvariablen des Hosts, weder systemweite noch benutzerspezifische, sondern startet mit einer minimalen, vom Container-Image vorgegebenen Umgebung. Variablen werden ausschließlich über -e VARIABLE=wert beim docker run, über die environment-Sektion in einer Compose-Datei, oder über ENV-Anweisungen im Dockerfile selbst in den Container-Scope eingebracht.
Diese vollständige Isolation ist ein bewusstes Sicherheits- und Reproduzierbarkeitsmerkmal: Ein Container soll unabhängig davon, auf welchem Host er läuft und welche Umgebungsvariablen dort zufällig gesetzt sind, immer dieselbe, explizit definierte Umgebung erhalten. Für produktive Magento-Setups mit Docker bedeutet das, dass sämtliche PHP- und Datenbank-Variablen konsequent über die Compose-Datei oder ein separates .env-File gepflegt werden müssen, statt sich auf irgendeine Host-Konfiguration zu verlassen.
8. Praxisbeispiel: PATH und PHP-Variablen für Magento richtig scopen
Ein typisches Praxisproblem bei Magento-Hosting-Setups ist ein PATH, der in der interaktiven Shell des Administrators korrekt auf eine bestimmte PHP-Version zeigt, aber im über Cron gestarteten bin/magento-Aufruf plötzlich die falsche, systemweite PHP-Version verwendet. Die Ursache ist fast immer derselbe Scope-Fehler: Der PATH-Zusatz wurde nur in der .bashrc des interaktiven Benutzers gesetzt, während der Cron-Job als nicht interaktive, nicht anmeldende Shell startet und diese Datei gar nicht liest.
Die robuste Lösung besteht darin, den benötigten PATH-Zusatz entweder direkt in der Crontab-Zeile selbst zu setzen, oder eine zentrale, über BASH_ENV eingebundene Datei zu verwenden, die sowohl interaktive als auch nicht interaktive Kontexte erreicht. Für systemd-gesteuerte Cron-Alternativen wie Timer-Units gehört die Variable direkt in die Unit-Datei über Environment=, wodurch der Scope unabhängig vom ausführenden Benutzerkonto konsistent bleibt.
| Scope-Ebene | Definitionsort | Sichtbar für | Erreicht Cron/systemd |
|---|---|---|---|
| Systemweit (PAM) | /etc/environment | Alle Benutzer, alle Logins | Nein |
| Systemweit (Login-Shell) | /etc/profile.d/*.sh | Alle Benutzer, nur Login-Shells | Nein |
| Benutzerspezifisch | ~/.bashrc, ~/.profile | Nur dieser Benutzer | Nein |
| Nicht interaktiv | BASH_ENV-Datei | Skripte, Cron über BASH_ENV | Ja, mit BASH_ENV |
| systemd-Unit | Environment=, EnvironmentFile= | Nur dieser Dienst | Ja, direkt |
9. Debugging und Vergleichstabelle der Scope-Ebenen
Um den tatsächlichen Scope einer Variable in einem bestimmten Prozess zu prüfen, liest man dessen Umgebung direkt aus dem Kernel über cat /proc/PID/environ | tr '\0' '\n', was insbesondere bei laufenden PHP-FPM-Workern oder systemd-Diensten zuverlässiger ist als jede Vermutung basierend auf Konfigurationsdateien. Für systemd-Units zeigt systemctl show DIENSTNAME -p Environment exakt die Variablen, die der Dienst tatsächlich beim letzten Start erhalten hat, inklusive aller über EnvironmentFile= nachgeladenen Werte.
Mironsoft
Linux-Serveradministration und Konfigurationsmanagement für PHP-Hosting
Konfigurationswerte, die zuverlässig dort ankommen, wo PHP sie braucht?
Wir bringen Ordnung in verstreute Umgebungsvariablen über Shell, systemd und PHP-FPM-Pools hinweg und sorgen dafür, dass Magento-Deployments und Cron-Jobs konsistente Konfiguration sehen, egal welcher Benutzer sie startet.
Scope-Audit
Analyse aller Definitionsorte und ihrer tatsächlichen Reichweite im Betrieb
systemd- und FPM-Konfiguration
Saubere Environment=- und env[]-Direktiven für reproduzierbare Deployments
Docker-Umstellung
Migration verstreuter Host-Variablen in explizite Compose- und .env-Dateien
10. Zusammenfassung
Umgebungsvariablen werden ausschließlich von Eltern- zu Kindprozessen vererbt, niemals umgekehrt und niemals quer zu unabhängigen Prozessbäumen. Der systemweite Scope über /etc/environment und /etc/profile.d/ unterscheidet sich fundamental vom benutzerspezifischen Scope in .bashrc und .profile, weil ersterer PAM-basiert und zweiterer Shell-basiert funktioniert. Ohne export bleibt eine Zuweisung eine lokale Shell-Variable, die niemals an Kindprozesse weitergegeben wird.
systemd-Dienste und Docker-Container besitzen jeweils einen komplett eigenen, von der Shell entkoppelten Scope, der ausschließlich über explizite Direktiven wie Environment= oder -e gefüllt wird. Wer diese Scope-Ebenen kennt und Variablen konsequent an der richtigen Stelle definiert, statt sich auf zufällig funktionierende Shell-Konfiguration zu verlassen, vermeidet den Großteil der Konfigurationsfehler in produktiven PHP- und Magento-Umgebungen.
Umgebungsvariablen-Scope: Das Wichtigste auf einen Blick
Vererbung
Nur von Eltern- zu Kindprozessen, niemals rückwärts. Änderungen im Kindprozess bleiben lokal.
Systemweit vs. Nutzer
/etc/environment über PAM erreicht mehr Kontexte als /etc/profile.d/, das nur Login-Shells betrifft.
export nicht vergessen
Ohne export bleibt eine Zuweisung eine lokale Shell-Variable, unsichtbar für jeden Kindprozess.
systemd & Docker
Eigener, von der Shell entkoppelter Scope über Environment=, EnvironmentFile= und -e.