ein Docker Image durch alle Stufen schleusen
Wer für jede Umgebung ein eigenes Docker Image baut, verliert genau die Garantie, die Container eigentlich versprechen. Build once, deploy many trennt den Build-Schritt von der Konfiguration und sorgt dafür, dass in Produktion exakt das Artefakt läuft, das zuvor in Staging getestet wurde, ohne einen einzigen Rebuild dazwischen.
Inhaltsverzeichnis
- 1. Warum build once, deploy many mehr ist als eine Parole
- 2. Das Anti-Pattern: ein Image pro Umgebung bauen
- 3. Ein Dockerfile ohne eingebrannte Umgebungslogik
- 4. Konfiguration zur Laufzeit statt zur Build-Zeit
- 5. Entrypoint-Skripte als Bindeglied zur Umgebung
- 6. Die Pipeline: einmal bauen, mehrfach deployen
- 7. Geheimnisse und umgebungsspezifische Werte sauber trennen
- 8. Typische Fehler beim Umstieg auf build once, deploy many
- 9. Build-Strategien im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum build once, deploy many mehr ist als eine Parole
Das Prinzip build once, deploy many klingt zunächst wie eine Selbstverständlichkeit, wird in der Praxis aber ständig verletzt. Die Idee dahinter: Ein Docker Image wird genau einmal aus dem Quellcode gebaut, danach durchläuft dasselbe, unveränderte Artefakt alle Stufen der Pipeline, von der Entwicklungsumgebung über Staging bis zur Produktion. Es wird niemals neu kompiliert, nur weil sich die Zielumgebung ändert. Genau diese Eigenschaft ist der eigentliche Mehrwert von Containern gegenüber klassischen Deployment-Verfahren mit Konfigurationsmanagement-Tools, die auf jedem Server eigene Build-Schritte ausführen.
Der Grundgedanke von build once, deploy many lässt sich direkt auf die Twelve-Factor-App-Methodik zurückführen, insbesondere auf den Grundsatz, Build- und Run-Stufen strikt zu trennen. Ein Team, das dieses Muster konsequent umsetzt, kann mit hoher Sicherheit sagen, dass ein in Staging erfolgreich getesteter Container in Produktion identisch funktioniert, weil es sich buchstäblich um dieselben Bytes handelt. Das reduziert die Klasse von Fehlern, die sonst durch abweichende Build-Umgebungen, unterschiedliche Compiler-Versionen oder vergessene Abhängigkeiten entstehen, drastisch.
In der Praxis bedeutet build once, deploy many auch einen kulturellen Wandel im Team. Entwickler müssen akzeptieren, dass Umgebungsunterschiede nicht mehr im Image selbst kodiert werden dürfen, sondern ausschließlich über externe Konfiguration fließen. Das erzwingt von Anfang an eine sauberere Architektur, in der die Anwendung ihre Laufzeitparameter aus der Umgebung liest, statt sie zur Build-Zeit fest einzubrennen. Die folgenden Abschnitte zeigen konkret, wie dieses Pattern mit Docker, Dockerfiles und CI-Pipelines umgesetzt wird.
2. Das Anti-Pattern: ein Image pro Umgebung bauen
Das Gegenteil von build once, deploy many ist ein Ansatz, den man in vielen gewachsenen Projekten findet: Für jede Umgebung gibt es einen eigenen Build-Vorgang, oft mit eigenem Dockerfile oder eigenen Build-Argumenten, die Umgebungsvariablen zur Build-Zeit fest ins Image schreiben. Das Ergebnis sind drei oder vier unterschiedliche Images, eines für Dev, eines für Staging, eines für Produktion, die zwar aus demselben Quellcode entstehen, aber technisch verschiedene Artefakte sind. Ein Bug, der erst in Produktion auftaucht, lässt sich dann nicht mehr zweifelsfrei auf das getestete Staging-Image zurückführen.
Ein typisches Symptom dieses Anti-Patterns sind Build-Argumente wie --build-arg APP_ENV=production, die genutzt werden, um innerhalb des Dockerfiles unterschiedliche Konfigurationsdateien zu kopieren oder unterschiedliche Compiler-Flags zu setzen. Das mag pragmatisch wirken, verletzt aber den Kerngedanken von build once, deploy many: Sobald ein Build-Argument das Ergebnis-Image inhaltlich verändert, ist es kein einheitliches Artefakt mehr, sondern eine von mehreren Varianten, die nie gemeinsam getestet wurden. Genau solche Varianten sind die Quelle vieler Produktionsvorfälle, die im Nachhinein mit "hat in Staging aber funktioniert" beschrieben werden.
# ANTI-PATTERN: environment baked in at build time
docker build --build-arg APP_ENV=staging -t myapp:staging .
docker build --build-arg APP_ENV=production -t myapp:production .
# Two different images from the same source -- never tested together
# PATTERN: build once, tag with a stable, traceable version
docker build -t myapp:1.4.2 .
docker tag myapp:1.4.2 registry.example.com/myapp:1.4.2
docker push registry.example.com/myapp:1.4.2
# The same image ID runs everywhere -- only the environment differs
docker run --env-file staging.env registry.example.com/myapp:1.4.2
docker run --env-file production.env registry.example.com/myapp:1.4.2
3. Ein Dockerfile ohne eingebrannte Umgebungslogik
Damit build once, deploy many überhaupt funktionieren kann, muss das Dockerfile so geschrieben sein, dass keine Umgebungsentscheidung zur Build-Zeit getroffen wird. Konkret bedeutet das: Keine ARG-Direktiven, die Konfigurationswerte wie API-Endpunkte, Datenbank-Hosts oder Feature-Flags ins Image kopieren. Stattdessen enthält das Image ausschließlich Code, Abhängigkeiten und ausführbare Artefakte, die in jeder Umgebung identisch bleiben. Das Image weiß nicht, ob es gleich in Dev oder Produktion startet, und genau das ist die gewünschte Eigenschaft.
Für PHP- und Node-Projekte bedeutet das build once, deploy many Pattern in der Praxis: Composer- und npm-Abhängigkeiten werden im Build-Schritt final installiert, kompilierte Assets landen fertig gebaut im Image, aber Konfigurationsdateien wie .env werden explizit nicht kopiert. Multi-Stage-Builds helfen dabei, den Build-Container von der schlanken Laufzeit-Stage zu trennen, sodass nur das fertige, umgebungsunabhängige Artefakt im finalen Image landet.
# Multi-stage Dockerfile with zero environment-specific logic
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist
FROM php:8.4-fpm AS runtime
WORKDIR /var/www/html
COPY --from=vendor /app/vendor ./vendor
COPY . .
# No .env, no environment-specific config copied here
RUN chown -R www-data:www-data /var/www/html
USER www-data
EXPOSE 9000
CMD ["php-fpm"]
4. Konfiguration zur Laufzeit statt zur Build-Zeit
Wenn das Image selbst keine Umgebungsinformationen enthält, muss die Konfiguration zwingend zur Laufzeit injiziert werden. Das ist der Kern von build once, deploy many: Environment-Variablen, gemountete Config-Dateien oder Secrets-Provider liefern zur Startzeit des Containers alles, was die Anwendung braucht, um sich korrekt zu verhalten. Der Container selbst bleibt dabei komplett unverändert, egal ob er in Dev, Staging oder Produktion gestartet wird.
Praktisch heißt das für ein Magento- oder ein anderes PHP-Projekt: Datenbank-Zugangsdaten, Cache-Backend-Adressen, Feature-Flags und externe API-Keys werden über docker run --env-file, über Kubernetes ConfigMaps und Secrets oder über einen Vault-Client zur Laufzeit gesetzt. Wichtig bei build once, deploy many ist, dass diese Werte niemals in das Image selbst geschrieben werden, sondern ausschließlich als externe Eingabe existieren, die sich pro Umgebung unterscheidet, ohne das Image anzufassen.
5. Entrypoint-Skripte als Bindeglied zur Umgebung
Ein sauberes Entrypoint-Skript ist das technische Bindeglied, das build once, deploy many im Container-Alltag erst praktikabel macht. Statt Konfiguration im Image zu verankern, liest das Entrypoint-Skript beim Containerstart Environment-Variablen, validiert Pflichtwerte und generiert daraus bei Bedarf Konfigurationsdateien, etwa eine Nginx-Konfiguration oder eine PHP-Ini mit umgebungsspezifischen Limits. Das Image bleibt dabei identisch, nur das Verhalten beim Start passt sich der jeweiligen Umgebung an.
Ein häufiges Muster bei build once, deploy many ist die Fail-Fast-Validierung im Entrypoint: Fehlt eine Pflichtvariable wie DATABASE_URL, bricht der Container sofort mit einer klaren Fehlermeldung ab, statt in einem halb-funktionsfähigen Zustand weiterzulaufen. Das verhindert, dass ein fehlkonfigurierter Container in Produktion unbemerkt Traffic annimmt und erst später mit kryptischen Fehlern auffällt.
#!/usr/bin/env bash
# docker-entrypoint.sh -- reads runtime config, same image everywhere
set -euo pipefail
: "${DATABASE_URL:?DATABASE_URL is required}"
: "${APP_ENV:?APP_ENV is required (dev, staging, production)}"
CACHE_BACKEND="${CACHE_BACKEND:-redis}"
echo "[entrypoint] Starting in environment: ${APP_ENV}"
echo "[entrypoint] Cache backend: ${CACHE_BACKEND}"
# Render environment-specific config from a template at runtime,
# never at build time -- the image itself stays unchanged
envsubst < /etc/nginx/templates/default.conf.template \
> /etc/nginx/conf.d/default.conf
exec "$@"
6. Die Pipeline: einmal bauen, mehrfach deployen
Die CI/CD-Pipeline muss build once, deploy many strukturell abbilden: Ein einziger Build-Job erzeugt das Image und schreibt es mit einer eindeutigen, unveränderlichen Tag-Referenz, meist dem Git-Commit-Hash oder einer Semver-Version, in die Registry. Alle folgenden Deploy-Jobs für Staging und Produktion referenzieren exakt diesen einen Tag, ohne ihn neu zu bauen. Erst wenn ein Deploy erfolgreich in Staging validiert wurde, wird derselbe Tag für Produktion freigegeben.
In GitLab CI oder GitHub Actions lässt sich build once, deploy many mit einem einzigen Build-Stage und mehreren nachgelagerten Deploy-Stages abbilden, die über Pipeline-Variablen denselben Image-Tag referenzieren. Wichtig ist, dass kein Deploy-Job jemals einen eigenen docker build-Aufruf enthält, sonst schleicht sich das Anti-Pattern durch die Hintertür wieder ein.
# .gitlab-ci.yml -- build once, reference the same tag in every stage
stages: [build, deploy-staging, deploy-production]
build:
stage: build
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
deploy-staging:
stage: deploy-staging
script:
# Same tag, no rebuild -- only runtime config differs
- kubectl set image deployment/app app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -n staging
deploy-production:
stage: deploy-production
when: manual
script:
- kubectl set image deployment/app app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -n production
7. Geheimnisse und umgebungsspezifische Werte sauber trennen
Ein Punkt, der bei build once, deploy many oft unterschätzt wird, ist die Trennung zwischen unkritischer Konfiguration und echten Geheimnissen. Beides fließt zwar auf demselben Weg, per Environment-Variable oder gemounteter Datei, in den Container, sollte aber unterschiedlich verwaltet werden. API-Keys, Datenbankpasswörter und Zertifikate gehören in einen Secrets-Manager wie Vault, AWS Secrets Manager oder Kubernetes Secrets, niemals in eine Klartext-Datei im Git-Repository.
Für build once, deploy many ergibt sich daraus eine klare Regel: Das Image selbst darf niemals Geheimnisse enthalten, auch nicht temporär während des Builds. Multi-Stage-Builds mit BuildKit-Secrets (RUN --mount=type=secret) erlauben es, während des Builds auf ein Geheimnis zuzugreifen, etwa für einen privaten Composer-Registry-Zugriff, ohne dass dieses Geheimnis in einem Layer des finalen Images landet.
8. Typische Fehler beim Umstieg auf build once, deploy many
Der häufigste Fehler beim Einführen von build once, deploy many ist, Konfigurationswerte nur teilweise aus dem Image zu entfernen. Ein Team entfernt zum Beispiel die Datenbank-Zugangsdaten aus dem Dockerfile, lässt aber einen fest kodierten API-Endpunkt für einen externen Zahlungsdienst im Code stehen, weil dieser "sich ja sowieso nie ändert". Genau solche Annahmen brechen später, wenn ein Staging-Environment plötzlich gegen einen Sandbox-Endpunkt testen soll, das Image dafür aber neu gebaut werden müsste.
# WRONG: partial migration -- config still baked in
FROM php:8.4-fpm
ENV PAYMENT_API_URL=https://api.payment-provider.com/v1 # hardcoded!
COPY . /var/www/html
# RIGHT: no environment value in the image at all
FROM php:8.4-fpm
COPY . /var/www/html
# PAYMENT_API_URL is supplied at `docker run` / deploy time only
Ein zweiter verbreiteter Fehler ist, den Image-Tag beim Promoten von Staging nach Produktion versehentlich zu ändern, etwa weil ein Deploy-Skript aus Gewohnheit :latest statt der konkreten Version referenziert. Damit ist nicht mehr garantiert, dass in Produktion dasselbe Artefakt läuft, das in Staging getestet wurde. Feste, unveränderliche Tags pro Build sind bei build once, deploy many keine Empfehlung, sondern eine harte Voraussetzung.
9. Build-Strategien im direkten Vergleich
Die folgende Tabelle stellt den klassischen Ansatz mit umgebungsspezifischen Builds dem konsequenten build once, deploy many Pattern gegenüber, jeweils mit den praktischen Konsequenzen für Testbarkeit und Nachvollziehbarkeit.
| Aspekt | Ein Image pro Umgebung | Build once, deploy many | Konsequenz |
|---|---|---|---|
| Anzahl Builds pro Release | 3 bis 4 separate Builds | Genau 1 Build | Weniger CI-Zeit, ein Artefakt zu prüfen |
| Staging = Produktion? | Nicht garantiert | Bitgleich garantiert | Weniger "geht in Staging, bricht in Prod" |
| Konfigurationsquelle | Build-Argumente im Dockerfile | Environment-Variablen zur Laufzeit | Konfiguration ohne Rebuild änderbar |
| Rollback | Erneuter Build nötig | Alten Tag erneut deployen | Rollback in Sekunden statt Minuten |
| Nachvollziehbarkeit | Welches Image lief wo? | Ein Tag, ein Commit, ein Artefakt | Auditierbar bis zum genauen Commit |
In der Praxis zeigt sich der Vorteil von build once, deploy many besonders bei Incident-Analysen: Wenn Staging und Produktion garantiert dasselbe Artefakt ausführen, entfällt die zeitraubende erste Frage nach einem Ausfall, ob überhaupt dieselbe Version lief. Diese Zeitersparnis in Stresssituationen rechtfertigt den anfänglichen Umstellungsaufwand meist innerhalb weniger Wochen.
Mironsoft
Docker-Pipelines, Deployment-Architektur und Container-Workflows
Ein Image, alle Umgebungen: sauber umgesetzt?
Wir analysieren bestehende Docker-Pipelines, entfernen umgebungsspezifische Build-Logik und richten build once, deploy many so ein, dass Staging und Produktion garantiert dasselbe Artefakt ausführen.
Pipeline-Audit
Vorhandene Builds auf umgebungsspezifische Logik und Build-Argumente prüfen
Dockerfile-Refactoring
Multi-Stage-Builds ohne eingebrannte Umgebungswerte aufsetzen
Secrets-Management
Runtime-Konfiguration und Geheimnisse sauber getrennt einrichten
10. Zusammenfassung
Build once, deploy many ist kein Nice-to-have, sondern die Voraussetzung dafür, dass Docker seine zentrale Garantie einlösen kann: Was getestet wurde, läuft auch in Produktion, unverändert bis auf das letzte Byte. Das Dockerfile enthält keine Umgebungsentscheidungen, die Konfiguration fließt ausschließlich zur Laufzeit über Environment-Variablen, gemountete Dateien oder Secrets-Manager ein. Ein Entrypoint-Skript übernimmt die Validierung und Anpassung beim Containerstart, ohne das Image selbst zu verändern.
Die CI-Pipeline baut das Image genau einmal, versieht es mit einem unveränderlichen Tag, und alle nachfolgenden Deploy-Schritte referenzieren exakt diesen Tag, ohne neu zu bauen. Wer dieses Pattern konsequent durchzieht, gewinnt schnellere Rollbacks, einfachere Incident-Analysen und die Sicherheit, dass ein in Staging validiertes Artefakt sich in Produktion identisch verhält.
Build once, deploy many — Das Wichtigste auf einen Blick
Ein Image, ein Tag
Genau ein Build pro Release, referenziert über Commit-Hash oder Semver, niemals über :latest.
Konfiguration zur Laufzeit
Environment-Variablen, ConfigMaps und Secrets liefern alle umgebungsspezifischen Werte erst beim Start.
Entrypoint validiert
Fail-Fast bei fehlenden Pflichtvariablen, statt halb-funktionsfähig weiterzulaufen.
Rollback in Sekunden
Ein alter, unveränderter Tag lässt sich sofort erneut deployen, ohne neuen Build.