Docker Compose - Lokale Entwicklungsumgebung von Grund auf
Docker Compose Lokale
devopsDocker Compose - Lokale Entwicklungsumgebung von Grund auf
Die Einrichtung einer lokalen Entwicklungsumgebung ist eine der ersten Herausforderungen, denen sich jedes Entwicklungsteam stellt. Ein neues Teammitglied verbringt oft den gesamten ersten Tag mit der Installation von Datenbanken, der Konfiguration von Umgebungsvariablen und der Behebung von Versionskonflikten. Docker Compose beseitigt diese Probleme und ermoglicht es, eine vollstandige Entwicklungsumgebung mit einem einzigen Befehl zu starten: docker compose up.
In diesem Artikel zeigen wir Schritt fur Schritt, wie Sie eine professionelle lokale Entwicklungsumgebung aufbauen, die ein Frontend (Next.js), Backend (.NET API), eine Datenbank (PostgreSQL), einen Cache (Redis) und einen E-Mail-Server (Mailhog) umfasst. Wir behandeln die Syntax der docker-compose.yml-Datei, Volumes, Umgebungsvariablen, Netzwerke, Health Checks, Hot Reload, Datenbankmigrationen und Debugging in Containern.
Was ist Docker Compose?#
Docker Compose ist ein Werkzeug zur Definition und Ausfuhrung von Multi-Container-Docker-Anwendungen. Anstatt jeden Container manuell mit einem separaten docker run-Befehl zu starten, definieren Sie alle Dienste in einer einzigen YAML-Datei und verwalten sie mit einfachen Befehlen.
Wesentliche Vorteile von Docker Compose:
- Eine einzige Konfigurationsdatei - die gesamte Infrastruktur in
docker-compose.ymlbeschrieben - Reproduzierbarkeit - identische Umgebung auf jedem Entwicklerrechner
- Isolation - keine Konflikte zwischen Projekten; jedes hat eigene Container
- Einfaches Onboarding - ein neuer Entwickler fuhrt
docker compose upaus und kann loslegen - Versionierung - die Compose-Datei ist Teil des Repositorys, Anderungen werden in Git verfolgt
Syntax der docker-compose.yml#
Die Datei docker-compose.yml ist das Herzstuck von Docker Compose. Hier ist die grundlegende Struktur:
# Spezifikationsversion (optional seit Compose v2)
version: "3.9"
services:
# Dienst- (Container-) Definitionen
dienstname:
image: image:tag # Fertiges Image von Docker Hub
build: ./pfad # Oder Build aus Dockerfile
ports:
- "host:container" # Port-Mapping
volumes:
- ./lokal:/container # Volume-Mapping
environment: # Umgebungsvariablen
- SCHLUESSEL=wert
env_file:
- .env # Datei mit Variablen
depends_on: # Abhangigkeiten zwischen Diensten
- anderer-dienst
networks: # Netzwerke
- mein-netzwerk
restart: unless-stopped # Neustart-Richtlinie
volumes:
# Benannte Volumes (persistente Daten)
postgres-data:
networks:
# Benutzerdefinierte Netzwerke
mein-netzwerk:
driver: bridge
Jeder Eintrag unter services definiert einen einzelnen Container. Der Abschnitt volumes deklariert benannte Volumes fur die persistente Datenspeicherung, und networks definiert Netzwerke, die es den Containern ermoglichen, miteinander zu kommunizieren.
Komplette Umgebung: Next.js + .NET API + PostgreSQL + Redis + Mailhog#
Kommen wir zur Praxis. Nachfolgend finden Sie die vollstandige Konfiguration einer Entwicklungsumgebung fur eine typische Enterprise-Anwendung. Die Projektstruktur sieht folgendermassen aus:
projekt/
├── docker-compose.yml
├── docker-compose.override.yml
├── docker-compose.prod.yml
├── .env
├── .env.example
├── frontend/
│ ├── Dockerfile
│ ├── Dockerfile.dev
│ ├── package.json
│ └── src/
├── backend/
│ ├── Dockerfile
│ ├── Dockerfile.dev
│ ├── MyApi.sln
│ └── src/
└── database/
├── init.sql
└── migrations/
Haupt-docker-compose.yml#
version: "3.9"
services:
# ─── Frontend (Next.js) ────────────────────────────────
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- ./frontend/src:/app/src
- ./frontend/public:/app/public
- /app/node_modules
environment:
- NODE_ENV=development
- NEXT_PUBLIC_API_URL=http://localhost:5000/api
- WATCHPACK_POLLING=true
depends_on:
backend:
condition: service_healthy
networks:
- app-network
restart: unless-stopped
# ─── Backend (.NET API) ────────────────────────────────
backend:
build:
context: ./backend
dockerfile: Dockerfile.dev
ports:
- "5000:5000"
- "5001:5001"
volumes:
- ./backend/src:/app/src
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://+:5000
- ConnectionStrings__DefaultConnection=Host=postgres;Port=5432;Database=myapp_dev;Username=devuser;Password=devpass123
- Redis__ConnectionString=redis:6379
- Email__SmtpHost=mailhog
- Email__SmtpPort=1025
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
networks:
- app-network
restart: unless-stopped
# ─── PostgreSQL ────────────────────────────────────────
postgres:
image: postgres:16-alpine
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
- ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
environment:
- POSTGRES_DB=myapp_dev
- POSTGRES_USER=devuser
- POSTGRES_PASSWORD=devpass123
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
healthcheck:
test: ["CMD-SHELL", "pg_isready -U devuser -d myapp_dev"]
interval: 5s
timeout: 5s
retries: 5
networks:
- app-network
restart: unless-stopped
# ─── Redis ─────────────────────────────────────────────
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
networks:
- app-network
restart: unless-stopped
# ─── Mailhog (Test-E-Mail-Server) ─────────────────────
mailhog:
image: mailhog/mailhog:latest
ports:
- "1025:1025"
- "8025:8025"
networks:
- app-network
restart: unless-stopped
volumes:
postgres-data:
driver: local
redis-data:
driver: local
networks:
app-network:
driver: bridge
Diese Konfiguration startet funf Dienste, die zusammen eine vollstandige Entwicklungsumgebung bilden. Das Next.js-Frontend lauscht auf Port 3000, das .NET-Backend auf Port 5000, PostgreSQL auf dem Standardport 5432, Redis auf 6379 und das Mailhog-Panel ist unter http://localhost:8025 erreichbar.
Volumes - Datenpersistenz#
Volumes in Docker Compose erfullen zwei entscheidende Aufgaben: Sie gewahrleisten die Datenpersistenz uber Container-Neustarts hinweg und ermoglichen die Dateisynchronisation zwischen Host und Container (unverzichtbar fur Hot Reload).
Benannte Volumes (persistente Daten)#
volumes:
postgres-data:
driver: local
Benannte Volumes (z.B. postgres-data) speichern Datenbankdaten unabhangig vom Container-Lebenszyklus. Das Entfernen des PostgreSQL-Containers mit docker compose down loscht die Daten nicht. Um die Daten zu bereinigen, muss man explizit docker compose down -v verwenden.
Bind Mounts (Dateisynchronisation)#
volumes:
- ./frontend/src:/app/src # Quellcode
- ./frontend/public:/app/public # Statische Dateien
- /app/node_modules # Anonymes Volume - nicht uberschreiben!
Bind Mounts binden Verzeichnisse vom Host direkt in den Container ein. Eine Dateiranderung auf dem Host ist sofort im Container sichtbar, was die Grundlage fur Hot Reload bildet. Es ist wichtig, ein anonymes Volume fur node_modules zu verwenden - ohne dieses wurde das node_modules-Verzeichnis vom Host das im Container uberschreiben, was zu Problemen mit fur eine andere Plattform kompilierten Abhangigkeiten fuhren wurde.
Umgebungsvariablen#
Docker Compose unterstutzt Umgebungsvariablen auf verschiedene Weisen. Die beste Praxis ist die Speicherung von Werten in einer .env-Datei:
.env-Datei#
# .env (NICHT in Git committen!)
POSTGRES_DB=myapp_dev
POSTGRES_USER=devuser
POSTGRES_PASSWORD=devpass123
REDIS_PASSWORD=
NODE_ENV=development
API_PORT=5000
FRONTEND_PORT=3000
.env.example-Datei#
# .env.example (als Vorlage in Git committen)
POSTGRES_DB=myapp_dev
POSTGRES_USER=devuser
POSTGRES_PASSWORD=CHANGE_ME
REDIS_PASSWORD=
NODE_ENV=development
API_PORT=5000
FRONTEND_PORT=3000
Verwendung in docker-compose.yml#
services:
postgres:
image: postgres:16-alpine
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
ports:
- "${POSTGRES_PORT:-5432}:5432"
Die Syntax ${POSTGRES_PORT:-5432} setzt einen Standardwert von 5432, wenn die Variable nicht definiert ist. Die .env-Datei sollte in .gitignore aufgenommen werden, und .env.example sollte als Vorlage fur neue Entwickler ins Repository committet werden.
Networking - Kommunikation zwischen Containern#
Docker Compose erstellt automatisch ein Netzwerk fur jedes Projekt. Container innerhalb desselben Netzwerks konnen uber Dienstnamen als DNS-Hostnamen miteinander kommunizieren.
networks:
app-network:
driver: bridge
In unserer Konfiguration verbindet sich das Backend mit PostgreSQL uber den Host postgres (den Dienstnamen):
Host=postgres;Port=5432;Database=myapp_dev
Und das Frontend kommuniziert mit dem Backend uber backend:5000 innerhalb des Docker-Netzwerks. Vom Browser aus (clientseitig) erfolgt die Kommunikation jedoch uber localhost:5000, da der Browser ausserhalb des Docker-Netzwerks arbeitet.
Netzwerkisolation zwischen Projekten#
Jedes Docker-Compose-Projekt hat sein eigenes isoliertes Netzwerk. Container aus Projekt A konnen Container aus Projekt B nicht sehen, selbst wenn sie dieselben internen Ports verwenden. Konflikte konnen nur auf der Ebene der Host-Ports auftreten - deshalb ist es empfehlenswert, Ports mit Umgebungsvariablen zu parametrisieren.
Health Checks - Kontrolle der Dienstbereitschaft#
Health Checks sind ein Mechanismus zur Uberprufung, ob ein Dienst vollstandig bereit ist, Anfragen zu bearbeiten. Ohne sie garantiert depends_on lediglich, dass ein Container gestartet wurde, nicht dass der Dienst bereit ist.
postgres:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U devuser -d myapp_dev"]
interval: 5s # Wie oft prufen
timeout: 5s # Maximale Wartezeit auf Antwort
retries: 5 # Wie viele Versuche vor der Markierung als unhealthy
start_period: 10s # Startzeit (Healthcheck markiert nicht als unhealthy)
backend:
depends_on:
postgres:
condition: service_healthy # Warten bis PostgreSQL healthy ist
redis:
condition: service_healthy # Warten bis Redis healthy ist
Ohne Health Checks konnte das Backend versuchen, sich mit der Datenbank zu verbinden, bevor diese vollstandig gestartet ist. Die Direktive condition: service_healthy in depends_on stellt die korrekte Startreihenfolge sicher.
Compose-Dateien: Entwicklung vs. Produktion#
In der Praxis benotigen Sie unterschiedliche Konfigurationen fur Entwicklungs- und Produktionsumgebungen. Docker Compose unterstutzt die Uberlagerung von Dateien (Override), was eine elegante Trennung der Konfigurationen ermoglicht.
docker-compose.yml (Basiskonfiguration)#
Enthalt gemeinsame Dienstdefinitionen, Netzwerke und Volumes - alles, was in beiden Umgebungen identisch ist.
docker-compose.override.yml (Entwicklung)#
# docker-compose.override.yml
# Wird automatisch zusammen mit docker-compose.yml geladen
version: "3.9"
services:
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.dev
volumes:
- ./frontend/src:/app/src
- ./frontend/public:/app/public
- /app/node_modules
environment:
- NODE_ENV=development
- WATCHPACK_POLLING=true
backend:
build:
context: ./backend
dockerfile: Dockerfile.dev
volumes:
- ./backend/src:/app/src
environment:
- ASPNETCORE_ENVIRONMENT=Development
ports:
- "5000:5000"
- "5001:5001" # Debugger-Port
postgres:
ports:
- "5432:5432" # Host-Zugriff (z.B. pgAdmin, DBeaver)
redis:
ports:
- "6379:6379" # Host-Zugriff (z.B. RedisInsight)
docker-compose.prod.yml (Produktion)#
# docker-compose.prod.yml
version: "3.9"
services:
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
args:
- NEXT_PUBLIC_API_URL=https://api.meinedomain.de
environment:
- NODE_ENV=production
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
backend:
build:
context: ./backend
dockerfile: Dockerfile
environment:
- ASPNETCORE_ENVIRONMENT=Production
deploy:
resources:
limits:
cpus: "2.0"
memory: 1G
postgres:
environment:
- POSTGRES_PASSWORD=${PROD_DB_PASSWORD}
# Kein Port-Mapping zum Host - Zugriff nur aus dem Docker-Netzwerk
ports: []
redis:
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD} --maxmemory 512mb
ports: []
Ausfuhrung der Umgebungen:
# Entwicklung (ladt standardmassig docker-compose.yml + docker-compose.override.yml)
docker compose up
# Produktion (explizite Angabe der Dateien)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
In der Produktionsumgebung werden Datenbankports nicht zum Host exponiert, Images werden aus Produktions-Dockerfiles gebaut (mehrstufige Builds, Grossenoptimierung) und Container-Ressourcen werden limitiert.
Hot Reload - Automatisches Neuladen des Codes#
Hot Reload ist eine entscheidende Funktion fur die Produktivitat von Entwicklern. Jede Codeanderung sollte sofort im Browser oder in den Serverlogs sichtbar sein.
Hot Reload fur Next.js#
# frontend/Dockerfile.dev
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Next.js unterstutzt Hot Reload standardmassig
# WATCHPACK_POLLING wird unter Windows/WSL2 benotigt
ENV WATCHPACK_POLLING=true
EXPOSE 3000
CMD ["npm", "run", "dev"]
Die Schlusselelemente sind das Volume, das den Quellcode einbindet (./frontend/src:/app/src), und die Variable WATCHPACK_POLLING=true, die auf Windows-Systemen mit WSL2 unverzichtbar ist, wo Standard-Dateiranderungsbenachrichtigungen (inotify) moglicherweise nicht korrekt funktionieren.
Hot Reload fur .NET#
# backend/Dockerfile.dev
FROM mcr.microsoft.com/dotnet/sdk:8.0
WORKDIR /app
COPY *.sln .
COPY src/**/*.csproj ./src/
RUN dotnet restore
COPY . .
EXPOSE 5000 5001
CMD ["dotnet", "watch", "run", "--project", "src/MyApi", "--urls", "http://+:5000"]
Der Befehl dotnet watch run kompiliert die Anwendung automatisch neu und startet sie bei jeder Anderung einer .cs-Datei. Das Volume-Mounting ./backend/src:/app/src stellt die Dateisynchronisation sicher.
Datenbankmigrationen in Containern#
Datenbankmigrationen sollten automatisch beim Containerstart oder als separater Dienst ausgefuhrt werden.
Ansatz 1: PostgreSQL-Initialisierungsskript#
-- database/init.sql
-- Wird automatisch beim ersten Start des PostgreSQL-Containers ausgefuhrt
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- Initiale Tabellen
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS audit_log (
id BIGSERIAL PRIMARY KEY,
user_id UUID REFERENCES users(id),
action VARCHAR(100) NOT NULL,
details JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
Ansatz 2: Separater Migrationsdienst#
services:
migrations:
build:
context: ./backend
dockerfile: Dockerfile.dev
command: ["dotnet", "ef", "database", "update", "--project", "src/MyApi"]
environment:
- ConnectionStrings__DefaultConnection=Host=postgres;Port=5432;Database=myapp_dev;Username=devuser;Password=devpass123
depends_on:
postgres:
condition: service_healthy
networks:
- app-network
restart: "no" # Einmal ausfuhren und beenden
Ein separater Migrationsdienst startet, fuhrt die Migrationen aus und beendet sich. Mit restart: "no" wird der Container nach Abschluss nicht neu gestartet.
Ansatz 3: Entrypoint mit Migrationen#
# backend/entrypoint.sh
#!/bin/bash
set -e
echo "Migrationen werden ausgefuhrt..."
dotnet ef database update --project src/MyApi
echo "Anwendung wird gestartet..."
exec dotnet watch run --project src/MyApi --urls "http://+:5000"
backend:
entrypoint: ["/bin/bash", "./entrypoint.sh"]
Dieser Ansatz fuhrt Migrationen automatisch vor dem Start der Anwendung aus. Er ist bequem, verlangert aber die Container-Startzeit.
Debugging in Containern#
Das Debuggen von Anwendungen in Containern erfordert eine entsprechende Konfiguration, ist aber nicht schwierig.
Debugging von .NET im Container (Visual Studio Code)#
Fugen Sie die Debugger-Konfiguration in .vscode/launch.json hinzu:
{
"version": "0.2.0",
"configurations": [
{
"name": "Docker .NET Attach",
"type": "docker",
"request": "attach",
"platform": "netCore",
"sourceFileMap": {
"/app/src": "${workspaceFolder}/backend/src"
}
}
]
}
Debugging von Next.js im Container#
Fugen Sie das Debug-Flag zum Startbefehl hinzu:
frontend:
command: ["node", "--inspect=0.0.0.0:9229", "node_modules/.bin/next", "dev"]
ports:
- "3000:3000"
- "9229:9229" # Node.js-Debugger-Port
VS Code-Konfiguration:
{
"version": "0.2.0",
"configurations": [
{
"name": "Docker Next.js Attach",
"type": "node",
"request": "attach",
"port": 9229,
"remoteRoot": "/app",
"localRoot": "${workspaceFolder}/frontend"
}
]
}
Logs und Fehlerbehebung#
# Logs fur einen bestimmten Dienst
docker compose logs backend
# Echtzeit-Logs (follow)
docker compose logs -f backend postgres
# Letzte 100 Zeilen der Logs
docker compose logs --tail=100 backend
# In den Container eintreten (interaktive Shell)
docker compose exec backend bash
# Containerstatus prufen
docker compose ps
# Health Checks prufen
docker inspect --format='{{.State.Health.Status}}' projekt-postgres-1
Nutzliche Docker-Compose-Befehle#
Hier ist eine Liste der am haufigsten verwendeten Befehle, die Ihren taglichen Arbeitsablauf beschleunigen:
# ─── Lifecycle-Management ───────────────────────────────
docker compose up # Alles starten
docker compose up -d # Im Hintergrund starten (detached)
docker compose up --build # Images neu bauen und starten
docker compose up backend postgres # Nur ausgewahlte Dienste starten
docker compose down # Container stoppen und entfernen
docker compose down -v # Container + Volumes stoppen und entfernen
docker compose stop # Container stoppen (ohne Entfernung)
docker compose start # Gestoppte Container starten
docker compose restart backend # Einen bestimmten Dienst neustarten
# ─── Bauen ──────────────────────────────────────────────
docker compose build # Alle Images bauen
docker compose build --no-cache # Ohne Cache bauen
docker compose build backend # Ein bestimmtes Image bauen
# ─── Diagnose ───────────────────────────────────────────
docker compose ps # Containerstatus
docker compose logs -f # Logs aller Dienste (follow)
docker compose logs -f backend # Logs eines bestimmten Dienstes
docker compose top # Prozesse in Containern
# ─── Container-Interaktion ──────────────────────────────
docker compose exec postgres psql -U devuser -d myapp_dev # PostgreSQL-Konsole
docker compose exec redis redis-cli # Redis-Konsole
docker compose exec backend dotnet ef migrations add Init # Neue Migration
# ─── Bereinigung ────────────────────────────────────────
docker compose down -v --rmi all # Alles entfernen (Container, Volumes, Images)
docker system prune -a # Ungenutzte Docker-Ressourcen bereinigen
docker volume prune # Ungenutzte Volumes bereinigen
Haufige Probleme und Losungen#
Port bereits belegt#
# Prufen, was den Port belegt
# Linux/Mac:
lsof -i :5432
# Windows:
netstat -ano | findstr :5432
# Port in .env andern
POSTGRES_PORT=5433
Langsame Volumes unter Windows/WSL2#
Unter Windows mit WSL2 konnen Bind Mounts sehr langsam sein. Losungen:
# 1. Delegated/cached verwenden (Docker Desktop)
volumes:
- ./frontend/src:/app/src:delegated
# 2. Synchronisierte Verzeichnisse einschranken
volumes:
- ./frontend/src:/app/src # Nur Quellcode
- /app/node_modules # node_modules im Container
- /app/.next # Build-Cache im Container
Container startet standig neu#
# Logs prufen
docker compose logs backend
# Health Check prufen
docker inspect projekt-backend-1 | jq '.[0].State.Health'
# In den Container eintreten und manuell debuggen
docker compose run --entrypoint bash backend
Zusammenfassung#
Docker Compose ist ein unverzichtbares Werkzeug im Arsenal jedes Entwicklungsteams. Es ermoglicht:
- Standardisierung der Umgebungen - Schluss mit "bei mir funktioniert es"
- Schnelles Onboarding - ein neuer Entwickler beginnt in Minuten statt Stunden
- Projektisolation - mehrere Projekte auf einem Rechner ohne Konflikte
- Automatisierung - Migrationen, Seed-Daten und Health Checks funktionieren automatisch
- Produktivitat - Hot Reload, Debugging und Logs an einem Ort
Eine gut konfigurierte Docker-Compose-Umgebung ist eine Investition, die sich mit jedem neuen Teammitglied und jeder Infrastrukturanderung auszahlt. Beginnen Sie mit einer einfachen Konfiguration und erweitern Sie sie schrittweise - Docker Compose unterstutzt einen iterativen Ansatz beim Aufbau der Infrastruktur.
Benotigen Sie eine professionelle Entwicklungsumgebung?#
Bei MDS Software Solutions Group helfen wir Teams, Docker Compose in ihren taglichen Arbeitsablauf zu integrieren. Wir bieten:
- Einrichtung lokaler Entwicklungsumgebungen
- Optimierung bestehender Compose-Dateien
- CI/CD-Implementierung mit Docker-Containern
- Schulungen fur Entwicklungsteams
- Migration von Projekten zur Container-Architektur
Kontaktieren Sie uns, um die Produktivitat Ihres Teams zu steigern!
Team von Programmierexperten, die sich auf moderne Webtechnologien spezialisiert haben.