Wann ein Wert ins .env gehört, wann in einen Parameter, und warum Docker diese Trennung gerne durcheinanderbringt
Symfony bietet mit der Dotenv-Komponente und den Parameter Bags zwei unterschiedliche Mechanismen für Konfiguration, die auf den ersten Blick austauschbar wirken, aber unterschiedliche Zwecke erfüllen. Werden beide vermischt, entstehen typische Probleme: Secrets landen im Git-Repository, dieselbe URL wird an drei Stellen gepflegt, oder eine Docker-Umgebungsvariable überschreibt in Produktion still einen Wert, den ein Entwickler eigentlich in der .env.local gesetzt hatte. Dieser Artikel klärt die tatsächliche .env-Hierarchie, zeigt eine klare Faustregel für die Trennung von .env und Parametern und erklärt, warum sensible Werte in Produktion in den Secrets-Vault statt in eine Klartext-.env gehören.
Inhaltsverzeichnis
- 1. Die .env-Hierarchie im Detail
- 2. .env vs. Parameter: eine klare Faustregel
- 3. Die env()-Funktion und ihre Prozessoren in der Praxis
- 4. Secrets-Vault statt Klartext-.env in Produktion
- 5. Warum .env eingecheckt wird und was composer dump-env bewirkt
- 6. Häufige Fallstricke bei Docker-Umgebungsvariablen
- 7. Den Parameter Bag zur Laufzeit gezielt nutzen
- 8. Tests und .env.test sinnvoll nutzen
- 9. Eine praktische Checkliste für saubere Konfigurationstrennung
- 10. Zusammenfassung
- 11. FAQ
1. Die .env-Hierarchie im Detail
Ein frisches Symfony-Projekt bringt bis zu vier verschiedene .env-Dateien mit, die jeweils einen klar definierten Zweck erfüllen. Die Datei .env enthält die projektweiten Standardwerte und wird ins Repository eingecheckt, damit jeder Entwickler nach einem git clone eine funktionierende Grundkonfiguration hat, typischerweise mit einer lokalen SQLite- oder Docker-DSN als Default. Die Datei .env.local enthält lokale, maschinenspezifische Überschreibungen, wird niemals eingecheckt und steht in der .gitignore, denn sie enthält häufig echte, individuelle Zugangsdaten für die lokale Entwicklungsumgebung eines einzelnen Entwicklers.
Zusätzlich gibt es Umgebungs-spezifische Varianten: .env.$APP_ENV, also etwa .env.test, wird eingecheckt und enthält Werte, die für eine bestimmte Umgebung wie Tests gelten sollen, zum Beispiel eine abweichende Test-Datenbank-DSN. Die Datei .env.$APP_ENV.local kombiniert beides, ist Umgebungs-spezifisch und lokal zugleich, wird ebenfalls nie eingecheckt und eignet sich für lokale Überschreibungen, die nur in einer bestimmten Umgebung gelten sollen. Die Ladereihenfolge ist wichtig: Symfony lädt .env zuerst, dann .env.local, dann .env.$APP_ENV und zuletzt .env.$APP_ENV.local, wobei später geladene Dateien früher gesetzte Werte überschreiben, und echte, im Betriebssystem oder im Container gesetzte Umgebungsvariablen haben letztlich immer Vorrang vor allen .env-Dateien.
2. .env vs. Parameter: eine klare Faustregel
Die entscheidende Frage lautet nicht 'ist der Wert geheim', sondern 'unterscheidet sich der Wert zwischen Umgebungen'. Eine Datenbank-DSN, eine Mailer-DSN, ein API-Schlüssel eines Drittanbieters oder eine Basis-URL sind zwischen lokaler Entwicklung, Staging und Produktion naturgemäß unterschiedlich und gehören deshalb ins .env-System, wo sie über die env()-Funktion in der Konfiguration referenziert werden können. Ein Wert, der dagegen in jeder Umgebung identisch ist, etwa die maximale Anzahl an Versandversuchen für eine Bestellbenachrichtigung oder ein Feature-Flag, das im gesamten Projekt einheitlich gilt, gehört als regulärer Parameter in config/services.yaml unter den Schlüssel parameters.
Diese Trennung hat einen praktischen Grund: Parameter in services.yaml sind Teil des versionierten Codes und ändern sich nur über einen expliziten Commit und Code-Review, während .env-Werte bewusst außerhalb dieses Prozesses pro Umgebung variieren dürfen sollen. Wer einen umgebungsunabhängigen Business-Wert fälschlich ins .env packt, verliert die Nachvollziehbarkeit über Git und riskiert, dass verschiedene Umgebungen ungewollt unterschiedliche fachliche Regeln anwenden. Wer umgekehrt eine Datenbank-URL fest als Parameter in services.yaml hinterlegt, zwingt jede Umgebung dazu, den Code selbst zu ändern, um eine andere Datenbank anzusprechen, was dem eigentlichen Zweck von Konfiguration widerspricht.
3. Die env()-Funktion und ihre Prozessoren in der Praxis
In services.yaml wird eine Umgebungsvariable über die Syntax %env(VARIABLE_NAME)% referenziert, wobei Symfony den Wert automatisch als String interpretiert, sofern kein Prozessor vorangestellt wird. Für andere Typen stehen Prozessoren wie int, bool, float oder json zur Verfügung, etwa %env(int:MAX_RETRIES)%, die den rohen String-Wert der Umgebungsvariable in den gewünschten PHP-Typ konvertieren, bevor er als Parameter im Container ankommt. Seit einigen Symfony-Versionen lassen sich diese Prozessoren auch direkt am Konstruktor-Parameter mit dem Attribut Autowire und dem env-Argument setzen, ohne den Umweg über eine explizite services.yaml-Parameterdefinition.
Das folgende Beispiel zeigt einen Service, der zwei Umgebungsvariablen direkt per Attribut injiziert bekommt, eine als String und eine als Boolean, beide typisiert und ohne zusätzliche YAML-Konfiguration. Dieser Ansatz reduziert die Indirektion deutlich, weil man beim Lesen der Klasse sofort sieht, welche Umgebungsvariablen sie tatsächlich benötigt, statt in einer separaten services.yaml-Datei danach suchen zu müssen.
<?php
declare(strict_types=1);
namespace App\Mailer;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final class NotificationMailer
{
public function __construct(
#[Autowire(env: 'string:MAILER_FROM_ADDRESS')]
private readonly string $fromAddress,
#[Autowire(env: 'bool:MAILER_SANDBOX_MODE')]
private readonly bool $sandboxMode,
) {
}
public function getFromAddress(): string
{
return $this->fromAddress;
}
public function isSandboxMode(): bool
{
return $this->sandboxMode;
}
}
4. Secrets-Vault statt Klartext-.env in Produktion
Sensible Werte wie API-Schlüssel, Signatur-Geheimnisse oder Datenbank-Passwörter sollten in Produktion nicht als Klartext in einer .env.local oder .env.prod.local auf dem Server liegen, selbst wenn diese Datei nicht im Git-Repository landet. Der Grund ist simpel: Jeder mit Dateisystem-Zugriff auf den Server, jedes Backup und jedes versehentlich mitkopierte Deployment-Artefakt legt das Geheimnis offen, ohne dass ein zusätzlicher Schutzmechanismus greift. Symfony bietet dafür seit einigen Jahren den Secrets-Vault, verwaltet über die Konsolenbefehle secrets:set, secrets:list und secrets:decrypt-to-local, der Werte verschlüsselt im Verzeichnis config/secrets/%kernel.environment%/ ablegt.
Der Vault nutzt ein asymmetrisches Schlüsselpaar: Der öffentliche Schlüssel liegt im Repository und erlaubt jedem Entwickler, neue Secrets zu verschlüsseln, während der private Schlüssel ausschließlich auf dem Produktionsserver liegt oder über die Umgebungsvariable SYMFONY_DECRYPTION_SECRET bereitgestellt wird, typischerweise durch einen separaten, restriktiv abgesicherten Deployment-Schritt. Ein im Vault gespeichertes Secret wird in der Konfiguration exakt wie eine gewöhnliche Umgebungsvariable über %env(SOME_SECRET)% referenziert, denn Symfony löst Secrets transparent über denselben env()-Mechanismus auf. Dadurch muss bestehender Code beim Umstieg von einer .env-Variable auf ein Vault-Secret nicht angepasst werden, nur die Quelle des Werts ändert sich.
5. Warum .env eingecheckt wird und was composer dump-env bewirkt
Ein häufiges Missverständnis ist die Annahme, .env sei per se eine geheime Datei. Tatsächlich ist die Basisdatei .env explizit dafür gedacht, ins Repository eingecheckt zu werden, damit ein neues Teammitglied sofort eine lauffähige Konfiguration mit sinnvollen Platzhaltern und Default-Werten vorfindet. Echte Geheimnisse gehören dort nur als offensichtlicher Platzhalter hinein, etwa DATABASE_URL=mysql://app:app@127.0.0.1:3306/app, während das tatsächliche Produktions-Passwort ausschließlich in der lokalen .env.local des jeweiligen Servers oder im Secrets-Vault landet.
In Produktion empfiehlt Symfony zusätzlich, die .env-Dateien nicht bei jedem Request neu zu parsen, sondern sie einmalig beim Deployment über composer dump-env prod in eine optimierte .env.local.php-Datei zu kompilieren, die als reines PHP-Array deutlich schneller geladen wird. Diese Datei ersetzt dann effektiv das Parsen aller .env-Dateien zur Laufzeit, wobei echte, im Container oder Betriebssystem gesetzte Umgebungsvariablen weiterhin Vorrang behalten. Wird composer dump-env vergessen, funktioniert die Anwendung zwar weiterhin korrekt, verliert aber unnötig Performance, weil bei jedem Request erneut alle .env-Dateien vom Dateisystem gelesen und geparst werden müssen.
6. Häufige Fallstricke bei Docker-Umgebungsvariablen
Der häufigste Fallstrick in containerisierten Setups ist, zu vergessen, dass echte Prozess-Umgebungsvariablen immer Vorrang vor allen .env-Dateien haben. Setzt eine docker-compose.yaml unter environment einen Wert wie APP_ENV=prod oder DATABASE_URL, überschreibt dieser Wert jede lokale .env.local, selbst wenn ein Entwickler in dieser Datei bewusst einen abweichenden Wert für lokales Debugging hinterlegt hat. Das führt zu dem verwirrenden Symptom, dass eine Änderung in .env.local scheinbar wirkungslos bleibt, obwohl die Datei syntaktisch korrekt ist und normalerweise gelesen wird.
Ein zweiter häufiger Fehler ist die Vermischung von Build-Zeit- und Laufzeit-Umgebungsvariablen: Ein im Dockerfile per ARG gesetzter Wert steht nur während des Image-Builds zur Verfügung und muss explizit per ENV weitergereicht werden, damit er zur Laufzeit im Container sichtbar ist, was besonders bei mehrstufigen Docker-Builds leicht übersehen wird. Ein dritter Fallstrick betrifft env_file-Direktiven in docker-compose.yaml, die eine eigene .env-Datei für Docker selbst referenzieren, welche mit der von Symfony gelesenen .env-Hierarchie verwechselt werden kann. Docker Compose liest eine .env-Datei im Projektroot für die Variablensubstitution innerhalb der docker-compose.yaml selbst, was ein völlig anderer Mechanismus als Symfonys Dotenv-Komponente ist und leicht zu Verwirrung führt, wenn beide Dateien denselben Namen tragen.
7. Den Parameter Bag zur Laufzeit gezielt nutzen
Parameter aus services.yaml lassen sich nicht nur in der Konfiguration selbst referenzieren, sondern auch programmatisch über ParameterBagInterface abfragen, was sich für generische Services eignet, die eine variable Menge an Konfigurationswerten zur Laufzeit nachschlagen müssen. Der Zugriff erfolgt über $parameterBag->get('app.max_retries'), wobei der Parametername üblicherweise mit einem Namespace-Präfix wie app. versehen wird, um Kollisionen mit internen Symfony- oder Bundle-Parametern zu vermeiden. Für die meisten Fälle ist jedoch die direkte Injektion eines einzelnen Parameters über das Attribut Autowire mit dem param-Argument oder über ein Binding in services.yaml die klarere Wahl, weil sie die tatsächliche Abhängigkeit im Konstruktor sichtbar macht.
Ein häufiger Anwendungsfall für den direkten ParameterBagInterface-Zugriff ist ein Feature-Flag-Service, der zur Laufzeit dynamisch prüfen muss, ob ein bestimmter, namentlich noch unbekannter Parameter existiert, etwa beim Aufbau eines generischen Admin-Konfigurations-Dashboards. Für den Normalfall eines einzelnen, klar benannten Konfigurationswerts sollte man diesen generischen Zugriff aber vermeiden, weil er, ähnlich wie der volle Service-Container, die tatsächlich benötigten Werte einer Klasse verschleiert und PHPStan oder ähnliche statische Analysewerkzeuge den referenzierten Parameternamen nicht zur Compile-Zeit prüfen können.
8. Tests und .env.test sinnvoll nutzen
Für die Testumgebung lädt Symfony automatisch .env.test, sofern APP_ENV beim Testlauf auf test gesetzt ist, was PHPUnit-Bridge und der KernelTestCase-Bootstrap standardmäßig sicherstellen. Wichtig ist dabei ein Detail, das leicht übersehen wird: .env.local wird explizit nicht geladen, wenn APP_ENV auf test steht, damit lokale Entwickler-Überschreibungen nicht versehentlich in die CI-Pipeline durchsickern und dort zu nicht reproduzierbaren Testfehlern führen. Stattdessen greift ausschließlich .env.test.local für lokale, Testumgebungs-spezifische Überschreibungen, die ebenfalls nicht eingecheckt werden.
In CI-Umgebungen empfiehlt es sich, alle für Tests benötigten Umgebungsvariablen entweder explizit in .env.test einzuchecken, sofern es sich um unkritische Test-Fixtures handelt, oder über die CI-Plattform selbst als echte Prozess-Umgebungsvariablen zu setzen, wenn es sich um Zugangsdaten für eine echte externe Testinstanz handelt. Auf keinen Fall sollte eine CI-Pipeline auf eine lokale .env.local eines Entwicklers angewiesen sein, denn diese Datei existiert im CI-Kontext naturgemäß nicht und ihr Fehlen führt sonst zu schwer nachvollziehbaren, umgebungsabhängigen Testfehlern, die auf dem eigenen Rechner nicht reproduzierbar sind.
9. Eine praktische Checkliste für saubere Konfigurationstrennung
Als Faustregel für neue Konfigurationswerte hilft eine kurze Kette von Fragen: Ändert sich der Wert zwischen Umgebungen? Wenn nein, gehört er als Parameter in services.yaml. Wenn ja, ist der Wert geheim oder sicherheitsrelevant? Wenn ja, gehört er in den Secrets-Vault statt in eine Klartext-.env-Datei, insbesondere in Produktion. Ist der Wert nicht geheim, aber umgebungsspezifisch, etwa eine Basis-URL oder ein Feature-Toggle pro Stage, gehört er regulär in die passende .env.$APP_ENV-Datei oder in eine echte, vom Deployment gesetzte Umgebungsvariable.
Diese Checkliste vermeidet die häufigsten Fehler in gewachsenen Projekten: Geheimnisse, die versehentlich in einer eingecheckten .env landen, Business-Konstanten, die fälschlich als Umgebungsvariable gepflegt werden und dadurch ihre Nachvollziehbarkeit über Git verlieren, und Docker-Compose-Konfigurationen, die stillschweigend lokale Entwickler-Overrides überschreiben. Wer diese vier Fragen konsequent bei jedem neuen Konfigurationswert stellt, vermeidet die meisten der hier beschriebenen Fallstricke von vornherein, statt sie erst nach einem Produktionsvorfall zu entdecken.
| Datei / Mechanismus | Eingecheckt? | Typischer Inhalt |
|---|---|---|
| .env | Ja | Projektweite Defaults und Platzhalter |
| .env.local | Nein | Lokale, maschinenspezifische Überschreibungen |
| .env.$APP_ENV | Ja | Umgebungsspezifische Werte, z.B. .env.test |
| .env.$APP_ENV.local | Nein | Lokale Überschreibung für eine bestimmte Umgebung |
| Secrets-Vault | Verschlüsselt eingecheckt | API-Schlüssel, Passwörter, Signatur-Secrets |
| Parameter in services.yaml | Ja | Umgebungsunabhängige Business-Werte |
Mironsoft
Symfony-Architektur, saubere Domänenlogik und Legacy-Modernisierung
Symfony-Anwendungen, die nach zwei Jahren noch wartbar sind?
Wir prüfen bestehende Symfony-Projekte auf aufgeblähte Controller, fehlende Serviceabstraktionen und ungetestete Kernlogik und bauen daraus eine Architektur, die neue Features aufnimmt, ohne bei jedem Release brüchiger zu werden.
Architektur-Review
Bundle-Struktur, Dependency Injection und Serviceabstraktionen auf Wartbarkeit prüfen.
Legacy-Modernisierung
Schrittweise Migration veralteter Symfony-Versionen ohne kompletten Neuschrieb.
Test- und Qualitätssicherung
PHPUnit, PHPStan und CI-Pipelines für nachhaltige Codequalität aufbauen.
10. Zusammenfassung
Dotenv und Parameter Bags: Das Wichtigste auf einen Blick
.env-Hierarchie
Vier Dateien mit klar getrennter Ladereihenfolge, echte Umgebungsvariablen haben immer Vorrang.
Faustregel
Ändert sich der Wert zwischen Umgebungen, gehört er ins .env, sonst in einen Parameter.
Secrets-Vault
Sensible Werte in Produktion verschlüsselt ablegen statt als Klartext-.env auf dem Server.
Docker-Fallstrick
Prozess-Umgebungsvariablen aus docker-compose überschreiben .env.local lautlos.