compose-inline: Compose-Configs und -Secrets auf einem entfernten Docker-Host

Ein Wrapper um docker compose, der lokale Config- und Secret-Dateien als Inhalt übergibt, damit Projekte, die sie nutzen, auf einem entfernten Docker-Host starten.

Das Problem

Docker Compose setzt configs.*.file und secrets.*.file als Bind-Mounts um, und ein Bind-Mount wird auf dem Docker-Host aufgelöst. Spricht Compose mit einem entfernten Host (DOCKER_HOST=ssh://..., ein docker context oder tcp://...), muss die Datei dort unter demselben absoluten Pfad liegen. Sonst startet der Container nicht:

Error response from daemon: invalid mount config for type "bind":
bind source path does not exist: /home/me/project/secrets/db_password.txt

Das Problem ist dokumentiert:

  • docker/compose#11867, "Support remote DOCKER_HOST by copying configs and secrets files instead of bind mounting them", ist seit Mai 2024 offen und hat 45 Reaktionen und 50 Kommentare. Der Maintainer befürwortet die Richtung, möchte aber eine Opt-in-Syntax, weil Kopieren bestehendes Verhalten ändert. Fünf Pull Requests, die es umsetzten, wurden ungemergt geschlossen. Ein Kommentar wünscht sich ausdrücklich einen Weg, die Dateien nicht von Hand in die Eigenschaft content übertragen zu müssen.
  • Frühere Meldungen desselben Fehlers sind #7857, #11141 und #7110.
  • Per Bind-Mount eingebundene Configs und Secrets ignorieren uid, gid und mode (#9648, 30 Reaktionen).

Die Umgehungen im Thread sind: die Dateien auf den Docker-Host kopieren und dort absolute Pfade verwenden, pro Host eine eigene Override-Datei pflegen oder die Dateien in ein Image einbauen.

Wie es funktioniert

Compose hat bereits einen Codepfad, der entfernt funktioniert: Configs mit content: sowie Configs und Secrets mit environment: werden vor dem Start über die Docker-API in den Container geschrieben. compose-inline, ein Python-Werkzeug ohne Abhängigkeiten, leitet Dateien auf diesen Pfad um. Es liest das aufgelöste Modell mit docker compose config --format json. Jede Config, deren Datei auf dem lokalen Rechner existiert, wird zu content:, wobei $ maskiert wird, weil Compose das Modell erneut interpoliert. Secrets und Configs, die kein gültiges UTF-8 sind, werden zu environment:-Einträgen, deren Werte nur in der Umgebung des Kindprozesses stehen. Secret-Werte landen deshalb nie auf der Platte. Mit --bind-files werden Bind-Mounts einzelner Dateien im Projektordner zu Configs, mit den Dateirechten als mode.

Compose bezieht Config-Inhalte nicht in den Hash des Dienstes ein. Eine geänderte Datei würde beim nächsten up den Container also nicht neu erzeugen. compose-inline hängt deshalb an jeden betroffenen Dienst ein Label mit einem Hash der übergebenen Inhalte. Das umgeschriebene Modell schreibt es in eine private temporäre Datei und führt damit den gewünschten docker compose-Befehl aus.

Das ist das Beispiel aus dem README, ein Projekt mit einer Config, einem Secret und einer per Bind-Mount eingebundenen nginx.conf, ausgeführt gegen einen Host ohne die Projektdateien (Pfade im README gekürzt):

$ export DOCKER_HOST=ssh://deploy@server
$ docker compose up -d
 Container app-web-1 Error response from daemon: invalid mount config for type "bind": bind source path does not exist: /home/me/app/secrets/db_password.txt

$ compose-inline --bind-files --verbose up -d --wait
compose-inline: inlined config site from /home/me/app/site.conf (87 bytes)
compose-inline: inlined secret db_password from /home/me/app/secrets/db_password.txt (22 bytes)
compose-inline: inlined bind mount /home/me/app/nginx.conf:/etc/nginx/nginx.conf of service web (57 bytes)
 Container app-web-1 Created
 Container app-web-1 Started
 Container app-web-1 Healthy

Tests

18 Unit-Tests decken das Umschreiben und die Argumentverarbeitung ab: maskierte Dollarzeichen, Secrets und Nicht-UTF-8-Configs über die Umgebung, abgelehnte NUL-Bytes und zu große Secrets, fehlende Dateien und Verzeichnisse, die Bind-Mounts bleiben, --bind-files und ein Hash-Label, das sich mit dem Inhalt ändert.

Der End-to-End-Test läuft gegen einen docker:27-dind-Container, der über TCP erreicht wird und die Dateien des Checkouts nicht sieht. Er prüft, dass einfaches docker compose mit "bind source path does not exist" scheitert, dass compose-inline die Config, die eingebundene Datei und das Secret mit den Rechten 600 und 640 liefert, dass up den Container nur nach einer Änderung einer Datei neu erzeugt und dass ein Config-Pfad, der nur auf dem Docker-Host existiert, ein Bind-Mount bleibt. Das CI führt die Unit-Tests mit Python 3.9, 3.12 und 3.13 aus und den End-to-End-Test mit Compose v2.38.2 und v2.24.7. Die ältere Version liest einen mode-String als Dezimalzahl, deshalb schreibt das Werkzeug die Rechte als Ganzzahl.

Grenzen

  • Dateien werden einmal kopiert, beim Erzeugen des Containers. Eine lokale Änderung erreicht den Container erst beim nächsten up.
  • Compose verweigert das Kopieren in Dienste mit read_only: true.
  • Kopierte Dateien gehören root, sofern Config oder Secret nicht uid und gid setzen. Configs haben standardmäßig den Modus 0444.
  • Verzeichnisse und Pfade, die lokal nicht existieren, bleiben Bind-Mounts. --bind-files wandelt nur Dateien im Projektordner um.
  • Dateien mit NUL-Bytes werden abgelehnt. Secrets laufen über die Umgebung, die Linux auf 128 KiB pro Wert begrenzt.
  • Das Label com.docker.compose.project.config_files der Container zeigt auf die gelöschte temporäre Datei.
  • Getestet ist es mit Compose v5.0.0, v2.38.2 und v2.24.7 gegen Docker 27.5.1. Mit Podman ist es nicht getestet.

Code und Installationsanleitung liegen unter github.com/Jwrede/compose-inline.