Docker Patterns
Docker- und Docker-Compose-Best-Practices für die containerisierte 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-Dockerfile
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: # Lokale E-Mail-Tests
image: axllent/mailpit
ports:
- "8025:8025" # Web-Benutzeroberfläche
- "1025:1025" # SMTP
volumes:
pgdata:
redisdata:
Development vs. Production Dockerfile
# Stage: Abhängigkeiten
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"]
Überschreibungsdateien
# docker-compose.override.yml (automatisch geladen, nur für Entwicklung)
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 Überschreibungen automatisch)
docker compose up
# Produktion
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Netzwerke
Dienstfindung
Dienste im selben Compose-Netzwerk werden per Dienstname aufgelöst:
# Vom „app“-Container aus:
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 komplett weglassen – nur innerhalb des Docker-Netzwerks erreichbar
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 die Entwicklung)
# - ./src:/app/src
# Anonymes Volume: bewahrt vom Container erstellte 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 # Persistente Daten
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql # Initialisierungsskripte
Containersicherheit
Dockerfile-Härtung
# 1. Spezifische Tags verwenden (niemals :latest)
FROM node:22.12-alpine3.20
# 2. Als Nicht-root-Benutzer ausführen
RUN addgroup -g 1001 -S app && adduser -S app -u 1001
USER app
# 3. Capabilities entfernen (in Compose)
# 4. Schreibgeschütztes 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 wenn Bindung an Ports < 1024 erforderlich
Geheimnisverwaltung
# GUT: Umgebungsvariablen verwenden (zur Laufzeit injiziert)
services:
app:
env_file:
- .env # .env niemals in Git committen
environment:
- API_KEY # Übernimmt aus Host-Umgebung
# GUT: Docker Secrets (Swarm-Modus)
secrets:
db_password:
file: ./secrets/db_password.txt
services:
db:
secrets:
- db_password
# SCHLECHT: Fest im Image codiert
# ENV API_KEY=sk-proj-xxxxx # DAS DARF NIEMALS PASSIEREN
Gehärtete CLI-Installations-Testumgebungen
Verwenden Sie Container, um das Installationsverhalten anhand von wegwerfbaren Projektkopien zu testen, ohne dass der Test den Quellcode-Checkout verändern kann.
Respektieren Sie die Plattformgrenze
- 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 gemeinsam nutzt. Führen Sie den gleichen shell-freien Test-Einstiegspunkt nativ unter 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, Befehlsshimps, Anführungszeichen und Dateisystemverhalten bei.
Behaupten Sie nicht, dass ein Linux-Container das Verhalten von macOS oder Windows validiert.
Durchsetzen des Isolationsvertrags
- Fixieren Sie Basis-Images mit unveränderlichem Digest und fixieren Sie die installierten CLI-Versionen.
- Führen Sie als nicht-root numerische UID/GID aus, wenn sich die Kontonamen der Distributionen unterscheiden.
- Mounten Sie das Repository und das Quellprojekt schreibgeschützt.
- Kopieren Sie das Quellprojekt vor jeder Änderung in einen beschreibbaren
tmpfs-Arbeitsbereich. - Mounten Sie
/workspacemitnoexec, UID/GID 1000 undmode=0700, sodass nur der Container-Benutzer die Projektdaten einsehen kann. - Halten Sie den ausführbaren Cache von npm und npx bei
NPM_CONFIG_CACHE=/tmp/npm-cacheauf dem ausführbaren/tmp-Mount. Seine Standardgröße beträgt 2 GiB und kann mitECC_TMPFS_SIZEangepasst werden;ECC_WORKSPACE_SIZEsteuert separat den privaten Arbeitsbereichsmount. - Setzen Sie
read_only: true,no-new-privileges:true,cap_drop: [ALL]und ein endlichespids_limit. - Belassen Sie die standardmäßigen echten CLI-Dienste auf
network_mode: none. Fügen Sie Netzwerkzugriff nur durch einen sichtbar benannten Opt-in-Dienst für eine authentifizierte Anbietersitzung hinzu; machen Sie es niemals zu einem versehentlichen umgebungsgesteuerten Standard. - Erstellen Sie nur die temporären beschreibbaren Pfade, die das Werkzeug benötigt.
- Übergeben Sie standardmäßig keine Host-Anmeldeinformationen in den Container.
- Standardmäßig einen Trockenlauf durchführen und nur die expliziten Modi
dry-run,install,pluginundshellauf die Whitelist setzen. - Verwenden Sie Argument-Arrays oder
spawnSync(..., { shell: false })für plattformübergreifende Runner. Interpolieren Sie niemals Projektpfade in einen Shell-Befehl.
Verwenden des ECC-Plugin-Setup-Harness
Verwenden Sie docker/plugin-setup/compose.yaml als Referenzimplementierung. Es bietet:
fixture-testsfür die fokussierte Installationsmanifest-, Ziel- und Testsuite.real-clifür das fixierte Debian-basierte generische Linux-Image.real-cli-ubuntufür das fixierte Ubuntu-Image.
Validieren Sie das Compose-Modell vor dem Bauen:
docker compose -f docker/plugin-setup/compose.yaml config --quiet
Bauen Sie beide echten Linux-Images:
docker compose -f docker/plugin-setup/compose.yaml \
build real-cli real-cli-ubuntu
Führen Sie den sicheren Standardablauf in jedem Image aus:
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,
die erforderlichen Installationsmanifeste und das eingeschränkte package.json-bin.ecc-Mapping,
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 exakten fixierten 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 den isolierten Apply zweimal aus, überprüft seinen verwalteten Installationsstatus,
listet das installierte Ziel auf und führt doctor aus.
Starten, Öffnen, Wiederverbinden und Bereinigen einer benannten Sitzung
Starten Sie einen Container im Hintergrund ohne --rm, damit das Verlassen eines 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 das schreibgeschützte Fixture in das stabile private Verzeichnis
/workspace/project. Bestätigen Sie, dass es ausgeführt wird, und geben Sie dann die Docker-Seite des
Terminal-Öffner v1-Datenvertrags 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 exakt 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 gleiche PTY im aktuellen Host-Terminal direkt:
docker exec -it -w /workspace/project ecc-plugin-shell bash
Verlassen Sie die Shell, ohne den Container im Hintergrund zu stoppen. Stellen Sie die Verbindung mit demselben
docker exec -it-Befehl wieder her. Wenn Sie fertig sind, entfernen Sie den genannten 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
gemountet. Der Standarddienst hat auch keinen Netzwerkzugriff. Wenn eine authentifizierte
Anbietersitzung wirklich ein Netzwerk benötigt, bauen Sie zuerst real-cli und melden Sie sich dann
sichtbar mit docker compose --profile networked run real-cli-networked shell an. Bevorzugen Sie die Authentifizierung innerhalb dieser wegwerfbaren Sitzung. Wenn ein CI-Lauf
Host-Umgebungsanmeldeinformationen erben muss, machen Sie dies zu einem Opt-in beim Aufruf mit einem expliziten
Compose---env NAME-Flag. Verstehen Sie, dass der Wert einsehbar ist und
für die Containerlebensdauer exfiltriert werden kann, und entfernen Sie den genannten 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/
Fehlerbehebung
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
docker compose top # Prozesse in jedem Container
docker stats # Ressourcennutzung
# Neu bauen
docker compose up --build # Images neu bauen
docker compose build --no-cache app # Kompletten Neubau erzwingen
# Aufräumen
docker compose down # Container stoppen und entfernen
docker compose down -v # Auch Volumes entfernen (ZERSTÖRERISCH)
docker system prune # Unbenutzte Images/Container entfernen
Fehlerbehebung bei Netzwerkproblemen
# DNS-Auflösung im Container überprü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 verwenden
FROM node:latest
# SCHLECHT: Als Root ausführen
# (Standardmäßig, kein USER-Befehl im Dockerfile)
# SCHLECHT: Ein großer, monolithischer Container mit allen Diensten
# (Verwenden Sie separate Container für jeden Dienst)
# SCHLECHT: Geheimnisse im Image codieren
ENV API_KEY=sk-proj-xxxxx
# SCHLECHT: Container ohne Healthchecks betreiben
# (Healthchecks in Compose oder Dockerfile definieren)
# SCHLECHT: Unbegrenzte Ressourcenlimits in der Produktion
# (Ressourcenbeschränkungen für cpu und memory definieren)
# SCHLECHT: `.env`-Dateien in git committen
# (Nur .env.example committen, .env in .gitignore aufnehmen)