Design wechseln

Dockerfile-Einstieg: Erstes Docker-Image von null (mit Beispielen)

Easton editorial illustration: image-layer stack

Im Terminal rollen Fehlermeldungen – „COPY ../config.json: no such file or directory“. Heute schon der achte fehlgeschlagene Build. Lokal läuft alles, im Docker-Image bricht es überall zusammen. Stack Overflow durchforstet, Top-Answer übernommen – und das Image wächst von 200 MB auf 2 GB.

In Ihrem Projekt liegt eine Dockerfile-Datei voller FROM, RUN, COPY, CMD – einzeln verständlich, zusammen verwirrend. Tutorials kopieren, in neun von zehn Fällen startet der Container nicht. Die Fehlermeldungen bleiben vage: falscher Pfad oder falsche Anweisung?

180x
Größenunterschied
Alpine vs. vollständiges Image
10x
Größenreduktion
RUN-Anweisungen zusammenfassen
30x
Build-Beschleunigung
5 Min. → 10 Sek.
Source: Praxisdaten

Ein Dockerfile ist nicht so mysteriös, wie es wirkt. Sobald Sie jede Anweisung und die typischen Fallen kennen, bleiben wenige Kernpunkte. Dieser Artikel erklärt von null aus, wie Sie Ihr erstes Docker-Image bauen – mit Code zu jeder Anweisung und den drei häufigsten Anfängerfehlern. Danach können Sie für Node.js- oder Python-Projekte ein lauffähiges Dockerfile schreiben.

Was ist ein Dockerfile?

Kurz gesagt: Ein Dockerfile ist eine Textdatei mit allen Schritten zum Bau eines Docker-Images. Stellen Sie es sich wie einen Bauplan vor – zuerst Boden, dann Wände, dann Lampen. Die Docker-Engine folgt diesem Plan Schritt für Schritt und packt Ihre Anwendung in ein Image.

Das Image ist ein lauffähiger „Umgebungsschnappschuss“. Ihre Node.js-App braucht Node 18, npm-Pakete und Quellcode – das Dockerfile packt alles hinein. Wer das Image hat, startet es mit docker run, ohne die Umgebung manuell einzurichten.

Im Kern macht ein Dockerfile drei Dinge:

  1. Basisumgebung wählen (z. B. Node.js 18)
  2. Code und Abhängigkeiten einpacken
  3. Den Startbefehl für den Container festlegen

Nach dem Schreiben reicht ein docker build – das Image entsteht. Klingt einfach? Stimmt – aber der Teufel steckt im Detail. Als Nächstes die Kernanweisungen.

Die 6 Kernanweisungen im Detail

Vollständiger Ablauf: Erstes Docker-Image von null

Schritt für Schritt Dockerfile schreiben – von der Basis-Image-Wahl bis zum ersten lauffähigen Image

Estimated time: PT20M

  1. 1

    Step 1: Schritt 1: Basis-Image wählen (FROM)

    FROM muss die erste Anweisung sein. Passendes Basis-Image wählen:
  2. 2

    Step 2: Schritt 2: Arbeitsverzeichnis setzen (WORKDIR)

    WORKDIR /app setzen
  3. 3

    Step 3: Schritt 3: Abhängigkeiten kopieren und installieren (COPY+RUN)

    Zuerst package*.json kopieren, dann npm install
  4. 4

    Step 4: Schritt 4: Anwendungscode kopieren (COPY)

    Quellcode in den Container kopieren
  5. 5

    Step 5: Schritt 5: Port deklarieren (EXPOSE)

    EXPOSE 3000 – nur Dokumentation
  6. 6

    Step 6: Schritt 6: Startbefehl setzen (CMD)

    CMD [“npm”, “start”] als Standard-Startbefehl
  7. 7

    Step 7: Schritt 7: Bauen und starten

    Image bauen:

FROM – Basis-Image wählen

FROM muss die erste Anweisung sein (außer Kommentaren und ARG). Es legt das „Fundament“ fest.

Node.js-App? node:18-alpine. Python? python:3.11-slim. Nginx als Reverse Proxy? nginx:alpine.

# Node.js 18 auf Alpine Linux als Basis
FROM node:18-alpine

alpine vs. slim: Alpine ist extrem klein (ca. 5 MB), gut für Produktion. Ein vollständiges node-Image hat ca. 900 MB – Faktor 180. Alpine nutzt musl libc statt glibc; manche native Abhängigkeiten können fehlschlagen. Bei Compile-Fehlern node:18-slim probieren.

Typischer Anfängerfehler: Beliebiges node-Image wählen, Version passt nicht zu package.json – Abhängigkeiten schlagen fehl. Die Node-Version in package.json = die Version in FROM.

RUN – Befehle beim Image-Build

RUN führt Befehle beim Build aus – Software installieren, Verzeichnisse anlegen, Konfiguration ändern. Wichtig: Jede RUN-Anweisung erzeugt eine neue Layer.

Schlechtes Beispiel:

# ❌ Schlecht: 3 Layer
RUN apt-get update
RUN apt-get install -y python3
RUN apt-get clean

Jede RUN packt eine Schicht drumherum. Gelöschte Dateien aus späteren RUN-Befehlen verschwinden nicht aus früheren Layern – das Image bleibt groß. Sieben separate RUN-Befehle, 2 GB Image, Upload dauert ewig.

Besser mit && verketten:

# ✅ Empfohlen: nur 1 Layer
RUN apt-get update && \
    apt-get install -y python3 && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

Der Backslash \ ist Zeilenumbruch. Das abschließende rm räumt Paket-Caches auf – spart dutzende MB.

Wichtig: Nie RUN apt-get update allein stehen lassen. Docker cached Layer – ein isoliertes update kann veralteten Cache nutzen und neue Pakete blockieren. update und install immer zusammen.

COPY vs. ADD – Dateien kopieren

Beide kopieren Dateien ins Image. COPY ist einfach und vorhersehbar; ADD hat Extras, die überraschen können. Offizielle Empfehlung: COPY statt ADD, wenn möglich.

# Einzelne Datei
COPY package.json /app/

# Verzeichnis
COPY ./src /app/src

# Alles aus dem aktuellen Verzeichnis nach /app
COPY . /app

Häufigster Irrtum: Pfade sind relativ zum Build-Context, nicht zum Dockerfile.

Der Build-Context ist das Verzeichnis des letzten Arguments bei docker build – z. B. docker build . im Projektroot. COPY darf nur Dateien in diesem Verzeichnis und darunter lesen.

Deshalb der Fehler am Anfang:

# ❌ Außerhalb des Build-Context
COPY ../config.json /app/
COPY /opt/myfile.txt /app/

Erstes will ins übergeordnete Verzeichnis, zweites einen absoluten Pfad – beides scheitert. Docker erlaubt das aus Sicherheits- und Reproduzierbarkeitsgründen nicht.

Lösungen:

  • config.json ins Projektverzeichnis verschieben
  • Build von oben: docker build -f myproject/Dockerfile .

ADD: Entpackt tar-Archive automatisch und kann von URLs laden:

# ADD entpackt automatisch
ADD myarchive.tar.gz /app/

# ADD lädt von URL (nicht empfohlen)
ADD https://example.com/file.txt /app/

Verhalten ist weniger transparent. Lieber explizit RUN tar -xzf oder RUN curl – klarer für alle Leser.

WORKDIR – Arbeitsverzeichnis

WORKDIR entspricht cd – setzt das Arbeitsverzeichnis für folgende Befehle. Fehlt das Verzeichnis, legt Docker es an.

WORKDIR /app
COPY . .  # Kopiert nach /app
RUN npm install  # Läuft in /app

Absolute Pfade nutzen. Relative Pfade bauen auf dem vorherigen WORKDIR auf – leicht verwirrend.

Mit WORKDIR sparen Sie in jedem RUN cd /app && – das Dockerfile bleibt lesbar.

CMD vs. ENTRYPOINT – Container-Start

CMD kann überschrieben werden, ENTRYPOINT nicht.

CMD setzt den Standard-Startbefehl:

CMD ["node", "server.js"]

docker run my-app führt node server.js aus. Mit docker run my-app npm test wird CMD überschrieben – es läuft npm test.

ENTRYPOINT definiert den Hauptprozess, der nicht überschrieben wird:

ENTRYPOINT ["node"]
CMD ["server.js"]

docker run my-appnode server.js. docker run my-app script.jsnode script.js. ENTRYPOINT bleibt, Parameter kommen von CMD oder docker run.

Wann was?

  • Nur CMD: Anwendungsdienst, verschiedene Startmodi (Produktion npm start, Tests npm test)
  • ENTRYPOINT + CMD: Tool-Images, fester Hauptbefehl, variable Parameter (Python: python + Skriptname)
  • Nur ENTRYPOINT: sehr feste Einzelfunktion
# Szenario 1: Web-App (CMD)
FROM node:18-alpine
WORKDIR /app
COPY . .
CMD ["npm", "start"]
# docker run my-app → npm start
# docker run my-app npm test → npm test (CMD überschrieben)

# Szenario 2: Python-Tool (ENTRYPOINT + CMD)
FROM python:3.11-slim
ENTRYPOINT ["python"]
CMD ["main.py"]
# docker run my-tool → python main.py
# docker run my-tool script.py → python script.py

Merksatz: ENTRYPOINT ist „was“, CMD ist „wie“.

ENV – Umgebungsvariablen

ENV setzt Variablen, die zur Laufzeit und in späteren RUN/CMD-Befehlen verfügbar sind.

ENV NODE_ENV=production
ENV PORT=3000

# In RUN nutzen
RUN echo "Environment: $NODE_ENV"

# Anwendung liest die Variablen ebenfalls
CMD ["node", "server.js"]

Typisch:

  • NODE_ENV=production für Node.js-Produktionsmodus
  • PATH um eigene Binärpfade zu ergänzen
  • App-Parameter (Port, DB-URL)

ENV-Werte landen im finalen Image. Keine Passwörter in ENV – stattdessen docker run -e oder Docker Secrets.

Praxis: Erstes Image bauen

Theorie allein reicht nicht – wir bauen ein einfaches Node.js-Image Schritt für Schritt.

Schritt 1: Projekt vorbereiten

Minimale Node.js-App anlegen:

mkdir my-node-app
cd my-node-app

package.json:

{
  "name": "my-node-app",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  }
}

server.js:

const express = require('express');
const app = express();
const PORT = 3000;

app.get('/', (req, res) => {
  res.send('Hello from Docker!');
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Schritt 2: Dockerfile schreiben

Im Projektroot Dockerfile anlegen (ohne Dateiendung):

# 1. Basis-Image
FROM node:18-alpine

# 2. Arbeitsverzeichnis
WORKDIR /app

# 3. Abhängigkeiten (Cache nutzen)
COPY package*.json ./

# 4. Abhängigkeiten installieren
RUN npm install --production

# 5. Anwendungscode
COPY . .

# 6. Port deklarieren
EXPOSE 3000

# 7. App starten
CMD ["npm", "start"]

Warum package.json und Quellcode getrennt kopieren? Docker-Cache: Jede Anweisung ist eine Layer. Ändert sich eine Layer, werden alle folgenden neu gebaut. package.json ändert sich selten, Quellcode oft. Kopieren Sie alles auf einmal und installieren danach, läuft bei jeder Code-Änderung npm install erneut.

Mit getrennten Schritten bleibt die Abhängigkeits-Layer gecacht, solange package.json gleich bleibt – der Build wird deutlich schneller.

Schritt 3: Image bauen

Im Projektroot:

docker build -t my-node-app:1.0 .

Parameter:

  • -t my-node-app:1.0: Tag im Format Name:Version
  • .: Build-Context = aktuelles Verzeichnis

Die Ausgabe zeigt jeden Dockerfile-Schritt. Erfolg: Successfully built xxx.

Schritt 4: Container starten

docker run -p 3000:3000 my-node-app:1.0

Parameter:

  • -p 3000:3000: Port-Mapping Host:Container
  • my-node-app:1.0: Image-Name

Terminal: „Server running on port 3000“.

Schritt 5: Verifizieren

Browser: http://localhost:3000 – „Hello from Docker!“ bedeutet Erfolg.

Mit Strg+C den Container stoppen.

Kurz zusammengefasst

Ablauf:

  1. Code schreiben (package.json + server.js)
  2. Dockerfile schreiben (Build-Anleitung)
  3. Image bauen (docker build)
  4. Container starten (docker run)

Nicht so schwer – wenn Sie jede Anweisung, den Build-Context und den Cache verstehen.

Fallen für Anfänger

Drei Stolpersteine, die ich selbst alle getroffen habe – sie sparen Ihnen Zeit.

Falle 1: Falscher Build-Context-Pfad

Symptom: COPY meldet „no such file or directory“, obwohl die Datei da ist.

Ursache: COPY-Pfade sind relativ zum Build-Context, nicht zum Dockerfile.

# ❌ Übergeordnetes Verzeichnis
COPY ../config.json /app/

# ❌ Absoluter Pfad
COPY /opt/myfile.txt /app/

Lösung:

  1. Datei ins Projektverzeichnis verschieben
  2. Build anpassen: docker build -f subdir/Dockerfile . (-f für Dockerfile-Pfad, Punkt bleibt Context)

Versteckte Falle: docker build . im Root sendet node_modules, .git usw. an den Daemon – mehrere GB, Minuten Wartezeit.

.dockerignore anlegen:

node_modules
.git
.env
*.log

Falle 2: Zu viele Layer – Image bläht sich auf

Symptom: Image mehrere GB, obwohl der Code nur wenige MB hat.

Ursache: Jede RUN/COPY/ADD erzeugt eine Layer; gelöschte Dateien bleiben in früheren Layern.

# ❌ 7 Layer, Daten bleiben erhalten
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN curl -o tool.sh https://example.com/tool.sh
RUN chmod +x tool.sh
RUN ./tool.sh
RUN rm tool.sh  # Löscht nur in dieser Layer – tool.sh liegt noch in früheren

Lösung: RUN mit && in einer Layer kombinieren:

# ✅ 1 Layer, Aufräumen wirkt
RUN apt-get update && \
    apt-get install -y curl git && \
    curl -o tool.sh https://example.com/tool.sh && \
    chmod +x tool.sh && \
    ./tool.sh && \
    rm tool.sh && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

Sieben RUN-Befehle → 2 GB. Zusammengefasst → 200 MB. Faktor 10.

10x
Größenreduktion
Source: RUN zusammenfassen: 2 GB → 200 MB

Falle 3: Abhängigkeits-Cache ungültig

Symptom: Jeder Build installiert Abhängigkeiten neu – sehr langsam.

Ursache: Falsche COPY-Reihenfolge – Code und package.json zusammen, jede Code-Änderung invalidiert npm install.

# ❌ Code-Änderung → npm install neu
COPY . .
RUN npm install

Lösung: Zuerst Abhängigkeitsdateien, dann Quellcode:

# ✅ Nur bei package.json-Änderung neu installieren
COPY package*.json ./
RUN npm install
COPY . .

Code-Änderungen triggern npm install nicht mehr – von 5 Minuten auf 10 Sekunden.

Hinweis: EXPOSE ist optional

Viele Tutorials setzen EXPOSE 3000 – Anfänger denken, ohne EXPOSE kein Portzugriff. EXPOSE ist nur Dokumentation.

Port-Mapping macht docker run -p:

# Funktioniert auch ohne EXPOSE im Dockerfile
docker run -p 3000:3000 my-app

Trotzdem EXPOSE setzen – hilft anderen, die Ports zu verstehen.

Fazit

Dockerfile-Einstieg in drei Punkten:

  1. Kernanweisungen: FROM, RUN, COPY, CMD – WORKDIR und ENV als Hilfen
  2. Build-Context: COPY nur im Verzeichnis des docker build-Arguments, kein ../ oder absolute Pfade
  3. Cache: Selten geänderte Schritte (Abhängigkeiten) zuerst, häufig geänderte (Code) danach; RUN zusammenfassen

Probieren Sie es an einem kleinen Projekt – ein minimales Dockerfile, das läuft, reicht. Später kommen Multi-Stage-Builds und weitere Optimierungen.

Docker ist überschaubar, wenn Sie üben. Mein erstes Dockerfile hat einen ganzen Abend Fehler produziert – danach war es klar. Das schaffen Sie auch.

Als Nächstes:

  • Docker Compose (mehrere Container)
  • Multi-Stage-Build (kleinere Images)
  • Docker-Netzwerk und Volumes (Kommunikation und Persistenz)

Viel Erfolg beim ersten Docker-Image! Fragen gern in den Kommentaren.

FAQ

Welche Kernanweisungen hat ein Dockerfile?
6 Kernanweisungen:
1) FROM wählt das Basis-Image (muss die erste sein)
2) RUN führt Befehle beim Image-Build aus (mit && zusammenfassen, um Layer zu reduzieren)
3) COPY kopiert Dateien (Pfade relativ zum Build-Context, kein ../ und keine absoluten Pfade)
4) WORKDIR setzt das Arbeitsverzeichnis (absolute Pfade empfohlen)
5) CMD setzt den Container-Startbefehl (kann überschrieben werden)
6) ENV setzt Umgebungsvariablen

Zusätzlich: ENTRYPOINT (fester Hauptbefehl), EXPOSE (Port-Deklaration, nur Dokumentation).
Warum schlägt COPY ../config.json fehl?
COPY-Pfade sind relativ zum Build-Context, nicht zum Dockerfile.

Der Build-Context ist das Verzeichnis, das Sie bei docker build als letztes Argument angeben (meist der Punkt .). COPY darf nur Dateien in diesem Verzeichnis und seinen Unterverzeichnissen lesen – nicht im übergeordneten Verzeichnis oder per absolutem Pfad.

Lösungen:
• Datei ins Projektverzeichnis verschieben
• Build-Befehl anpassen: docker build -f subdir/Dockerfile .

Mit .dockerignore node_modules, .git und andere große Ordner ausschließen beschleunigt den Build.
Wie reduziert man die Docker-Image-Größe?
Drei Methoden:

1) Alpine-Images nutzen:
• ca. 5 MB vs. vollständiges Image ca. 900 MB – Faktor 180

2) RUN-Anweisungen zusammenfassen:
• Mit && in einer Layer installieren, nutzen und aufräumen
• Image von 2 GB auf 200 MB möglich – Faktor 10

3) .dockerignore für unnötige Dateien

apt-get update und install immer zusammen schreiben – sonst nutzt Docker veralteten Cache.
Wie nutzt man den Docker-Cache für schnellere Builds?
Regel: Selten geänderte Schritte nach oben, häufig geänderte nach unten.

Richtige Reihenfolge:
• Zuerst package*.json kopieren und Abhängigkeiten installieren
• Dann Quellcode kopieren
• Bleibt package.json unverändert, wird die Abhängigkeits-Layer nicht neu gebaut
• Build-Zeit von 5 Minuten auf 10 Sekunden (Faktor 30)

Falsche Reihenfolge:
• Alle Dateien zuerst kopieren, dann Abhängigkeiten installieren
• Bei jeder Code-Änderung npm install erneut – sehr langsam
Was ist der Unterschied zwischen CMD und ENTRYPOINT?
CMD kann durch docker-run-Parameter überschrieben werden, ENTRYPOINT nicht.

Anwendungsfälle:
1) Nur CMD: Anwendungsdienst, unterschiedliche Startmodi (Produktion npm start, Tests npm test)
2) ENTRYPOINT+CMD: Tool-Images, fester Hauptbefehl, variable Parameter (z. B. Python-Skripte)
3) Nur ENTRYPOINT: sehr feste Szenarien

Merksatz: ENTRYPOINT ist „was“, CMD ist „wie“.
Ist EXPOSE zwingend erforderlich?
Nein. EXPOSE ist nur Dokumentation – es sagt, welchen Port das Image nutzt. Ohne EXPOSE läuft der Container trotzdem.

Port-Mapping steuert docker run -p. Auch ohne EXPOSE im Dockerfile funktioniert docker run -p 3000:3000 my-app.

Trotzdem EXPOSE setzen – erleichtert anderen das Verständnis der Ports.
Was ist der Unterschied zwischen Alpine- und slim-Images?
Alpine basiert auf Alpine Linux:
• ca. 5 MB, gut für Produktion
• nutzt musl libc statt glibc – manche native Abhängigkeiten können fehlschlagen
• vollständiges Image ca. 900 MB – Faktor 180

Bei merkwürdigen Compile-Fehlern auf slim wechseln (z. B. node:18-slim).

Prinzip: zuerst Alpine, bei Kompatibilitätsproblemen slim.

9 Min. Lesezeit · Veröffentlicht am: 17. Dez. 2025 · Aktualisiert am: 14. Juli 2026

Ähnliche Beiträge

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog