Praxisnahe Anleitung für saubere Docker-Compose-Setups auf Ubuntu mit Ordnerstruktur, Variablen, Persistenz, Healthchecks, Updates und Backups.
Docker Compose ist für viele Self-Hosted-Setups auf Ubuntu die pragmatischste Art, mehrere Container gemeinsam zu betreiben. Genau deshalb lohnt es sich, das nicht einfach nur irgendwie zum Laufen zu bringen, sondern von Anfang an sauber aufzubauen.
In dieser Anleitung schauen wir uns die wichtigsten Best Practices für Docker Compose auf Ubuntu an: sinnvolle Ordnerstrukturen, Umgebungsvariablen, Persistenz, Netzwerke, Updates, Healthchecks, Logs, Backups und sichere Freigaben nach aussen. Ziel ist nicht nur ein funktionierendes Setup, sondern eines, das du auch in drei oder sechs Monaten noch sauber warten kannst.
1. Warum saubere Compose-Strukturen so wichtig sind
Viele Docker-Probleme entstehen nicht durch Docker selbst, sondern durch unklare Ordner, unübersichtliche Compose-Dateien, wilde Port-Freigaben und fehlende Dokumentation. Das funktioniert oft kurzfristig, wird aber später mühsam.
Ein gutes Compose-Setup hilft dir dabei:
- Dienste schneller zu verstehen
- Backups gezielt zu machen
- Updates kontrolliert einzuspielen
- Fehler schneller zu finden
- Setups an Kunden oder Mitarbeitende sauber zu übergeben
Compose ist also nicht nur eine Startmethode für Container, sondern oft die eigentliche Betriebsgrundlage deiner Self-Hosted-Dienste.
2. Eine klare Ordnerstruktur verwenden
Lege für jeden Stack ein eigenes Verzeichnis an. Ich würde Dienste nicht alle in ein einziges Grossprojekt kippen, sondern sauber trennen.
Ein gutes Beispiel sieht so aus:
~/stacks/
├── authentik/
│ ├── compose.yaml
│ ├── .env
│ ├── data/
│ └── backups/
├── paperless/
│ ├── compose.yaml
│ ├── .env
│ ├── data/
│ └── backups/
└── uptime-kuma/
├── compose.yaml
├── .env
└── data/
So ist auf einen Blick klar, welcher Dienst welche Dateien, Volumes oder Konfigurationen braucht. Gerade bei Backups und Troubleshooting spart das enorm Zeit.
3. Moderne Compose-Datei sinnvoll aufbauen
Eine Compose-Datei sollte lesbar bleiben. Trenne also Services, Volumes und Netzwerke logisch und gib Containern sprechende Namen nur dann, wenn du einen guten Grund dafür hast. Oft ist es besser, den Standard von Compose zu nutzen.
Ein sauberes Grundmuster könnte so aussehen:
services:
app:
image: ghcr.io/example/app:latest
restart: unless-stopped
env_file:
- .env
volumes:
- ./data/app:/app/data
networks:
- internal
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- ./data/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 30s
timeout: 5s
retries: 5
networks:
- internal
networks:
internal:
Wichtig dabei:
restart: unless-stoppedist für viele produktive Setups sinnvollenv_fileoder Variablen aus.envhalten Konfigurationswerte aus der Compose-Datei herausdepends_onersetzt keine echte Bereitschaftsprüfung, kann aber mitservice_healthyhilfreich sein- eigene Netzwerke schaffen Trennung zwischen internem Verkehr und öffentlich freigegebenen Diensten
4. Geheimnisse und Variablen nie hart codieren
Passwörter, Tokens, API-Keys und Secrets gehören nicht direkt in die Compose-Datei. Nutze mindestens eine .env-Datei, besser noch zusätzlich eine klare Dokumentation, welche Variablen ein Stack braucht.
Ein Beispiel:
APP_URL=https://app.example.com DB_PASSWORD=ein-langes-zufaelliges-passwort SMTP_HOST=smtp.example.com SMTP_PORT=587 SMTP_USERNAME=mailer@example.com SMTP_PASSWORD=noch-ein-geheimes-passwort
Wichtig ist dabei:
.envnie in Git einchecken- Secrets nicht in Screenshots oder Tickets posten
- bei Kundenprojekten festhalten, wo produktive Zugangsdaten sicher abgelegt sind
Wenn du mehrere Personen im Betrieb hast, lohnt sich zusätzlich ein Passwort-Manager oder eine zentrale Secret-Ablage.
5. Daten bewusst persistieren
Container selbst sind ersetzbar. Wichtig sind die Daten. Genau deshalb solltest du sehr bewusst entscheiden, welche Pfade persistent gespeichert werden.
Die Grundfrage lautet immer: Was muss nach einem Neustart oder Container-Tausch erhalten bleiben?
Typische persistente Daten sind:
- Datenbanken
- Uploads und Medien
- Anwendungskonfigurationen
- Zertifikate
- Queues, Indizes oder Arbeitsdaten je nach Anwendung
Ich empfehle, wenn möglich bind mounts mit klaren Projektpfaden zu verwenden, zum Beispiel ./data/postgres statt blind alles in anonyme Volumes zu legen. Das macht Backups, Sichtbarkeit und Wiederherstellung meist einfacher.
Wenn du Volumes bevorzugst, dokumentiere sauber, welche Volumes zu welchem Dienst gehören. Sonst wird ein Restore unnötig mühsam.
6. Nicht jeden Port nach aussen öffnen
Einer der häufigsten Fehler in Compose-Dateien ist, dass interne Dienste unnötig öffentlich freigegeben werden. Nur weil ein Container auf Port 3000, 5432 oder 8080 läuft, heisst das nicht, dass dieser Port auf dem Host veröffentlicht werden muss.
Faustregel:
- Datenbanken wie PostgreSQL oder Redis nicht öffentlich publishen, wenn es nicht wirklich nötig ist
- Admin-Oberflächen wenn möglich nur intern, per VPN oder hinter zusätzlichem Schutz bereitstellen
- öffentliche Webdienste lieber über einen Reverse Proxy veröffentlichen
Statt:
ports: - "5432:5432"
oft besser gar keine Port-Freigabe, wenn nur andere Container auf die Datenbank zugreifen sollen.
Für Webdienste ist häufig dieses Muster sauberer:
- App läuft intern im Compose-Netzwerk
- Reverse Proxy greift intern auf den Dienst zu
- nur Proxy-Ports wie 80 und 443 sind am Host offen
7. Healthchecks sinnvoll einsetzen
Ein Container kann laufen und trotzdem nicht wirklich bereit sein. Genau hier helfen Healthchecks. Sie sind besonders nützlich für Datenbanken, Proxys und Anwendungen, die erst nach einer Startphase korrekt reagieren.
Ein gutes Beispiel ist PostgreSQL:
healthcheck: test: ["CMD-SHELL", "pg_isready -U app -d app"] interval: 30s timeout: 5s retries: 5
Oder bei einem Webdienst:
healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"] interval: 30s timeout: 5s retries: 5
Das verbessert nicht nur die Übersicht, sondern hilft auch bei kontrollierten Abhängigkeiten und beim Monitoring.
8. Logs bewusst behandeln
Docker-Logs sind praktisch, aber nicht unbegrenzt. Ohne Begrenzung können sie unnötig wachsen und Speicher fressen. Gerade auf kleineren Ubuntu-Servern ist das schnell ein Problem.
Eine einfache Rotation direkt in Compose sieht so aus:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Das ist kein vollwertiges Log-Management, aber für viele kleinere Setups ein sehr guter Anfang. Wenn du später zentrale Logs brauchst, kannst du immer noch Graylog, Loki oder ein anderes System ergänzen.
9. Updates kontrolliert einspielen
Ein Compose-Stack sollte nicht nach dem Motto laufen: schnell pullen und hoffen. Besser ist ein klarer Ablauf.
Ein vernünftiger Standardprozess sieht so aus:
cd ~/stacks/mein-dienst docker compose pull docker compose up -d docker compose ps docker compose logs --tail=50
Vor Updates solltest du bei wichtigen Diensten zusätzlich:
- ein Backup machen
- Release Notes prüfen, wenn ein grösserer Versionssprung ansteht
- im Idealfall erst in einer Testumgebung prüfen
Vor allem Datenbank-lastige Dienste wie Authentik, Paperless-ngx, Immich oder Nextcloud danken dir einen vorsichtigen Update-Prozess.
10. Backups nicht erst später planen
Ein Compose-Setup ohne Backup ist kein produktives Setup. Sichere mindestens:
- Compose-Datei
.envund Konfigurationsdateien- persistente Datenverzeichnisse oder Volumes
- Datenbank-Dumps, wenn die Anwendung das sinnvoll unterstützt
Wichtig ist ausserdem, dass du nicht nur ein Backup machst, sondern auch weisst, wie ein Restore funktioniert. Genau dieser Punkt wird im Alltag am häufigsten unterschätzt.
11. Compose-Dateien kommentieren und dokumentieren
Gerade wenn mehrere Personen mit einem Server arbeiten, ist eine kurze Dokumentation Gold wert. Halte pro Stack mindestens fest:
- welcher Dienst hier läuft
- welche Domain dazugehört
- welche Daten persistent sind
- wie Updates laufen
- wie Backups und Restore funktionieren
- welche Ports absichtlich veröffentlicht sind
Das kann eine kleine README.md im jeweiligen Stack-Ordner sein. Der Aufwand ist klein, der Nutzen im Notfall riesig.
12. Beispiel für einen gut wartbaren Stack
Ein sauberer Dienst-Ordner könnte am Ende so aussehen:
~/stacks/uptime-kuma/ ├── compose.yaml ├── .env ├── README.md ├── data/ └── backups/
Und in der README.md steht kurz:
- Domain:
status.example.com - Reverse Proxy: Caddy
- Datenpfad:
./data - Update:
docker compose pull && docker compose up -d - Backup: tägliches Restic-Backup des Ordners
Genau diese Kleinigkeiten machen aus einem Bastel-Setup eine wartbare Lösung.
13. Typische Fehler, die ich vermeiden würde
- Compose-Dateien mit fest eingebauten Passwörtern
- alle Dienste in einem einzigen riesigen Compose-Projekt
- unnötig veröffentlichte Datenbank- oder Admin-Ports
- keine Trennung zwischen Konfiguration und Daten
- kein Backup der
.env-Datei - Updates ohne vorherigen Datenstand
- keine Dokumentation, warum bestimmte Ports oder Mounts gesetzt wurden
Wenn du diese Fehler vermeidest, bist du schon deutlich weiter als viele spontane Compose-Installationen.
14. Fazit
Docker Compose ist auf Ubuntu ein extrem starkes Werkzeug, wenn du es sauber strukturierst. Gute Best Practices sorgen nicht nur für Ordnung, sondern sparen bei Updates, Fehlern, Backups und Übergaben enorm viel Zeit.
Die wichtigsten Punkte sind eine klare Ordnerstruktur, saubere Variablen, bewusste Persistenz, wenige veröffentlichte Ports, Healthchecks, Log-Rotation und ein nachvollziehbarer Update- und Backup-Prozess.
Tipp:
Wenn du bereits mehrere Compose-Stacks betreibst, nimm dir einen Abend Zeit und räume sie nach diesen Punkten auf. Der Effekt ist oft grösser, als man zuerst denkt.