Docker Basics: Container verstehen
Was ein Container wirklich ist, wie Images aufgebaut sind und welche Docker-Kommandos du im Alltag brauchst. Mit Diagrammen und einem ersten eigenen Image.
Zuletzt aktualisiert: 11. Mai 2026
Container fühlen sich am Anfang an wie virtuelle Maschinen — sind aber etwas grundlegend anderes. Wer Docker einmal wirklich verstanden hat, sieht den Unterschied zwischen Image und Container sofort und weiß, warum dasselbe Container-Image auf jedem Rechner gleich läuft.
Lernziele
- Du verstehst den Unterschied zwischen Image, Container und Layer
- Du schreibst ein eigenes Dockerfile
- Du kennst die wichtigsten Befehle für den Alltag
- Du erkennst typische Fehlerbilder (Caches, Volumes, Ports)
Container vs. VM
flowchart TB
subgraph VM["Virtuelle Maschine"]
H1[Host-OS]
HV[Hypervisor]
G1[Gast-OS 1]
G2[Gast-OS 2]
A1[App 1]
A2[App 2]
H1 --> HV --> G1 --> A1
HV --> G2 --> A2
end
subgraph C["Container"]
H2[Host-OS]
D[Container-Runtime]
C1[App 1 + Libs]
C2[App 2 + Libs]
H2 --> D --> C1
D --> C2
endVMs virtualisieren die Hardware — jede VM bringt ihr eigenes Betriebssystem mit. Container teilen sich den Kernel des Hosts und isolieren nur Prozesse, Dateisystem und Netzwerk. Das macht sie deutlich leichter (Sekunden statt Minuten zum Start, MB statt GB).
Image vs. Container
Die wichtigste Unterscheidung in Docker — und Quelle vieler Missver ständnisse:
| Begriff | Was es ist | Analogie |
|---|---|---|
| Image | Read-only Vorlage, eine Folge von Layern | Klasse / Bauplan |
| Container | Eine laufende Instanz eines Images | Objekt / Haus |
| Layer | Eine Änderung im Dateisystem | Diff / Commit |
Du kannst aus einem Image beliebig viele Container starten — jeder mit seinem eigenen Zustand.
flowchart LR
I[Image: myapp:1.0]
I --> C1[Container A]
I --> C2[Container B]
I --> C3[Container C]Die wichtigsten Kommandos
# Image holen
docker pull nginx:alpine
# Image als Container starten
docker run -d -p 8080:80 --name web nginx:alpine
# │ │ └ Container-Name (optional)
# │ └── Port-Mapping host:container
# └── detached (im Hintergrund)
# Was läuft?
docker ps # nur laufende Container
docker ps -a # auch gestoppte
# Logs ansehen
docker logs -f web # -f = follow
# In den Container reinspringen
docker exec -it web sh
# Container stoppen & entfernen
docker stop web
docker rm web
# Aufräumen
docker image prune # ungenutzte Images
docker system prune # alles Ungenutzte (Vorsicht!)Dein erstes Dockerfile
Ein Dockerfile beschreibt Schritt für Schritt, wie ein Image gebaut wird. Jede Zeile erzeugt einen neuen Layer.
# 1. Basis-Image (selbst schon eine Stack-Image-Reihe)
FROM node:20-alpine
# 2. Arbeitsverzeichnis im Image
WORKDIR /app
# 3. Erst nur die Lockfile kopieren — wichtig fürs Caching!
COPY package.json package-lock.json ./
RUN npm ci
# 4. Dann den Rest des Codes
COPY . .
# 5. Build-Schritt
RUN npm run build
# 6. Welcher Port wird intern verwendet (Doku, kein Mapping)
EXPOSE 3000
# 7. Wie der Container gestartet wird
CMD ["node", "dist/server.js"]Bauen und laufen lassen:
docker build -t myapp:1.0 .
docker run -d -p 3000:3000 --name myapp myapp:1.0Layer & Build-Cache
Docker cached jede Zeile des Dockerfiles. Ändert sich eine Zeile, werden alle nachfolgenden neu gebaut.
flowchart TB
L1["FROM node:20-alpine<br/><small>~50 MB, cached</small>"]
L2["WORKDIR /app"]
L3["COPY package*.json<br/><small>ändert sich selten</small>"]
L4["RUN npm ci<br/><small>teuer, nur bei lockfile-Änderung</small>"]
L5["COPY . .<br/><small>ändert sich oft</small>"]
L6["RUN npm run build"]
L1 --> L2 --> L3 --> L4 --> L5 --> L6Goldene Regel: Was sich selten ändert, kommt oben ins Dockerfile. Was sich oft ändert, kommt unten. Sonst zerstörst du den Cache bei jeder Änderung.
Multi-Stage Builds
Ein Build-Image ist oft riesig (Compiler, Dev-Tools). Du willst aber ein schlankes Runtime-Image. Lösung: zwei Stages.
# Stage 1: Build
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Runtime (nur das fertige Artefakt)
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]Das fertige Image enthält nur die Runtime — der Build-Stack wird weggeworfen. Oft Reduktion um Faktor 5–10.
Volumes — Daten überleben lassen
Ein Container ist vergänglich. Wenn du ihn löschst, sind alle Änderungen im Inneren weg. Für Daten, die überleben sollen → Volumes.
# Named Volume (von Docker verwaltet)
docker run -d \
-v postgres-data:/var/lib/postgresql/data \
postgres:16
# Bind Mount (Pfad vom Host)
docker run -d \
-v "$(pwd)/src":/app/src \
-p 3000:3000 \
myapp:dev| Typ | Wofür |
|---|---|
| Named Volume | Persistente Daten (Datenbank, Uploads) |
| Bind Mount | Dev-Workflow (Hot-Reload mit lokalem Code) |
| tmpfs | Sensible Daten nur im RAM |
Netzwerke & Ports
docker run -p 8080:80 nginx
# │ └── Port im Container
# └────── Port auf dem HostMehrere Container reden über Docker-Networks miteinander:
docker network create app-net
docker run -d --name db --network app-net postgres:16
docker run -d --name api --network app-net myapi
# Im api-Container: host "db" zeigt auf die DatenbankContainer im selben Netzwerk erreichen sich über den Containernamen als Hostname. (Im nächsten Kurs zu Docker Compose passiert das automatisch.)
.dockerignore — nicht alles einpacken
Wie .gitignore, aber für docker build. Spart Zeit und Image-Größe.
node_modules
dist
.git
.env
*.log
coverage
.DS_StoreOhne .dockerignore kopiert COPY . . auch dein lokales
node_modules ins Image — was meistens ein Bug ist.
Häufige Stolperfallen
„Es funktioniert lokal, aber nicht im Container"
Erste Verdächtige:
- Du läufst lokal als
root, im Container alsnode/anderer User. - Lokale Umgebungsvariablen sind im Container nicht da.
- Pfade — Container ist Linux, dein Host vielleicht nicht.
„Mein Container startet und stoppt sofort"
Der Hauptprozess (Prozess 1, CMD) ist beendet. Ein Container lebt
nur, solange dieser Prozess läuft. docker logs <name> zeigt warum.
„Port ist nicht erreichbar"
docker run -p 3000:3000 myapp # 3000 auf 3000App hört auf 0.0.0.0, nicht nur localhost — sonst kommt nichts
von außen rein.
„Mein Image wird riesig"
- Multi-Stage-Build benutzen
.dockerignoreergänzen- Alpine- oder Distroless-Basisimage prüfen
docker image history myappzeigt, welcher Layer wie groß ist
Praxis-Übung
Bau in einem leeren Ordner ein winziges Image:
mkdir docker-hello && cd docker-helloapp.js:
const http = require("http");
http.createServer((_, res) => {
res.end("Hallo aus dem Container\n");
}).listen(3000, "0.0.0.0");
console.log("listening on 3000");Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY app.js .
EXPOSE 3000
CMD ["node", "app.js"]docker build -t hello .
docker run -d -p 3000:3000 --name hello hello
curl localhost:3000
docker logs hello
docker stop hello && docker rm helloSpiele dann damit:
- Ändere
app.js, baue neu — welcher Layer wird gecached? - Mount den Quellcode mit
-v "$(pwd)":/appund schau, was passiert.
Weiterführendes
- Offizielle Docs: https://docs.docker.com/get-started/
- Best Practices Dockerfile: https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
- Play with Docker (Browser-Sandbox): https://labs.play-with-docker.com/
- Alpine vs. Distroless vs. Slim: https://snyk.io/blog/choosing-the-best-node-js-docker-image/
Ein Container ist nur ein isolierter Prozess mit eigenem Datei system. Wenn du das verinnerlicht hast, ist der Rest Werkzeugkunde. 🐳