Docker Patterns
Best Practices für Docker und Docker Compose in der containerisierten Entwicklung.
Docker Compose für die lokale Entwicklung
Standard-Web-App-Stack
# docker-compose.yml
services:
app:
build:
context: .
target: dev # Verwende dev-Stufe des Multi-Stage-Dockerfiles
ports:
- "3000:3000"
volumes:
- .:/app # Bind-Mount für Hot-Reload
- /app/node_modules # Anonymes Volume – bewahrt Container-Abhängigkeiten
environment:
- DATABASE_URL=postgres://postgres:postgres@db:5432/app_dev
- REDIS_URL=redis://redis:6379/0
- NODE_ENV=development
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
command: npm run dev
db:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app_dev
volumes:
- pgdata:/var/lib/postgresql/data
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redisdata:/data
mailpit: # Lokales E-Mail-Testing
image: axllent/mailpit
ports:
- "8025:8025" # Web-UI
- "1025:1025" # SMTP
volumes:
pgdata:
redisdata:
Dockerfile für Entwicklung vs. Produktion
# Stage: dependencies
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# Stage: dev (Hot-Reload, Debug-Tools)
FROM node:22-alpine AS dev
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
# Stage: build
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build && npm prune --production
# Stage: production (minimales Image)
FROM node:22-alpine AS production
WORKDIR /app
RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001
USER appuser
COPY --from=build --chown=appuser:appgroup /app/dist ./dist
COPY --from=build --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=build --chown=appuser:appgroup /app/package.json ./
ENV NODE_ENV=production
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
Override-Dateien
# docker-compose.override.yml (automatisch geladen, Nur-Entwicklung-Einstellungen)
services:
app:
environment:
- DEBUG=app:*
- LOG_LEVEL=debug
ports:
- "9229:9229" # Node.js-Debugger
# docker-compose.prod.yml (explizit für Produktion)
services:
app:
build:
target: production
restart: always
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
# Entwicklung (lädt automatisch Override)
docker compose up
# Produktion
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Netzwerke
Service-Discovery
Dienste im selben Compose-Netzwerk werden über den Dienstnamen aufgelöst:
# Vom „app“-Container:
postgres://postgres:postgres@db:5432/app_dev # „db“ wird zum db-Container aufgelöst
redis://redis:6379/0 # „redis“ wird zum redis-Container aufgelöst
Benutzerdefinierte Netzwerke
services:
frontend:
networks:
- frontend-net
api:
networks:
- frontend-net
- backend-net
db:
networks:
- backend-net # Nur von api erreichbar, nicht von frontend
networks:
frontend-net:
backend-net:
Nur das Nötigste freigeben
services:
db:
ports:
- "127.0.0.1:5432:5432" # Nur vom Host aus zugänglich, nicht aus dem Netzwerk
# Ports in der Produktion vollständig weglassen – nur innerhalb des Docker-Netzwerks zugänglich
Volume-Strategien
volumes:
# Benanntes Volume: bleibt über Container-Neustarts hinweg erhalten, von Docker verwaltet
pgdata:
# Bind-Mount: bildet Host-Verzeichnis in den Container ab (für Entwicklung)
# - ./src:/app/src
# Anonymes Volume: bewahrt container-generierte Inhalte vor Überschreibung durch Bind-Mount
# - /app/node_modules
Häufige Muster
services:
app:
volumes:
- .:/app # Quellcode (Bind-Mount für Hot-Reload)
- /app/node_modules # Schützt das node_modules des Containers vor dem Host
- /app/.next # Schützt den Build-Cache
db:
volumes:
- pgdata:/var/lib/postgresql/data # Dauerhafte Daten
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql # Initialisierungs-Skripte
Containersicherheit
Dockerfile-Härtung
# 1. Bestimmte Tags verwenden (niemals :latest)
FROM node:22.12-alpine3.20
# 2. Als Nicht-Root ausführen
RUN addgroup -g 1001 -S app && adduser -S app -u 1001
USER app
# 3. Capabilities entfernen (in Compose)
# 4. Nur-Lese-Root-Dateisystem, wo möglich
# 5. Keine Geheimnisse in Image-Layern
Compose-Sicherheit
services:
app:
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp
- /app/.cache
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Nur beim Binden an Ports < 1024
Geheimnisverwaltung
# GUT: Umgebungsvariablen verwenden (zur Laufzeit injiziert)
services:
app:
env_file:
- .env # .env niemals in Git committen
environment:
- API_KEY # Wird von der Host-Umgebung geerbt
# GUT: Docker-Secrets (Swarm-Modus)
secrets:
db_password:
file: ./secrets/db_password.txt
services:
db:
secrets:
- db_password
# SCHLECHT: Hartcodiert im Image
# ENV API_KEY=sk-proj-xxxxx # NIEMALS SO MACHEN
Gehärtete CLI-Installations-Testumgebungen
Verwenden Sie Container, um das Installationsverhalten anhand von wegwerfbaren Projektkopien zu testen, ohne dass der Test das Quell-Checkout verändert.
Die Plattformgrenze respektieren
- Führen Sie echte Container für Linux-Distributionen wie Debian und Ubuntu aus.
- macOS kann nicht als Docker-Container ausgeführt werden, da Docker einen Linux-Kernel teilt. Führen Sie den selben shell-freien Test-Einstiegspunkt nativ auf macOS aus.
- Windows-Container erfordern eine Windows-Docker-Engine. Führen Sie plattformunabhängige Logik auf einem nativen Windows-CI-Runner aus und reservieren Sie Windows-Container für einen Windows-Host.
- Behalten Sie eine native Ubuntu/macOS/Windows-CI-Matrix für hostspezifische Pfade, Befehlsshims, Zitierung und Dateisystemverhalten bei.
Behaupten Sie nicht, dass ein Linux-Container das Verhalten von macOS oder Windows validiert.
Den Isolationsvertrag durchsetzen
- Basis-Images per unveränderlichem Digest festlegen und installierte CLI-Versionen festlegen.
- Als nicht privilegierte numerische UID/GID ausführen, wenn sich die Kontonamen der Distributionen unterscheiden.
- Das Repository und das Quellprojekt schreibgeschützt einbinden.
- Das Quellprojekt vor jeder Änderung in einen beschreibbaren
tmpfs-Arbeitsbereich kopieren. /workspacemitnoexec, UID/GID 1000 undmode=0700einbinden, sodass nur der Container-Benutzer Projektdaten einsehen kann.- Den ausführbaren Cache von npm und npx unter
NPM_CONFIG_CACHE=/tmp/npm-cacheauf dem ausführbaren/tmp-Mount belassen. Seine Standardgröße beträgt 2 GiB und kann mitECC_TMPFS_SIZEangepasst werden;ECC_WORKSPACE_SIZEsteuert separat den privaten Workspace-Mount. read_only: true,no-new-privileges:true,cap_drop: [ALL]und ein endlichespids_limitsetzen.- Die standardmäßigen realen CLI-Dienste auf
network_mode: nonebelassen. Netzwerkzugriff nur über einen sichtbar benannten Opt-in-Dienst für eine authentifizierte Anbietersitzung bereitstellen; niemals als versehentlichen, umgebungsgesteuerten Standard. - Nur die temporären Schreibpfade erstellen, die das Werkzeug benötigt.
- Standardmäßig keine Host-Anmeldeinformationen in den Container übergeben.
- Standardmäßig einen Trockenlauf durchführen und nur die expliziten Modi
dry-run,install,pluginundshellzulassen. - Argument-Arrays oder
spawnSync(..., { shell: false })für plattformübergreifende Runner verwenden. Projektpfade niemals in einen Shell-Befehl interpolieren.
Das ECC-Plugin-Setup-Harness verwenden
Verwenden Sie docker/plugin-setup/compose.yaml als Referenzimplementierung. Es bietet:
fixture-testsfür die fokussierte Installationsmanifest-, Ziel- und Ausführersuite.real-clifür das festgelegte Debian-basierte generische Linux-Image.real-cli-ubuntufür das festgelegte Ubuntu-Image.
Validieren Sie das Compose-Modell vor dem Bauen:
docker compose -f docker/plugin-setup/compose.yaml config --quiet
Beide Linux-Images bauen:
docker compose -f docker/plugin-setup/compose.yaml \
build real-cli real-cli-ubuntu
Den sicheren Standardablauf in jedem Image ausführen:
docker compose -p ecc-plugin-debian-test \
-f docker/plugin-setup/compose.yaml \
run --rm -T real-cli dry-run
docker compose -p ecc-plugin-ubuntu-test \
-f docker/plugin-setup/compose.yaml \
run --rm -T real-cli-ubuntu dry-run
Der Trockenlauf führt den aktuellen öffentlichen Befehlsvertrag aus:
ecc install --profile core --target claude-project --dry-run --json
Bevor dieser Befehl ausgeführt wird, erstellt der Container ein lokal gepacktes npm-Artefakt
aus dem schreibgeschützten Checkout mit npm pack --ignore-scripts. Er extrahiert das
selbsterstellte Tarball unter /tmp, validiert den Paketnamen ecc-universal,
erforderliche Installationsmanifeste und die eingeschränkte package.json-bin.ecc-Zuordnung,
und ruft dann die extrahierte ecc-ausführbare Datei auf. Die Laufzeit bleibt auf
network_mode: none, führt keine Paket-Lebenszyklus-Skripte aus und verlässt sich nicht
auf node_modules des Hosts; seine genauen, festgelegten Produktionsabhängigkeiten sind
bereits im Image vorhanden.
Das Harness lehnt einen leeren Plan, ein nicht-claude-project-Ziel, jeden Vorgang
außerhalb von /workspace/project/.claude oder jeden Trockenlauf ab, der das Zielverzeichnis
erstellt. install führt die isolierte Anwendung zweimal aus, überprüft seinen verwalteten
Installationsstatus, listet das installierte Ziel auf und führt doctor aus.
Eine benannte Sitzung starten, öffnen, wieder verbinden und bereinigen
Einen Container im abgetrennten (detached) Modus ohne --rm starten, damit das Verlassen
des Terminals die Sitzung nicht entfernt:
docker compose -p ecc-plugin-session \
-f docker/plugin-setup/compose.yaml \
run --detach --name ecc-plugin-shell real-cli shell
Der Container kopiert die schreibgeschützte Fixture in das stabile private Verzeichnis
/workspace/project. Bestätigen Sie, dass er läuft, und geben Sie dann die Docker-Seite des
data-Vertrags des Terminal-Öffners v1 aus:
docker inspect --format '{{.State.Running}}' ecc-plugin-shell
node docker/plugin-setup/interactive-plan.js \
--container ecc-plugin-shell \
--workdir /workspace/project \
--json \
-- bash
Das JSON-Ergebnis hat genau eine executable- und argv-Grenze (plus
contractVersion: 1): Die ausführbare Datei ist docker, und argv beginnt mit
exec, -it und -w. Übergeben Sie diese Daten an die separate Terminal-Öffner-Skill,
wenn sie installiert ist. Dieses Docker-Harness importiert bewusst keinen Terminal-Adapter,
interpoliert keinen Shell-Befehl und verwaltet keinen Host-GUI-Prozess. Bis dahin öffnen Sie
das selbe PTY direkt im aktuellen Host-Terminal:
docker exec -it -w /workspace/project ecc-plugin-shell bash
Verlassen Sie die Shell, ohne den abgetrennten Container zu stoppen. Mit dem selben
docker exec -it-Befehl wieder verbinden. Wenn Sie fertig sind, entfernen Sie den genauen
benannten Container und seine Compose-Projektressourcen:
docker rm --force ecc-plugin-shell
docker compose -p ecc-plugin-session \
-f docker/plugin-setup/compose.yaml \
down --remove-orphans
Host-Anmeldeinformationen sind standardmäßig nicht vorhanden und Anmeldeinformationsverzeichnisse
werden niemals eingebunden. Der Standarddienst hat auch keinen Netzwerkzugriff. Wenn eine
authentifizierte Anbietersitzung tatsächlich ein Netzwerk benötigt, erstellen Sie real-cli
zuerst und entscheiden Sie sich dann sichtbar mit docker compose --profile networked run real-cli-networked shell. Bevorzugen Sie die Authentifizierung innerhalb dieser
wegwerfbaren Sitzung. Wenn ein CI-Durchlauf eine Host-Umgebungsanmeldung erben muss, machen
Sie diesen Opt-In beim Aufruf mit einem expliziten Compose---env NAME-Flag, verstehen
Sie, dass der Wert einsehbar ist und für die Lebensdauer des Containers exfiltriert werden
kann, und entfernen Sie den genauen benannten Container sofort danach.
Führen Sie die gleiche fokussierte Suite nativ auf dem Host aus:
npm run test:plugin-setup-platform
Überprüfen Sie die erstellte Identität und Umgebung, bevor Sie dem Image vertrauen:
docker image inspect ecc-plugin-setup:debian ecc-plugin-setup:ubuntu
Bereinigen Sie jedes benannte Testprojekt, ohne nicht zusammenhängende Volumes oder Images zu löschen:
docker compose -p ecc-plugin-debian-test \
-f docker/plugin-setup/compose.yaml down --remove-orphans
docker compose -p ecc-plugin-ubuntu-test \
-f docker/plugin-setup/compose.yaml down --remove-orphans
.dockerignore
node_modules
.git
.env
.env.*
dist
coverage
*.log
.next
.cache
docker-compose*.yml
Dockerfile*
README.md
tests/
Debugging
Häufige Befehle
# Logs anzeigen
docker compose logs -f app # App-Logs verfolgen
docker compose logs --tail=50 db # Letzte 50 Zeilen von db
# Befehle im laufenden Container ausführen
docker compose exec app sh # Shell in App öffnen
docker compose exec db psql -U postgres # Mit postgres verbinden
# Überprüfen
docker compose ps # Laufende Dienste anzeigen
docker compose top # Prozesse in jedem Container
docker stats # Ressourcennutzung
# Neu erstellen
docker compose up --build # Images neu erstellen
docker compose build --no-cache app # Vollständigen Neubau erzwingen
# Aufräumen
docker compose down # Container und Netzwerke anhalten und entfernen
docker compose down -v # Auch Volumes entfernen (ZERSTÖRERISCH)
docker system prune # Ungenutzte Images/Container entfernen
Netzwerkprobleme debuggen
# DNS-Auflösung im Container prüfen
docker compose exec app nslookup db
# Konnektivität prüfen
docker compose exec app wget -qO- http://api:3000/health
# Netzwerk überprüfen
docker network ls
docker network inspect <project>_default
Anti-Patterns
Schlecht: 'latest'-Tag im Dockerfile
# ❌ Vermeiden
FROM node:latest
Schlecht: Datenbank-Passwörter hartcodieren
# ❌ Vermeiden
services:
db:
environment:
POSTGRES_PASSWORD: mein-schlechtes-passwort
Schlecht: Container als Root ausführen
# ❌ Vermeiden
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3
CMD ["python3", "app.py"]
Schlecht: Alle Ports für Dienste freigeben (db, redis)
# ❌ Vermeiden, wenn von außen nicht benötigt
services:
db:
ports:
- "5432:5432" # Nur innerhalb des Docker-Netzwerks benötigt