envfile-compat: eine .env-Datei für docker run und Compose

Ein kleines Werkzeug, das zeigt, wo docker run --env-file und Docker Compose dieselbe .env-Datei unterschiedlich lesen, und die Datei in die Form umwandelt, die docker run erwartet.

Das Problem

docker run --env-file und der Schlüssel env_file: von Docker Compose lesen beide .env-Dateien, folgen dabei aber unterschiedlichen Regeln. docker run übernimmt jeden Wert wörtlich: Anführungszeichen, # Kommentare am Zeilenende und Leerzeichen am Ende werden Teil des Werts, und Zeilen mit export oder KEY = value sind Fehler. Compose und Bash entfernen die Anführungszeichen, verwerfen Kommentare am Zeilenende und setzen ${VAR} ein. Eine Datei, die mit source .env und mit Compose funktioniert, übergibt einem mit docker run gestarteten Container daher "My App" samt Anführungszeichen.

Das Problem ist dokumentiert:

  • docker/cli#3630, "Handle quotes in --env-file values consistently with Linux/WSL2 sourcing", hat 134 Reaktionen und 58 Kommentare und ist seit 2022 offen.
  • Drei Pull Requests, die den Parser ändern, sind offen und nicht gemergt (#3660, #6046, #6805). Eine Änderung des Parsers würde verändern, wie bestehende Dateien gelesen werden.
  • Die im Issue beschriebene Umgehung ist eine zweite Kopie jeder .env-Datei ohne Anführungszeichen. dotenv-linter prüft den Stil. Wie jedes Werkzeug einen Wert liest, zeigt er nicht.

Wie es funktioniert

envfile-compat enthält ein Modell jedes Lesers. Das Modell für docker run folgt den Regeln von pkg/kvfile in docker/cli: Leerraum am Zeilenanfang wird entfernt, #-Zeilen werden übersprungen, der Schlüssel endet am ersten =, und alles danach ist der Wert. Eine Zeile ohne = übernimmt ihren Wert aus der Umgebung. Das Compose-Modell folgt dem dotenv-Parser von compose-go. Es behandelt das Präfix export, KEY: value, einfache Anführungszeichen (wörtlich), doppelte Anführungszeichen (Escapes, Variablen, Zeilenumbrüche) und Werte ohne Anführungszeichen mit #-Kommentaren. Verweise wie ${VAR} und ${VAR:-default} werden wie in Compose aufgelöst, zuerst aus der aufrufenden Umgebung, dann aus früheren Zeilen der Datei.

envfile-compat check FILE liest die Datei mit beiden Modellen und listet jede Variable mit unterschiedlichen Werten sowie jede Zeile, die docker run ablehnt. Der Exit-Code ist 1, sobald sich etwas unterscheidet, die Prüfung kann also im CI laufen. Mit --bash wird die Datei zusätzlich in einer leeren bash mit set -a eingelesen; dabei wird sie ausgeführt. envfile-compat docker FILE gibt die Variablen so aus, wie Compose sie liest, in der wörtlichen Form KEY=value, die docker run erwartet.

Das ist die Ausgabe für eine Datei mit drei Zeilen:

$ cat .env
APP_NAME="My App"
GREETING=hello # say hi
PORT=8080

$ envfile-compat check .env
APP_NAME
  docker run  '"My App"'
  compose     'My App'
GREETING
  docker run  'hello # say hi'
  compose     'hello'

2 variable(s) differ. `envfile-compat docker .env` writes a file that
docker run --env-file reads the way Compose does.

$ envfile-compat docker .env
APP_NAME=My App
GREETING=hello
PORT=8080

Die umgewandelte Datei lässt sich direkt übergeben, zum Beispiel mit docker run --env-file <(envfile-compat docker .env) myimage. Das Werkzeug braucht Python 3.10 oder neuer und hat keine Abhängigkeiten.

Tests

Die Unit-Tests lassen beide Modelle über eine Korpus-Datei laufen. Sie enthält Fälle zu Anführungszeichen, Kommentaren, Escapes und Variablen, eine export-Zeile, KEY = value, KEY: value, eine eingerückte Zeile, Leerzeichen am Zeilenende und einen Schlüssel ohne Wert. Weitere Tests decken die Operatoren der Variablenersetzung ab (-, :-, +, :+, :?), mehrzeilige Werte, nicht geschlossene Anführungszeichen, den Bash-Leser und die Umwandlung: Die Ausgabe von envfile-compat docker muss wieder dieselben Werte ergeben.

Eine zweite Gruppe von Tests vergleicht die Modelle mit den echten Werkzeugen. docker compose config muss für den Korpus genau die Werte des Compose-Modells liefern, und env in einem mit docker run --env-file gestarteten Container genau die Werte des docker run-Modells. Auch die umgewandelte Datei muss durch docker run unverändert gelesen werden. Ohne Docker werden diese Tests übersprungen. Das CI führt die Tests auf Python 3.10 bis 3.13 mit Docker Compose v2 und dem Image alpine:3.20 aus.

Grenzen

  • Werte mit Zeilenumbrüchen lassen sich in dem Format, das docker run liest, nicht ausdrücken. envfile-compat docker meldet sie als Fehler.
  • check --bash führt die Datei aus und ist nur für vertrauenswürdige Dateien gedacht.
  • Die beiden Leser sind Nachbauten. Ihre Übereinstimmung mit Docker ist für die Fälle im Testkorpus geprüft.

Code und Installationsanleitung liegen unter github.com/Jwrede/envfile-compat.