Docker Release-Images bauen statt auf Servern kompilieren
AI generated
Docker · Release-Images · Multi-Stage · CI/CD
Docker Release-Images bauen
statt auf Servern kompilieren

Wer auf Produktionsservern kompiliert, verknüpft Build-Umgebung und Laufzeit-Umgebung auf eine Weise, die zu nicht reproduzierbaren Deployments führt. Docker Release-Images mit Multi-Stage-Builds trennen diese Phasen sauber, liefern schlanke Artefakte und machen jedes Deployment zu einem kontrollierbaren, rücksetzbaren Vorgang.

12 Min. Lesezeit Multi-Stage · Build-Container · CI-Pipeline · Registry Docker 24+ · GitHub Actions · GitLab CI

1. Das Problem mit Builds auf Produktionsservern

Viele Teams deployen, indem sie auf dem Produktionsserver git pull ausführen, danach den Paketmanager starten und anschließend den Compiler oder Transpiler aufrufen. Das fühlt sich einfach an, schafft aber eine fatale Kopplung: Der Produktionsserver muss alle Build-Tools – Compiler, Node.js, Maven, Composer mit Dev-Dependencies – installiert haben. Der Server mutiert bei jedem Deployment, und ein fehlgeschlagener Build hinterlässt den Server in einem Zwischenzustand, der schwer zu debuggen ist.

Das tiefere Problem ist die fehlende Reproduzierbarkeit. Wenn ein Docker Release-Image auf dem CI-Server gebaut wird und danach auf dem Produktionsserver ausgeführt wird, ist sichergestellt, dass exakt dasselbe Artefakt läuft, das getestet wurde. Auf dem Server kompilierte Software wurde hingegen unter anderen Bedingungen gebaut – andere Bibliotheksversionen, andere Umgebungsvariablen, anderer Zustand des Dateisystems. "Works on my machine" ist das symptomatische Resultat dieser Herangehensweise.

Ein weiterer Nachteil liegt in der Angriffsfläche: Ein Produktionsserver mit installiertem Compiler, Git-Client und Paketmanager ist deutlich schwieriger zu sichern als ein Server, der ausschließlich Container-Laufzeit ausführt. Docker Release-Images ermöglichen es, Produktionsserver auf eine minimale Rolle zu reduzieren: Container starten, Container stoppen, Logs lesen.

2. Das Konzept: Release-Image als unveränderliches Artefakt

Ein Docker Release-Image ist ein unveränderliches Artefakt – es enthält genau das, was zur Laufzeit benötigt wird, und nichts weiter. Es wird einmal gebaut, in einer Container-Registry gespeichert und kann danach beliebig oft auf beliebig vielen Servern gestartet werden. Jedes Deployment ist damit identisch mit dem vorherigen Test in der CI-Pipeline, weil dasselbe Image verwendet wird. Das ist der fundamentale Unterschied zu einer Build-auf-Server-Strategie, bei der jedes Deployment ein neuer, potenziell anderer Build ist.

Die Tag-Strategie für Release-Images ist entscheidend für die Nachvollziehbarkeit. Ein Image, das nur als latest getaggt wird, lässt sich nicht zu einem bestimmten Commit zurückverfolgen. Die empfohlene Strategie kombiniert den Git-Commit-SHA mit einem semantischen Versions-Tag: registry.example.com/app:1.4.2 und registry.example.com/app:sha-a1b2c3d zeigen auf dasselbe Image. Der SHA-Tag ermöglicht Rollbacks zu einem exakt bekannten Zustand, der Versions-Tag kommuniziert die semantische Bedeutung an andere Teams.

Für das Rollback-Szenario ist dieses Konzept besonders wertvoll: Statt einen Revert-Commit zu erstellen, zu pushen und einen neuen Build abzuwarten, startet man schlicht das vorherige Release-Image neu. Weil das Image in der Registry verfügbar bleibt, dauert das Rollback Sekunden statt Minuten. Die Produktionsumgebung ist zu jedem Zeitpunkt in einem definierten, testbaren Zustand.

3. Multi-Stage-Builds: Builder und Runtime trennen

Multi-Stage-Builds sind das zentrale Werkzeug für saubere Docker Release-Images. In einem Multi-Stage-Dockerfile gibt es mehrere FROM-Direktiven, wobei jede eine neue Stage eröffnet. Die Builder-Stage enthält alle Build-Tools und erstellt die kompilierten Artefakte. Die Runtime-Stage startet mit einem minimalen Base-Image und kopiert ausschließlich die fertigen Artefakte aus der Builder-Stage. Was nicht explizit kopiert wird, landet nicht im finalen Image.

Das Ergebnis ist ein Release-Image, das nur die Laufzeitabhängigkeiten enthält – keine Compiler, keine Dev-Dependencies, keine Test-Frameworks, keine temporären Build-Dateien. Für eine PHP-Anwendung bedeutet das: Die Builder-Stage hat Composer mit allen Dev-Paketen und führt composer install, npm run build und den Asset-Compile aus. Die Runtime-Stage basiert auf php:8.4-fpm-alpine und enthält nur die Vendor-Dateien und die kompilierten Assets. Imagegrößen von unter 150 MB für Anwendungen, die im Builder-Stadium mehrere Gigabyte benötigen, sind realistisch.


# Dockerfile — Multi-stage release image for PHP/Node application
# Stage 1: Node asset builder
FROM node:22-alpine AS node-builder
WORKDIR /build
COPY package*.json ./
# Install only production-relevant node deps first (cache layer)
RUN npm ci --ignore-scripts
COPY web/tailwind/ ./web/tailwind/
COPY web/src/ ./web/src/
RUN npm run build

# Stage 2: PHP dependency builder
FROM composer:2.8 AS php-builder
WORKDIR /app
COPY composer.json composer.lock ./
# Install without dev dependencies for production
RUN composer install \
    --no-dev \
    --no-interaction \
    --no-progress \
    --optimize-autoloader \
    --classmap-authoritative
COPY . .

# Stage 3: Minimal runtime image
FROM php:8.4-fpm-alpine AS runtime
RUN apk add --no-cache \
    nginx \
    supervisor \
    && docker-php-ext-install pdo_mysql opcache
WORKDIR /var/www/html
# Copy only built artifacts — no build tools in final image
COPY --from=php-builder /app .
COPY --from=node-builder /build/pub/static ./pub/static
COPY docker/php.ini /usr/local/etc/php/conf.d/app.ini
COPY docker/supervisord.conf /etc/supervisord.conf
USER www-data
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]

4. Layer-Cache gezielt nutzen ohne Invalidierungsfallen

Der Docker Layer-Cache ist der wichtigste Hebel für kurze Build-Zeiten in der CI-Pipeline. Jeder Layer im Release-Image wird gecacht und wiederverwendet, solange sich seine Eingaben nicht geändert haben. Die häufigste Invalidierungsfalle: COPY . . früh im Dockerfile. Sobald sich eine beliebige Datei im Projektverzeichnis ändert – auch eine README – wird dieser Layer und alle nachfolgenden Layer als ungültig markiert und neu gebaut. Für composer install oder npm ci, die Minuten dauern können, ist das ein erheblicher Performanceverlust.

Das korrekte Muster: Zuerst nur die Dependency-Manifeste kopieren (package.json, composer.lock), den Paketmanager ausführen, und erst danach den restlichen Quellcode kopieren. Wenn sich nur Anwendungscode ändert, bleibt der Dependency-Layer im Cache und wird nicht neu ausgeführt. Mit docker buildx build --cache-from type=registry,ref=registry.example.com/app:buildcache --cache-to type=registry,ref=registry.example.com/app:buildcache,mode=max kann der Cache in CI-Umgebungen ohne lokale Layer-History genutzt werden.

BuildKit, seit Docker 23 standardmäßig aktiv, bietet zusätzlich Mount-basierte Caches: RUN --mount=type=cache,target=/root/.npm npm ci nutzt einen persistenten Cache-Mount, der zwischen Builds erhalten bleibt. Das ist besonders effektiv für Paketmanager-Caches, die unabhängig vom Layer-Cache funktionieren und auch dann funktionieren, wenn sich package.json geändert hat und der Layer-Cache invalidiert wurde.

5. CI-Pipeline: Build, Tag und Registry-Push

Eine CI-Pipeline für Docker Release-Images folgt einem festen Ablauf: Image bauen, testen, taggen und in die Registry pushen. Der Build-Schritt sollte den vollständigen Commit-SHA als Tag verwenden, damit das Artefakt zu einem exakten Quelltextzustand rückverfolgbar ist. Ergänzend wird für Tagged Commits ein semantischer Versions-Tag gesetzt. Der latest-Tag wird nur auf dem Hauptbranch gesetzt und zeigt immer auf den aktuellen, produktionsbereiten Stand.

Nach dem Push kann die Deployment-Pipeline das neue Release-Image automatisch auf einer Staging-Umgebung deployen und Integrationstests ausführen. Nur wenn diese Tests erfolgreich sind, wird das Image für die Produktion freigegeben. Dieses Muster – Build einmal, deploy mehrfach – stellt sicher, dass das Produktions-Deployment identisch mit dem getesteten Staging-Deployment ist. Kein erneuter Build, kein "könnte sich geändert haben".


# .github/workflows/release.yml — Docker release image CI pipeline
name: Build and Push Release Image

on:
  push:
    branches: [main]
    tags: ['v*.*.*']

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${ { github.repository } }

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${ { env.REGISTRY } }
          username: ${ { github.actor } }
          password: ${ { secrets.GITHUB_TOKEN } }

      - name: Extract metadata for Docker
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${ { env.REGISTRY } }/${ { env.IMAGE_NAME } }
          tags: |
            type=sha,prefix=sha-
            type=semver,pattern={ {version} }
            type=raw,value=latest,enable={ {is_default_branch} }

      - name: Build and push release image
        uses: docker/build-push-action@v5
        with:
          context: .
          target: runtime
          push: true
          tags: ${ { steps.meta.outputs.tags } }
          cache-from: type=registry,ref=${ { env.REGISTRY } }/${ { env.IMAGE_NAME } }:buildcache
          cache-to: type=registry,ref=${ { env.REGISTRY } }/${ { env.IMAGE_NAME } }:buildcache,mode=max
          build-args: |
            BUILD_DATE=${ { github.event.head_commit.timestamp } }
            GIT_SHA=${ { github.sha } }

6. PHP- und Node-Projekte: konkrete Dockerfile-Muster

PHP-Projekte haben eine Besonderheit beim Bau von Release-Images: Composer lädt Dev-Dependencies herunter, die im Produktions-Image nicht benötigt werden. Mit composer install --no-dev --optimize-autoloader in der Builder-Stage und dem anschließenden Kopieren des vendor/-Verzeichnisses in die Runtime-Stage erhält man ein sauberes Produktions-Image ohne Test-Frameworks, Entwicklungstools und Debugging-Bibliotheken. Der Autoloader-Optimierung mit --classmap-authoritative eliminiert zusätzlich die Notwendigkeit, beim Autoloading das Dateisystem zu befragen.

Node.js-Projekte haben vergleichbare Muster: In der Builder-Stage werden alle Dependencies installiert und der Build ausgeführt. In der Runtime-Stage werden nur node_modules aus einem separaten npm ci --omit=dev-Schritt und die kompilierten Dateien aus dist/ kopiert. Für Next.js und ähnliche Frameworks gibt es den standalone-Output-Mode, der alle benötigten Dateien in einem minimalen Verzeichnis zusammenfasst und das vollständige Kopieren von node_modules überflüssig macht.

Magento 2 als komplexes PHP-Framework hat besondere Anforderungen an Release-Images: setup:di:compile und setup:static-content:deploy müssen im Build-Prozess ausgeführt werden, nicht auf dem Produktionsserver. Das bedeutet, dass die Builder-Stage Zugriff auf Datenbankverbindung oder eine Stub-Konfiguration für den DI-Compiler benötigt. Mit ARG-Variablen und einem dedizierten Build-Netzwerk lässt sich das lösen, ohne echte Produktions-Credentials in das Image zu baken.

7. Reproduzierbarkeit sicherstellen: Pinning und Build-Args

Ein Docker Release-Image ist nur so reproduzierbar wie seine Eingaben. Das Base-Image-Tag php:8.4-fpm-alpine zeigt auf eine veränderliche Referenz – bei jedem Build könnte eine neue Patch-Version verwendet werden. Für vollständige Reproduzierbarkeit sollte das Base-Image auf seinen SHA256-Digest gepinnt werden: FROM php:8.4-fpm-alpine@sha256:abc123.... Renovate Bot oder Dependabot können diese Pins automatisch aktualisieren, wenn neue Versionen verfügbar werden, ohne die Reproduzierbarkeit des aktuellen Builds zu gefährden.

Build-Args mit ARG ermöglichen es, veränderliche Werte wie den Commit-SHA oder das Build-Datum in das Image einzubetten, ohne den Layer-Cache zu invalidieren. Diese Werte werden üblicherweise als Docker-Labels mit LABEL gespeichert, sodass docker inspect auf einem laufenden Container sofort Aufschluss über den genauen Quelltext-Stand gibt. org.opencontainers.image.revision, org.opencontainers.image.created und org.opencontainers.image.source sind standardisierte OCI-Labels für diese Metadaten.


# Reproducibility: pinned base image + OCI labels + build-args
# Pin to digest for full reproducibility (update via Renovate/Dependabot)
FROM php:8.4-fpm-alpine@sha256:1a2b3c4d5e6f AS runtime

# Build-time metadata injected by CI — does NOT invalidate dependency cache
ARG BUILD_DATE
ARG GIT_SHA
ARG GIT_REF

# OCI standard labels for traceability
LABEL org.opencontainers.image.created="${BUILD_DATE}" \
      org.opencontainers.image.revision="${GIT_SHA}" \
      org.opencontainers.image.source="https://github.com/mironsoft/app" \
      org.opencontainers.image.ref.name="${GIT_REF}"

# Inspect running container: docker inspect <id> | jq '.[0].Config.Labels'
# Shows exact git SHA and build timestamp for any running container

# Verify reproducibility locally
# docker buildx imagetools inspect registry.example.com/app:sha-abc123

8. Release-Image vs. Build-auf-Server im Vergleich

Die Entscheidung zwischen Docker Release-Images und Build-auf-Server-Strategie hat weitreichende Konsequenzen für Betrieb, Sicherheit und Deployment-Geschwindigkeit. Die folgende Tabelle stellt die relevantesten Dimensionen gegenüber.

Dimension Build auf Produktionsserver Docker Release-Image Vorteil
Reproduzierbarkeit Abhängig von Serverzustand Identisches Artefakt Gleicher Build überall
Rollback Revert-Commit + neuer Build Altes Image neu starten Sekunden statt Minuten
Angriffsfläche Build-Tools auf Produktion Nur Runtime auf Produktion Minimale Angriffsfläche
Fehlgeschlagener Build Server in Zwischenzustand Altes Image läuft weiter Keine Produktionsunterbrechung
Deployment-Dauer Compile-Zeit blockiert Nur Image-Pull + Start Schnellere Deployments

Ein Argument gegen Release-Images ist der initialen Aufwand für die CI-Pipeline und das Dockerfile. Dieser Aufwand amortisiert sich jedoch spätestens beim ersten fehlgeschlagenen Produktions-Build, der einen Server in einem inkonsistenten Zustand hinterlässt – und das wird passieren. Die Investition in eine saubere Image-Build-Pipeline ist eine Investition in beherrschbare Deployments.

9. Security: Secrets im Build-Prozess sicher handhaben

Eine der häufigsten Sicherheitsfallen bei Docker Release-Images ist das Einbetten von Secrets in das Image. Jede ENV- oder ARG-Direktive mit einem Secret-Wert landet im Image-Layer und ist mit docker history oder durch Inspektion der Layer sichtbar – auch wenn der Wert in einem späteren Layer überschrieben wird. Das gilt insbesondere für Composer-Tokens für private Repositories und npm-Auth-Tokens für private Pakete.

BuildKit bietet die Lösung mit Secret-Mounts: RUN --mount=type=secret,id=composer_auth cat /run/secrets/composer_auth > ~/.composer/auth.json && composer install && rm ~/.composer/auth.json. Das Secret ist während des Build-Befehls im Container verfügbar, wird aber nicht in den Layer geschrieben. Der Aufruf erfolgt mit docker buildx build --secret id=composer_auth,src=./auth.json .. Das Secret verlässt die Build-Maschine nicht und landet nicht im Release-Image.

SSH-Agent-Forwarding für private Git-Repositories folgt demselben Prinzip: RUN --mount=type=ssh git clone git@github.com:private/repo.git nutzt den SSH-Agenten des Hosts, ohne den privaten Schlüssel in den Layer zu schreiben. In CI-Umgebungen wird der SSH-Agent mit ssh-agent und dem entsprechenden Action-Secret eingerichtet. So können Release-Images auf private Abhängigkeiten zugreifen, ohne Credentials einzubetten.

Mironsoft

Docker Release-Images, CI/CD-Pipelines und Deployment-Infrastruktur

Release-Images, die in der Produktion zuverlässig laufen?

Wir bauen Multi-Stage-Dockerfiles, CI-Pipelines mit Registry-Integration und Deployment-Strategien für reproduzierbare, sichere Release-Images – von der ersten Stage bis zum automatischen Rollback.

Dockerfile-Audit

Multi-Stage-Analyse, Cache-Optimierung und Security-Review bestehender Images

CI-Pipeline-Aufbau

Build, Test, Tag und Push in GitHub Actions oder GitLab CI integrieren

Registry-Setup

Private Registry, Retention-Policies und automatisches Rollback einrichten

10. Zusammenfassung

Docker Release-Images lösen das fundamentale Problem nicht reproduzierbarer Deployments, indem sie Build-Umgebung und Laufzeitumgebung sauber trennen. Multi-Stage-Builds in Dockerfiles erlauben es, einen vollständig ausgestatteten Builder zu verwenden und ein minimales Runtime-Image zu produzieren – ohne Build-Tools, Dev-Dependencies oder temporäre Dateien. Der Layer-Cache, richtig strukturiert, macht CI-Builds schnell und inkrementell. BuildKit-Secret-Mounts verhindern, dass Credentials in Images eingebettet werden.

Die Tag-Strategie mit SHA und semantischen Versionen macht jedes Release-Image rückverfolgbar zu einem exakten Quelltext-Commit. Rollbacks dauern Sekunden, weil das vorherige Image in der Registry verfügbar ist. Fehlgeschlagene Builds lassen die Produktion unberührt, weil das alte Image weiterläuft. Der initiale Aufwand für saubere Multi-Stage-Dockerfiles und CI-Pipeline-Integration zahlt sich bei jedem Deployment zurück – und erst recht beim ersten fehlgeschlagenen Produktions-Build, der andernfalls Downtime verursacht hätte.

Docker Release-Images — Das Wichtigste auf einen Blick

Multi-Stage-Builds

Builder-Stage mit allen Tools, Runtime-Stage mit nur den Artefakten. Imagegrößen drastisch reduzieren ohne Funktionsverlust.

Layer-Cache

Dependency-Manifeste zuerst kopieren, dann Paketmanager ausführen, zuletzt Anwendungscode. Cache-Invalidierung auf das Nötigste beschränken.

Tagging-Strategie

SHA-Tag für Rückverfolgbarkeit, Versions-Tag für Semantik, latest nur auf Hauptbranch. Rollback = altes Image neu starten.

Secret-Handling

BuildKit --mount=type=secret für Composer-Tokens und npm-Tokens. Secrets landen nie im Image-Layer, auch nicht in Zwischenlayern.

11. FAQ: Docker Release-Images

1Release-Image vs. Development-Image?
Release-Image: nur Laufzeitabhängigkeiten, unveränderlich, für Produktion. Development-Image: alle Tools, für lokales Arbeiten. Nie vertauschen.
2Base-Image-Updates kontrollieren?
SHA256-Digest pinnen. Renovate Bot oder Dependabot reichen Digest-Updates als PRs ein – kontrolliert und testbar.
3Warum keine Build-Tools in Produktion?
Compiler auf Produktion vergrößert Angriffsfläche massiv. Angreifer mit Containerzugang kann eigenen Code kompilieren und ausführen.
4Optimale Image-Größe?
Unter 200 MB ist für PHP/Node erreichbar. Alpine-Base, --no-dev Composer, keine Tmpfiles im Layer. Distroless-Images für maximale Reduktion.
5Multi-Stage-Builds lokal testen?
docker build --target runtime . baut nur bis zur Runtime-Stage. Jede Stage einzeln testbar.
6Was passiert bei fehlgeschlagenem CI-Build?
Kein Deployment. Produktion läuft unverändert weiter. Kein inkonsistenter Serverzustand. Build korrigieren und neu starten.
7Rollback mit Release-Images?
Alten Image-Tag setzen, Container neu starten. Dauert Sekunden. Retention-Policy sicherstellt, dass alte Releases in Registry verfügbar bleiben.
8Composer-Tokens sicher im Build?
BuildKit --mount=type=secret. Token nur während RUN verfügbar, kein Eintrag im Layer. docker buildx build --secret id=x,src=file .
9latest-Tag in Produktion verwenden?
Niemals. latest ist veränderlich – zwischen Pull und Start könnte sich das Image geändert haben. Immer konkreten SHA oder Versions-Tag deployen.
10Release-Images mit Kubernetes?
Image-Tag im Manifest auf neuen SHA setzen, committen. ArgoCD oder Flux deployen automatisch. GitOps-Workflow für nachvollziehbare Deployments.