Dockerfile schreiben
Eigene Docker-Images bauen: FROM, WORKDIR, COPY, RUN, CMD, ENV - und die Best Practices, die den Unterschied machen.
Inhaltsverzeichnis
Dockerfile schreiben
Ein Dockerfile ist das Rezept, um ein eigenes Image zu bauen. Es listet Schritt fuer Schritt auf, was im Image enthalten sein soll.
Das erste Dockerfile
Nehmen wir eine kleine Node.js-App. Projektstruktur:
meine-app/
โโโ Dockerfile
โโโ package.json
โโโ package-lock.json
โโโ server.js
server.js:
import http from "node:http";
const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hallo aus dem Container!\n");
});
server.listen(3000, () => console.log("Server on :3000"));
Dockerfile:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Build:
docker build -t meine-app:1.0 .
Run:
docker run -d -p 3000:3000 meine-app:1.0
Im Browser: http://localhost:3000. Fertig.
Die wichtigsten Instruktionen
FROM - das Basis-Image
FROM node:22-alpine
node:22-alpine- Node.js 22 auf minimaler Alpine-Linux-Basispython:3.12-slim- Python 3.12, schlanke Debian-Varianteubuntu:24.04- volle Ubuntu-Basisscratch- leer, fuer statische Binaries (z.B. Go)
Tipp: Wenn moeglich, nimm alpine oder slim - deutlich kleiner.
WORKDIR - Arbeitsverzeichnis
WORKDIR /app
Erstellt /app und wechselt dort rein. Alle folgenden Befehle laufen hier.
COPY - Dateien rein
COPY package*.json ./
COPY . .
Kopiert vom Host ins Image. Pfade sind relativ zum Dockerfile.
Warum in zwei Schritten?
- Erst
package*.jsonkopieren undnpm ci- das wird gecached. - Dann den Rest kopieren.
Wenn sich nur Code aendert, bleibt der langsame npm ci-Schritt im Cache. Big win.
RUN - Kommando beim Build
RUN npm ci
RUN apt-get update && apt-get install -y curl
RUN laeuft zur Build-Zeit, nicht beim Container-Start.
CMD - was beim Start passiert
CMD ["node", "server.js"]
Das default Kommando des Containers. Jede Zeile in JSON-Array-Form (exec form) - nicht als String.
Exec form vermeidet eine Shell zwischen drin - bessere Signal-Behandlung.
ENTRYPOINT vs. CMD
Beide starten den Prozess. Unterschied:
CMDkann ueberschrieben werden:docker run meine-app anderer-befehlENTRYPOINTist fest, CMD wird zu Argumenten
Im Alltag reicht meist CMD.
EXPOSE - Port dokumentieren
EXPOSE 3000
Ist nur Dokumentation - es oeffnet den Port nicht automatisch. -p beim docker run ist weiterhin noetig.
ENV - Environment-Variablen
ENV NODE_ENV=production
ENV PORT=3000
Werden im Container gesetzt. Kannst du beim Run ueberschreiben.
USER - als welcher User laufen
Security Best Practice: Nicht als root laufen.
FROM node:22-alpine
RUN addgroup -S app && adduser -S app -G app
USER app
WORKDIR /app
# ...
.dockerignore
Wie .gitignore - verhindert, dass unnoetige Dateien ins Image wandern:
node_modules
.git
.env
*.log
Dockerfile
.dockerignore
dist
Das macht den Build schneller (weniger Daten zum Build-Kontext) und das Image sicherer (keine Secrets aus .env drin).
Layer und Cache
Jede Dockerfile-Zeile ist ein Layer. Docker cached Layer und baut ab der ersten Aenderung neu.
Schlechte Reihenfolge
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci
CMD ["node", "server.js"]
Problem: Jede Code-Aenderung invalidiert den Cache fuer npm ci - der Build wird langsam.
Bessere Reihenfolge
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]
Nur wenn sich package*.json aendert, wird npm ci neu ausgefuehrt.
docker build - die Build-Befehle
docker build -t meine-app:1.0 .
-t name:tag- Name und Tag.- Build-Kontext (aktuelles Verzeichnis)
Multi-Tag:
docker build -t meine-app:1.0 -t meine-app:latest .
Build-Args:
docker build --build-arg VERSION=1.2.3 -t meine-app:1.2.3 .
Im Dockerfile dazu:
ARG VERSION=dev
ENV APP_VERSION=$VERSION
Ein Python-Beispiel
FROM python:3.12-slim AS build
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
Beachte: --no-cache-dir bei pip macht Images kleiner.
Ein Go-Beispiel mit statischem Binary
Go-Binaries sind statisch - Image kann extrem klein sein:
FROM golang:1.23 AS build
WORKDIR /src
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app .
FROM scratch
COPY --from=build /out/app /app
EXPOSE 8080
CMD ["/app"]
Das Ergebnis: vielleicht 5 MB statt 1 GB mit Go-Toolchain.
Best Practices zusammengefasst
1. Offizielle Images nehmen
node:22-alpine statt selbst Alpine + Node bauen. Wartung, Security, Updates - alles schon erledigt.
2. Spezifische Tags
node:22.12.0-alpine > node:22-alpine > node:latest.
3. Kleine Basis-Images
Alpine (~5 MB) statt Ubuntu (~70 MB) - wenn Kompatibilitaet stimmt.
4. .dockerignore nutzen
Keine node_modules, .git, .env im Image.
5. Cache-freundliche Reihenfolge
Dependencies zuerst, Code zuletzt.
6. Keine Secrets im Image
Niemals COPY .env oder ENV API_KEY=.... Secrets via Env-Variablen zur Laufzeit oder Docker Secrets.
7. Als Nicht-Root laufen
USER app fuer Security.
8. CMD in exec form
CMD ["node", "server.js"] statt CMD node server.js.
9. Nur Prod-Dependencies in Prod-Image
RUN npm ci --production
10. Multi-Stage Builds
Siehe naechstes Kapitel - das ist das Muster fuer schlanke Images.
Image pruefen
docker image ls # alle Images
docker image inspect meine-app:1.0
docker history meine-app:1.0 # Layer-Historie
# Was macht das Image gross? Tool `dive`:
dive meine-app:1.0
Zusammenfassung
FROM- Basis,WORKDIR- Arbeitsverzeichnis,COPY- Dateien reinRUNbeim Build,CMDbeim Start.dockerignorefuer cleanen Build-Context- Cache-freundliche Layer-Reihenfolge spart Build-Zeit
- Alpine/slim fuer kleine Images, spezifische Tags, Nicht-Root-User
Im naechsten Kapitel: Multi-Stage Builds fuer noch kleinere Images.