edu@dorner-it
WerkzeugeEinsteiger22 min

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
    end

VMs 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.0

Layer & 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 --> L6

Goldene 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 Host

Mehrere 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 Datenbank

Container 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_Store

Ohne .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 als node/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 3000

App 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
  • .dockerignore ergänzen
  • Alpine- oder Distroless-Basisimage prüfen
  • docker image history myapp zeigt, welcher Layer wie groß ist

Praxis-Übung

Bau in einem leeren Ordner ein winziges Image:

mkdir docker-hello && cd docker-hello

app.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 hello

Spiele dann damit:

  • Ändere app.js, baue neu — welcher Layer wird gecached?
  • Mount den Quellcode mit -v "$(pwd)":/app und schau, was passiert.

Weiterführendes


Ein Container ist nur ein isolierter Prozess mit eigenem Datei­ system. Wenn du das verinnerlicht hast, ist der Rest Werkzeugkunde. 🐳