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

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?
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:
- Basisumgebung wählen (z. B. Node.js 18)
- Code und Abhängigkeiten einpacken
- 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
Step 1: Schritt 1: Basis-Image wählen (FROM)
FROM muss die erste Anweisung sein. Passendes Basis-Image wählen: -
2
Step 2: Schritt 2: Arbeitsverzeichnis setzen (WORKDIR)
WORKDIR /app setzen -
3
Step 3: Schritt 3: Abhängigkeiten kopieren und installieren (COPY+RUN)
Zuerst package*.json kopieren, dann npm install -
4
Step 4: Schritt 4: Anwendungscode kopieren (COPY)
Quellcode in den Container kopieren -
5
Step 5: Schritt 5: Port deklarieren (EXPOSE)
EXPOSE 3000 – nur Dokumentation -
6
Step 6: Schritt 6: Startbefehl setzen (CMD)
CMD [“npm”, “start”] als Standard-Startbefehl -
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-app → node server.js. docker run my-app script.js → node script.js. ENTRYPOINT bleibt, Parameter kommen von CMD oder docker run.
Wann was?
- Nur CMD: Anwendungsdienst, verschiedene Startmodi (Produktion
npm start, Testsnpm 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=productionfür Node.js-ProduktionsmodusPATHum 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 FormatName: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-MappingHost:Containermy-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:
- Code schreiben (package.json + server.js)
- Dockerfile schreiben (Build-Anleitung)
- Image bauen (
docker build) - 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:
- Datei ins Projektverzeichnis verschieben
- 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.
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:
- Kernanweisungen: FROM, RUN, COPY, CMD – WORKDIR und ENV als Hilfen
- Build-Context: COPY nur im Verzeichnis des
docker build-Arguments, kein ../ oder absolute Pfade - 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?
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?
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?
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?
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?
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?
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?
• 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
Docker Praxisleitfaden
Wenn du über die Suche hier gelandet bist, kommst du am schnellsten weiter, indem du zum vorherigen oder nächsten Beitrag dieser Serie springst.
Vorheriger
Docker-Installation ohne Fallstricke (2025): Vollständige Lösungen von permission denied bis zum erfolgreichen Start
WSL 2 unter Windows, Chip-Version unter macOS, Berechtigungen und fehlende Pakete unter Linux? Dieser Leitfaden fasst über 10 häufige Docker-Installationsfehler auf allen drei Plattformen zusammen – mit konkreten Lösungen von permission denied bis zum erfolgreichen Start.
Teil 2 von 38
Nächster
Dockerfile-Optimierung in der Praxis: 5 Tipps, die Image-Größe um 80 % reduzieren
Docker-Images von mehreren GB? Mit Alpine-Basis-Image, zusammengeführten RUN-Anweisungen, Multi-Stage-Build, .dockerignore und Cache-Bereinigung schrumpft ein Image von 1,2 GB auf 180 MB – minus 85 %. Inklusive vollständigem Node.js-Optimierungsfall und Messdaten.
Teil 4 von 38
Ähnliche Beiträge
Docker vs. virtuelle Maschine: Performance-Unterschiede und Szenario-Auswahl in 5 Minuten

Docker vs. virtuelle Maschine: Performance-Unterschiede und Szenario-Auswahl in 5 Minuten
Docker Multi-Stage-Build in der Praxis: Go-/Java-/Rust-Images von GB auf MB schlanken


Kommentare
Melde dich mit GitHub an, um einen Kommentar zu hinterlassen