Zum Inhalt springen
Docker Anfรคnger 30 min

Dockerfile schreiben

Eigene Docker-Images bauen: FROM, WORKDIR, COPY, RUN, CMD, ENV - und die Best Practices, die den Unterschied machen.

Aktualisiert:
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-Basis
  • python:3.12-slim - Python 3.12, schlanke Debian-Variante
  • ubuntu:24.04 - volle Ubuntu-Basis
  • scratch - 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?

  1. Erst package*.json kopieren und npm ci - das wird gecached.
  2. 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:

  • CMD kann ueberschrieben werden: docker run meine-app anderer-befehl
  • ENTRYPOINT ist 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 rein
  • RUN beim Build, CMD beim Start
  • .dockerignore fuer 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.

Zurรผck zum Docker Kurs