ARG vs. ENV im Dockerfile wirklich verstehen
AI generated
FROM
RUN
Docker · Dockerfile · Build-System
ARG vs. ENV im Dockerfile
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.

17 Min. Lesezeit ARG ENV Dockerfile Multi-Stage-Build Build-Secrets

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/ zur Verfügung gestellt, ist ausschließlich für diesen einen Befehl sichtbar und wird weder in der Image-Historie noch in einem Layer dauerhaft gespeichert. Nach Abschluss der RUN-Anweisung ist der Secret-Wert vollständig verschwunden.

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.

11. FAQ: ARG vs. ENV: Das Wichtigste auf einen Blick

1Kann ich einen ARG-Wert im laufenden Container mit process.env auslesen?
Nein, ARG-Werte existieren ausschließlich während des Build-Vorgangs und werden nicht automatisch Teil der Laufzeit-Umgebung. Um einen Wert auch im Container verfügbar zu machen, muss er explizit per ENV auf den ARG-Wert gesetzt werden.
2Warum ist eine global vor FROM deklarierte ARG-Variable in einer späteren Stage plötzlich leer?
Weil Docker ARG-Deklarationen pro Stage scoped. Eine globale ARG-Zeile vor der ersten FROM-Anweisung macht die Variable zwar potenziell in jeder Stage verfügbar, sie muss aber innerhalb jeder Stage erneut per ARG referenziert werden, sonst bleibt sie dort leer.
3Ist es sicher, ein Datenbank-Passwort per --build-arg zu übergeben?
Nein. Der Wert landet standardmäßig in der Image-Historie und lässt sich mit docker history --no-trunc auslesen, zusätzlich häufig auch in Shell-Historie und CI/CD-Logs. Für Secrets sollten stattdessen BuildKit Secret Mounts mit --mount=type=secret verwendet werden.
4Was passiert, wenn ich eine ARG-Variable ohne Default-Wert und ohne --build-arg verwende?
Die Variable ist dann während des Builds leer, was in RUN-Anweisungen zu einer leeren String-Ersetzung führt, ohne dass Docker automatisch einen Fehler wirft. Das kann zu subtilen Bugs führen, etwa leeren Konfigurationswerten in generierten Dateien.
5Kann ich einen ENV-Wert beim docker run überschreiben?
Ja, mit docker run -e VARIABLENNAME=neuerWert lässt sich ein im Image per ENV gesetzter Wert zur Laufzeit überschreiben. Der Default aus dem Dockerfile gilt nur, solange keine explizite Umgebungsvariable beim Start übergeben wird.
6Muss ich ARG in jeder Build-Stage eines Multi-Stage-Builds neu deklarieren?
Ja, das ist eine der häufigsten Fallen. Jede Stage benötigt ihre eigene ARG-Zeile, um eine zuvor global deklarierte Variable nutzen zu können, selbst wenn dieselbe Variable bereits vor der ersten FROM-Zeile oder in einer früheren Stage deklariert wurde.
7Werden ENV-Werte automatisch von einer Stage in die nächste kopiert?
Nein, ENV-Werte gelten nur innerhalb der Stage, in der sie gesetzt wurden. Für eine spätere Stage müssen sie dort explizit erneut per ENV definiert werden, ein COPY --from überträgt lediglich Dateien, keine Umgebungsvariablen.
8Wofür eignet sich ARG am besten, wenn nicht für Laufzeit-Konfiguration?
Für alles, was ausschließlich den Build-Prozess steuert: Basis-Image-Versionen in FROM, Auswahl zwischen Build-Varianten wie debug und release, Compiler- oder Optimierungs-Flags, oder temporäre Werte, die nach dem letzten referenzierenden RUN-Befehl keine Rolle mehr spielen.
9Gibt es vordefinierte ARG-Variablen, die ich nicht selbst deklarieren muss?
Ja, Docker unterstützt automatisch verfügbare ARG-Variablen wie HTTP_PROXY, HTTPS_PROXY und NO_PROXY für Build-Umgebungen hinter einem Proxy. Eigene Variablennamen müssen dagegen immer explizit im Dockerfile per ARG deklariert werden.
10Wie kann ich nachträglich prüfen, welche ENV-Werte in einem fertigen Image gesetzt sind?
Mit docker inspect --format '{{.Config.Env}}' lassen sich alle im Image fest hinterlegten Umgebungsvariablen einsehen. Das ist auch ein guter Weg, um versehentlich per ENV statt per BuildKit-Secret gesetzte sensible Werte aufzuspüren.