wirklich verstehen
ARG und ENV wirken auf den ersten Blick austauschbar, beide setzen schließlich eine Variable. Wer den Unterschied bei Sichtbarkeit, Laufzeitverhalten und Multi-Stage-Builds nicht kennt, baut leicht Images, die zur Laufzeit unerwartete Werte liefern oder, schlimmer, versehentlich Zugangsdaten im Image-Layer verewigen.
Inhaltsverzeichnis
- 1. Zwei Variablen-Arten mit unterschiedlichem Lebenszyklus
- 2. Warum ein Container den Wert von ARG nicht kennt
- 3. ARG-Werte beim Build setzen und überschreiben
- 4. Sichtbarkeit über Multi-Stage-Build-Grenzen hinweg
- 5. Der häufige Fehler: Secrets über ARG einschleusen
- 6. Die richtige Lösung: BuildKit Secret Mounts
- 7. Typische Anwendungsfälle für ARG und ENV im Vergleich
- 8. ARG in docker-compose.yml: der build.args-Abschnitt
- 9. Weitere Details zum Scoping-Verhalten
- 10. Zusammenfassung
- 11. FAQ
1. Zwei Variablen-Arten mit unterschiedlichem Lebenszyklus
ARG und ENV definieren beide benannte Werte, die innerhalb eines Dockerfiles referenziert werden können, doch ihr Lebenszyklus unterscheidet sich fundamental. Eine mit ARG deklarierte Variable existiert ausschließlich während des Build-Vorgangs. Sie steht innerhalb der RUN-, COPY- und anderer Build-Zeit-Anweisungen zur Verfügung, verschwindet aber vollständig, sobald das fertige Image erstellt ist. Im laufenden Container, der aus diesem Image gestartet wird, ist von einer reinen ARG-Variable nichts mehr zu sehen.
Eine mit ENV deklarierte Variable hingegen wird fest in die Image-Metadaten geschrieben und ist sowohl während des restlichen Build-Vorgangs als auch in jedem später aus dem Image gestarteten Container als echte Umgebungsvariable verfügbar. Wer also einen Wert benötigt, den die Anwendung zur Laufzeit per getenv oder process.env auslesen soll, muss zwingend ENV verwenden, ARG allein reicht dafür nicht aus.
FROM node:20-alpine
# ARG: nur waehrend des Builds sichtbar
ARG BUILD_VERSION=dev
RUN echo "Baue Version ${BUILD_VERSION}"
# ENV: auch zur Laufzeit im Container sichtbar
ENV APP_ENV=production
CMD ["node", "server.js"]
2. Warum ein Container den Wert von ARG nicht kennt
Ein häufiges Missverständnis ist die Annahme, ein per --build-arg gesetzter Wert stünde auch im laufenden Container zur Verfügung, weil er ja beim Bauen des Images sichtbar war. Das stimmt nicht: ARG-Werte fließen zwar in die während RUN-Anweisungen ausgeführten Befehle ein und können dort in Dateien geschrieben, kompiliert oder anderweitig verarbeitet werden, aber sie werden nicht automatisch Teil der finalen Image-Konfiguration. Nach Abschluss des Builds existiert die Variable schlicht nicht mehr.
Wer einen Build-Zeit-Wert auch zur Laufzeit benötigt, muss ihn explizit von ARG nach ENV übertragen, indem ENV auf den Wert der ARG-Variable verweist. Diese Kombination ist ein etabliertes Muster: ARG nimmt den Wert von außen entgegen, etwa über --build-arg, und ENV macht ihn dauerhaft zu einem Teil des Images, inklusive Sichtbarkeit im späteren Container.
FROM python:3.12-slim
# Wert von aussen entgegennehmen
ARG APP_VERSION=1.0.0
# Explizit in eine Laufzeit-Umgebungsvariable uebertragen
ENV APP_VERSION=${APP_VERSION}
# Jetzt zur Laufzeit im Container sichtbar:
# docker run mein-image env | grep APP_VERSION
CMD ["python", "app.py"]
3. ARG-Werte beim Build setzen und überschreiben
Werte für ARG-Variablen lassen sich beim Aufruf von docker build über die Option --build-arg setzen, zum Beispiel docker build --build-arg BUILD_VERSION=2.4.1. Ist im Dockerfile ein Default-Wert per ARG BUILD_VERSION=dev definiert, wird dieser Default nur verwendet, wenn kein passender --build-arg übergeben wird. Fehlt sowohl der Default als auch der --build-arg, ist die Variable innerhalb des Builds leer, was in nachfolgenden RUN-Anweisungen zu unerwarteten Ergebnissen führen kann.
Zusätzlich unterstützt Docker global vordefinierte ARG-Variablen wie HTTP_PROXY, HTTPS_PROXY oder NO_PROXY, die automatisch für Build-Umgebungen hinter einem Proxy verfügbar sind, ohne dass sie im Dockerfile deklariert werden müssen. Eigene Variablennamen müssen dagegen immer explizit per ARG-Anweisung im Dockerfile deklariert werden, bevor sie referenziert werden können, sonst bleibt die Ersetzung im Build-Log stumm leer statt einen Fehler zu werfen.
# Build mit explizitem Wert fuer ARG BUILD_VERSION
docker build --build-arg BUILD_VERSION=2.4.1 -t mironsoft/app:2.4.1 .
# Mehrere ARG-Werte gleichzeitig setzen
docker build \
--build-arg BUILD_VERSION=2.4.1 \
--build-arg NODE_ENV=production \
-t mironsoft/app:2.4.1 .
4. Sichtbarkeit über Multi-Stage-Build-Grenzen hinweg
Bei Multi-Stage-Builds mit mehreren FROM-Anweisungen gilt eine wichtige Regel, die häufig zu Verwirrung führt: Eine ARG-Deklaration gilt nur innerhalb der Build-Stage, in der sie steht, beziehungsweise ab der Stelle, an der sie deklariert wurde, bis zur nächsten FROM-Zeile. Wird eine globale ARG-Variable vor der ersten FROM-Zeile deklariert, steht sie zwar in jeder Stage grundsätzlich zur Verfügung, muss aber innerhalb jeder einzelnen Stage erneut per ARG referenziert werden, um dort nutzbar zu sein.
Dieses Verhalten überrascht viele Entwickler, die annehmen, eine einmal global gesetzte ARG-Variable sei automatisch in allen nachfolgenden Stages sichtbar. Ohne die erneute ARG-Zeile innerhalb der jeweiligen Stage bleibt die Variable dort leer, selbst wenn sie global vor dem ersten FROM deklariert wurde. ENV-Variablen hingegen gelten immer nur innerhalb der Stage, in der sie gesetzt wurden, und werden nicht automatisch in eine spätere Stage kopiert, es sei denn, man setzt sie dort explizit erneut.
# Globale ARG vor der ersten FROM-Zeile
ARG NODE_VERSION=20
# --- Stage 1: build ---
FROM node:${NODE_VERSION}-alpine AS build
# Globale ARG muss hier erneut referenziert werden, um nutzbar zu sein
ARG NODE_VERSION
RUN echo "Baue mit Node ${NODE_VERSION}"
WORKDIR /app
COPY . .
RUN npm ci && npm run build
# --- Stage 2: runtime ---
FROM node:${NODE_VERSION}-alpine AS runtime
# Ohne erneute ARG-Zeile hier waere NODE_VERSION in dieser Stage leer
ARG NODE_VERSION
ENV NODE_VERSION=${NODE_VERSION}
COPY --from=build /app/dist /app/dist
CMD ["node", "/app/dist/server.js"]
5. Der häufige Fehler: Secrets über ARG einschleusen
Ein weit verbreitetes Anti-Pattern ist der Versuch, sensible Werte wie API-Keys, Datenbank-Passwörter oder npm-Registry-Tokens über ARG in den Build-Prozess einzuschleusen, etwa mit ARG NPM_TOKEN gefolgt von einem docker build --build-arg NPM_TOKEN=geheim. Das Problem dabei ist nicht die Sichtbarkeit zur Laufzeit im fertigen Container, ARG-Werte landen dort ja gerade nicht automatisch, sondern die Sichtbarkeit in der Build-Historie: ARG-Werte werden standardmäßig in der Image-Metadaten-Historie gespeichert und lassen sich mit docker history --no-trunc jederzeit aus dem fertigen Image auslesen.
Selbst wenn der Wert nur in einer frühen Build-Stage verwendet und diese Stage in einem Multi-Stage-Build nicht in das finale Image übernommen wird, bleibt er in vielen Fällen dennoch im Build-Cache oder in der lokalen Historie der Zwischen-Images sichtbar, solange diese nicht explizit gelöscht werden. Zudem landet jeder per --build-arg übergebene Secret-Wert typischerweise auch in Shell-Historie, CI/CD-Logs und Build-Metadaten der Registry, was ihn effektiv dauerhaft kompromittiert, selbst wenn er im laufenden Container später nirgends mehr auftaucht.
# ANTI-PATTERN: Secret ueber ARG einschleusen, landet in der Image-Historie
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc \
&& npm ci
# docker history --no-trunc zeigt NPM_TOKEN im Klartext!
# RICHTIG: BuildKit Secret Mount, landet nicht im Image oder in der Historie
# syntax=docker/dockerfile:1
FROM node:20-alpine
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) && \
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc && \
npm ci
# Aufruf: docker build --secret id=npm_token,src=./npm_token.txt .
6. Die richtige Lösung: BuildKit Secret Mounts
BuildKit, der moderne Docker-Build-Backend, bietet mit --mount=type=secret einen dedizierten Mechanismus für genau dieses Problem. Ein per --secret übergebener Wert wird während der jeweiligen RUN-Anweisung als temporäre Datei unter /run/secrets/
Diese Herangehensweise löst das Problem sauber an der Wurzel, statt es nachträglich zu kaschieren: Secrets werden nie Teil des Images, weder in ARG-Form noch versehentlich in einer COPY-Anweisung. Für CI/CD-Pipelines lässt sich der Secret-Wert dabei typischerweise aus einer sicher verwalteten Umgebungsvariable oder einem Secret-Manager in eine temporäre Datei schreiben, die dem docker build-Aufruf übergeben und danach sofort wieder gelöscht wird.
7. Typische Anwendungsfälle für ARG und ENV im Vergleich
ARG eignet sich hervorragend für alles, was ausschließlich den Build-Prozess selbst steuert: die Basis-Image-Version über eine Variable in FROM, Compiler-Flags, die Auswahl einer Build-Variante wie debug oder release, oder temporäre Werte, die nur während RUN-Befehlen gebraucht werden und danach keine Rolle mehr spielen. Diese Werte müssen im fertigen Image nicht sichtbar sein, ihre Aufgabe endet mit dem letzten Build-Schritt, der sie referenziert.
ENV ist dagegen die richtige Wahl für alles, was die Anwendung zur Laufzeit tatsächlich benötigt: Datenbank-Hostnamen, Feature-Flags, Log-Level, Zeitzonen-Einstellungen oder Pfade, die von der Anwendung selbst über Umgebungsvariablen ausgelesen werden. Ein guter Test zur Entscheidung: Würde die Anwendung im laufenden Container mit process.env oder getenv() auf den Wert zugreifen wollen? Wenn ja, gehört er in ENV, nicht in ARG.
FROM php:8.4-fpm-alpine AS base
# ARG: steuert nur den Build (welche Composer-Abhaengigkeiten)
ARG APP_ENV=production
RUN if [ "$APP_ENV" = "production" ]; then \
composer install --no-dev --optimize-autoloader; \
else \
composer install; \
fi
# ENV: wird von der Anwendung zur Laufzeit ausgelesen
ENV APP_ENV=${APP_ENV}
ENV TZ=Europe/Berlin
ENV LOG_LEVEL=info
CMD ["php-fpm", "--nodaemonize"]
8. ARG in docker-compose.yml: der build.args-Abschnitt
Wer seine Images nicht direkt per docker build, sondern über docker compose build beziehungsweise docker compose up --build erzeugt, setzt ARG-Werte nicht über die CLI-Option --build-arg, sondern über den args-Abschnitt innerhalb von build in der Compose-Datei. Diese Werte werden beim Build genauso an das Dockerfile übergeben wie ein CLI-Flag und unterliegen exakt denselben Sichtbarkeitsregeln: nach dem Build sind sie verschwunden, sofern sie nicht explizit per ENV übernommen wurden.
Ein verbreiteter Fehler ist die Annahme, ein in environment definierter Wert in der Compose-Datei stehe automatisch auch als ARG im Build zur Verfügung. Das ist nicht der Fall: environment setzt ausschließlich Laufzeit-Umgebungsvariablen für den späteren Container, ganz unabhängig vom Build-Prozess, während build.args ausschließlich Build-Zeit-Werte betrifft. Beide Mechanismen sind strikt getrennt und müssen bei Bedarf beide gepflegt werden, falls ein Wert sowohl beim Bauen als auch zur Laufzeit gebraucht wird.
# docker-compose.yml
services:
app:
build:
context: .
args:
BUILD_VERSION: "2.4.1" # entspricht --build-arg BUILD_VERSION=2.4.1
environment:
APP_ENV: production # nur zur Laufzeit sichtbar, unabhaengig vom Build
image: mironsoft/app:2.4.1
9. Weitere Details zum Scoping-Verhalten
Eine Besonderheit betrifft die ARG-Zeile vor der allerersten FROM-Anweisung: Sie kann in der FROM-Zeile selbst referenziert werden, etwa um die Basis-Image-Version dynamisch zu wählen, zählt aber nach dem FROM nicht mehr automatisch als deklariert. Für eine Nutzung innerhalb der Stage muss sie, wie bereits gezeigt, dort erneut per ARG ohne neuen Default-Wert aufgeführt werden, damit der zuvor übergebene Wert übernommen wird.
ENV-Variablen wiederum werden, einmal gesetzt, an alle nachfolgenden Anweisungen innerhalb derselben Stage vererbt, einschließlich weiterer RUN-Befehle, und überschreiben dabei auch eventuell vorher per ARG gesetzte gleichnamige Variablen für den Rest der Stage. Wichtig ist außerdem, dass jede ENV-Anweisung einen eigenen Image-Layer erzeugt und der Wert somit dauerhaft und unveränderlich Teil der Image-Metadaten wird, sichtbar für jeden, der später docker inspect auf das Image anwendet.
# ENV-Werte eines fertigen Images einsehen
docker inspect mironsoft/app:latest --format '{{.Config.Env}}'
# ARG-Werte aus der Build-Historie auslesen (Warnung: auch Secrets!)
docker history --no-trunc mironsoft/app:latest | grep ARG
| Aspekt | ARG | ENV |
|---|---|---|
| Sichtbar während des Builds | Ja | Ja |
| Sichtbar im laufenden Container | Nein, verschwindet nach dem Build | Ja, dauerhaft als Umgebungsvariable |
| Setzbar von außen | Ja, per --build-arg | Nein, nur im Dockerfile oder per docker run -e (überschreibt) |
| Über Multi-Stage-Grenzen hinweg gültig | Nein, muss pro Stage neu deklariert werden | Nein, muss pro Stage neu gesetzt werden |
| Geeignet für Secrets | Nein, landet in Build-Historie | Nein, landet dauerhaft im Image |
Mironsoft
Container-Infrastruktur, CI-Pipelines und Deployment-Automatisierung
Docker-Setups, die im Team und in Produktion tragfähig bleiben?
Wir prüfen bestehende Dockerfiles und Compose-Stacks auf Sicherheitslücken, aufgeblähte Images und fragile Build-Pipelines und bauen daraus eine Container-Infrastruktur, die schnell baut, sicher läuft und im Team nachvollziehbar bleibt.
Dockerfile-Review
Multi-Stage-Builds, Layer-Caching und Image-Größe systematisch optimieren.
Security-Audit
Container-Isolation, Secrets-Handling und Image-Scanning gegen echte Angriffsflächen absichern.
CI/CD-Integration
Build-Pipelines, Registries und Deployment-Strategien für reproduzierbare Releases aufbauen.
10. Zusammenfassung
ARG vs. ENV: Das Wichtigste auf einen Blick
ARG = Build-Zeit
Nur während docker build sichtbar, verschwindet vollständig im fertigen Image.
ENV = Laufzeit
Wird dauerhaft Teil der Image-Metadaten und ist in jedem gestarteten Container sichtbar.
Multi-Stage-Grenzen
Beide müssen in jeder einzelnen Build-Stage erneut deklariert bzw. gesetzt werden.
Secrets niemals über ARG
BuildKit-Secret-Mounts statt ARG verwenden, um Zugangsdaten nicht in der Historie zu verewigen.