Docker Compose: 5 Stacks, die ich täglich nutze
Fertige docker-compose.yml Dateien zum Kopieren. Webserver, Datenbanken, Monitoring – direkt einsatzbereit.
- Docker
- Docker Compose
- DevOps
- Tutorial
Warum Docker Compose?
Ich habe am Anfang wirklich alles mit einzelnen docker run Befehlen gestartet. Das geht für einen Container noch. Spätestens beim zweiten wird es unübersichtlich. Beim dritten vergisst du irgendeinen Port, ein Volume oder eine Umgebungsvariable.
Docker Compose löst genau dieses Problem. Du beschreibst dein Setup in einer Datei und startest den ganzen Stack reproduzierbar.
docker compose up -d
Danach laufen die Container, das Netzwerk ist da und die Volumes sind angelegt. Vor allem aber ist sichtbar, warum alles so läuft.
Was Docker Compose eigentlich besser macht
Der eigentliche Vorteil ist nicht der eine Befehl. Der eigentliche Vorteil ist: Das Setup steht lesbar in einer Datei.
Dort definierst du:
- welche Services zusammengehören
- welche Ports nach außen offen sind
- welche Daten persistent bleiben
- welche Umgebungsvariablen nötig sind
- welcher Container von welchem anderen abhängt
Wenn du dein Setup ein paar Wochen liegen lässt, ist genau diese Datei Gold wert. Ohne Compose musst du dir sonst docker run Historie, Shell-Notizen oder halb vergessene Befehle zusammensuchen.
Die wichtigsten Befehle
docker compose up -d # Starten
docker compose down # Stoppen
docker compose logs -f # Logs anschauen
docker compose ps # Status
Für den Anfang reicht das. Später kommen oft noch dazu:
docker compose pull # Images aktualisieren
docker compose down -v # Stoppen und Volumes löschen
docker compose exec web sh
Mit exec kommst du direkt in einen laufenden Container. Das braucht man beim Debuggen öfter, als man am Anfang denkt.
Meine Grundregel vor jedem Stack
Ich lege fast immer erst diese drei Dinge an:
projekt/
├── compose.yaml
├── .env
└── data/ oder config/
Warum:
compose.yamlbeschreibt den Stack.envhält Werte, die man ändern willdata/oder benannte Volumes halten persistente Daten
Passwörter und API-Keys schreibe ich nicht hart in die YAML, wenn ich es vermeiden kann. Für kleine Teststacks ist das oft egal. Sobald das Setup länger lebt, rächt es sich.
Stack 1: Nginx Webserver
Der einfachste Stack. Gut zum Testen von statischen Dateien, kleinen Landingpages oder als erstes Compose-Beispiel.
services:
web:
image: nginx:alpine
ports:
- '80:80'
volumes:
- ./html:/usr/share/nginx/html:ro
restart: unless-stopped
So nutzt du es:
- Ordner
htmlerstellen - HTML-Dateien reinlegen
docker compose up -d- Browser: http://localhost
Die Kleinigkeit, die ich dabei wichtig finde: :ro beim Volume. Das Mount ist read-only. Für statische Inhalte ist das sauberer, weil der Container nichts an deinen Dateien herumschreibt.
Stack 2: WordPress + MySQL
Das ist ein typischer Test- oder Migrationsstack. Ich habe ihn öfter genutzt, wenn ich schnell prüfen wollte, wie ein Theme oder Plugin in einer frischen Umgebung läuft.
services:
wordpress:
image: wordpress:latest
ports:
- '8080:80'
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: geheim123
WORDPRESS_DB_NAME: wordpress
volumes:
- wordpress_data:/var/www/html
depends_on:
- db
restart: unless-stopped
db:
image: mysql:8.0
environment:
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: geheim123
MYSQL_ROOT_PASSWORD: rootgeheim123
volumes:
- db_data:/var/lib/mysql
restart: unless-stopped
volumes:
wordpress_data:
db_data:
Wichtig:
- Passwörter ändern
- nichts davon ungeprüft in Produktion schieben
- Datenbank-Zugang nicht dauerhaft hart in der YAML lassen
Für ein saubereres Setup:
environment:
WORDPRESS_DB_PASSWORD: ${WORDPRESS_DB_PASSWORD}
und dazu eine .env:
WORDPRESS_DB_PASSWORD=bitte-aendern
Browser: http://localhost:8080
Für echte Produktion würde ich zusätzlich einen Reverse Proxy, Backups und Update-Routinen davor setzen. Als lokaler Teststack ist das hier aber völlig okay.
Stack 3: PostgreSQL + pgAdmin
Das ist mein Standard, wenn ich lokal mit PostgreSQL arbeite und nicht alles nur in der Shell machen will.
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: geheim123
POSTGRES_DB: development
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- '5432:5432'
restart: unless-stopped
pgadmin:
image: dpage/pgadmin4:latest
environment:
PGADMIN_DEFAULT_EMAIL: admin@example.com
PGADMIN_DEFAULT_PASSWORD: admin123
ports:
- '5050:80'
depends_on:
- postgres
restart: unless-stopped
volumes:
postgres_data:
pgAdmin: http://localhost:5050
Login mit admin@example.com / admin123. Dann Server hinzufügen mit Host postgres.
Der klassische Fehler
Ich habe am Anfang in pgAdmin als Host localhost eingetragen. Das funktioniert in diesem Setup nicht.
In Compose reden Container im gemeinsamen Netzwerk über den Service-Namen miteinander. Also hier:
postgres
nicht localhost.
Das ist einer dieser Fehler, den man einmal macht und dann hoffentlich nie wieder.
Stack 4: Traefik Reverse Proxy
Wenn mehrere Apps hinter einer Domain oder mehreren Subdomains laufen sollen, wird es ohne Reverse Proxy schnell hässlich. Dann ist Traefik praktisch.
services:
traefik:
image: traefik:v3.0
command:
- '--api.insecure=true'
- '--providers.docker=true'
- '--providers.docker.exposedbydefault=false'
- '--entrypoints.web.address=:80'
- '--entrypoints.websecure.address=:443'
- '--certificatesresolvers.letsencrypt.acme.httpchallenge=true'
- '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web'
- '--certificatesresolvers.letsencrypt.acme.email=dein@email.de'
- '--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json'
ports:
- '80:80'
- '443:443'
- '8080:8080'
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- letsencrypt:/letsencrypt
restart: unless-stopped
volumes:
letsencrypt:
Dashboard: http://localhost:8080
Andere Container fügst du mit Labels hinzu:
services:
app:
image: deine-app
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.app.rule=Host(`app.deinedomain.de`)'
Traefik erkennt den Container dann automatisch.
Ich mag daran, dass Routing nah am Service beschrieben ist. Du siehst direkt im Compose-File, unter welcher Domain der Container später erreichbar sein soll.
Wichtig dabei:
- Docker-Socket nur read-only mounten
- Dashboard nicht offen im Internet hängen lassen
- Zertifikatsdateien persistent speichern
Für Homelab und kleine VPS-Setups ist Traefik oft der Punkt, an dem Compose von “lokaler Spielerei” zu “brauchbarem Betriebs-Setup” wird.
Stack 5: Prometheus + Grafana
Monitoring gehört für mich irgendwann dazu, auch wenn das Setup klein ist. Gerade im Homelab willst du irgendwann sehen, ob ein Container wirklich kaputt ist oder ob nur der Host keine Luft mehr hat.
services:
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
ports:
- '9090:9090'
restart: unless-stopped
grafana:
image: grafana/grafana:latest
environment:
GF_SECURITY_ADMIN_PASSWORD: admin123
volumes:
- grafana_data:/var/lib/grafana
ports:
- '3000:3000'
depends_on:
- prometheus
restart: unless-stopped
node-exporter:
image: prom/node-exporter:latest
ports:
- '9100:9100'
restart: unless-stopped
volumes:
prometheus_data:
grafana_data:
Du brauchst noch eine prometheus.yml:
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'node'
static_configs:
- targets: ['node-exporter:9100']
Grafana: http://localhost:3000 (admin/admin123)
Dann Prometheus als Datenquelle hinzufügen: URL = http://prometheus:9090
Tipps aus der Praxis
Secrets in .env Datei
Statt Passwörter direkt in die YAML zu schreiben:
environment:
MYSQL_PASSWORD: ${DB_PASSWORD}
.env Datei:
DB_PASSWORD=sicheres-passwort
Die .env in .gitignore aufnehmen.
Health Checks
depends_on startet Container in einer Reihenfolge. Es bedeutet aber nicht, dass ein Dienst wirklich fertig hochgefahren ist.
Genau deshalb sind Health Checks wichtig:
services:
db:
image: postgres:16
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 10s
timeout: 5s
retries: 5
Wenn ein Service erst erreichbar sein muss, nachdem die Datenbank wirklich da ist, hilft dir das mehr als ein blindes sleep 10.
Logs debuggen
# Alle Logs
docker compose logs
# Nur ein Service, live
docker compose logs -f wordpress
# Letzte 100 Zeilen
docker compose logs --tail=100
Das ist oft der schnellste Weg zum eigentlichen Problem. Bevor ich in Config-Dateien herumrate, schaue ich fast immer erst in die Logs.
Drei Fehler, die ich immer wieder sehe
1. localhost im falschen Kontext
Innerhalb eines Containers bedeutet localhost immer der Container selbst, nicht dein Host und nicht ein anderer Service.
Wenn ein Container zu PostgreSQL verbinden soll, ist der Host in Compose meistens:
postgres
nicht localhost.
2. Volumes vergessen
Ohne persistente Volumes sind Daten schnell weg. Das ist bei Datenbanken offensichtlich, aber auch bei Tools wie Grafana oder WordPress wichtig.
Wenn du nach einem docker compose down plötzlich alles neu einrichten musst, fehlt meistens genau dieser Teil.
3. Zu früh zu groß denken
Für ein bis fünf Services ist Compose oft genau richtig. Viele bauen aber sofort Kubernetes-Fantasien, obwohl sie erstmal nur:
- App
- Datenbank
- Reverse Proxy
- Monitoring
sauber brauchen.
Compose ist kein Notbehelf. Für kleine und mittlere Setups ist es oft der vernünftigste Anfang.
Wann Compose reicht und wann nicht mehr
Compose reicht aus meiner Sicht sehr weit, wenn:
- das Setup überschaubar bleibt
- du wenige Hosts hast
- Deployment und Betrieb noch gut nachvollziehbar sind
Ich fange fast immer damit an. Erst wenn Dinge wie Scheduling über viele Nodes, komplexe Autoskalierung oder sehr große Teams dazukommen, schaue ich mir schwerere Orchestrierung an.
Für FISI-Lernen, Homelab, interne Tools und viele kleine Produktionssysteme ist Compose nicht die Vorstufe. Es ist oft schon die passende Lösung.
Nächste Schritte
- Traefik vor WordPress oder vor deine eigene App setzen
- Secrets und Ports ausmisten
- Compose-Datei ins Git packen
- Logs und Health Checks bewusst nachziehen
Wenn du Docker erst lernst, ist genau das der Punkt: nicht zehn Stacks kopieren, sondern einen davon wirklich verstehen.
Weiterlesen: Docker auf Ubuntu installieren