Docker Compose ersetzt weder eine Sicherungsstrategie noch eine Update-Politik noch die Härtung des VPS. Es liefert jedoch ein deklaratives Modell, das einen Stack reproduzierbar macht: Dienste, Images, Netzwerke, Volumes, Ports und Neustartrichtlinien werden in einer Datei beschrieben, die Sie lesen, testen und versionieren können.
Dieser Leitfaden behandelt nicht die Installation von Docker. Engine und Compose-Plugin müssen bereits einsatzbereit sein, gemäß Docker auf einem Linux-VPS installieren. Die Docker/UFW-Falle wird dort ebenfalls behandelt. Hier geht es darum, eine Anwendung und ihre Datenbank dauerhaft auf einem VPS zu betreiben.
Das Praxisbeispiel verwendet die offiziellen Images von WordPress und MariaDB. Die Prinzipien gelten anschließend für Bitwarden, Pi-hole, Jellyfin, Portainer und die meisten Stacks aus einer Anwendung und einem Datendienst.
Drei Konventionen, die Sie heute anwenden sollten
1. Keinen Schlüssel version: hinzufügen
Eine moderne Datei beginnt direkt mit services:. Die Top-Level-Eigenschaft version bleibt nur aus Kompatibilitätsgründen erhalten. Docker bezeichnet sie als veraltet, weist darauf hin, dass sie rein informativ ist, und erklärt, dass ihre Verwendung eine Warnung erzeugt. Compose validiert die Datei ohnehin anhand der aktuellsten Spezifikation. version: "3.8" hinzuzufügen wählt also keinen bestimmten Kompatibilitätsmodus aus.
Quelle: docs.docker.com, Eigenschaft version
2. docker compose in zwei Wörtern verwenden
Die Befehle in diesem Leitfaden nutzen das in die Docker-CLI integrierte Plugin:
docker compose up -d Der Befehl docker-compose mit Bindestrich bezeichnet das alte, in Python geschriebene Werkzeug v1. Compose v2, angekündigt 2020, ist in Go geschrieben und wird mit docker compose aufgerufen. Compose v5, veröffentlicht 2025, ist auf CLI-Seite funktional identisch mit v2: die wesentliche Neuerung ist ein offizielles Go-SDK, und die Nummerierung sprang direkt auf 5, um Verwechslungen mit den alten Dateiformaten „v2" und „v3" zu vermeiden. In jedem Fall bleibt die zu verwendende Form docker compose.
Quelle: docs.docker.com, Geschichte von Docker Compose
3. Die Datei compose.yaml nennen
Der kanonische Name lautet compose.yaml. Die Varianten compose.yml, docker-compose.yaml und docker-compose.yml werden aus Kompatibilitätsgründen weiterhin erkannt, doch Docker empfiehlt den kanonischen Namen und bevorzugt ihn, wenn mehrere Varianten im selben Verzeichnis liegen.
Quelle: docs.docker.com, Compose-Anwendungsmodell
Die gewählte Architektur
Der Stack hält vier einfache Grenzen ein:
- Der Dienst
appist der einzige, der einen Port auf dem Host veröffentlicht. - Dieser Port lauscht ausschließlich auf
127.0.0.1, damit ein auf dem VPS installierter Reverse Proxy der einzige öffentliche Einstiegspunkt ist. - Der Dienst
dbveröffentlicht keinen Port und ist nur über seinen DNS-Namendbim privaten Netzwerkbackenderreichbar. - Die Daten überleben das Neuerstellen der Container dank zweier benannter Volumes.
Compose erstellt ein internes DNS für Dienste im selben Netzwerk. Die Anwendung verbindet sich also mit db:3306, nicht mit einer Container-IP-Adresse. Eine Container-IP ist flüchtig und darf niemals in der Konfiguration festgeschrieben werden.
Ein Betriebsverzeichnis vorbereiten
Ein Stack braucht einen bekannten Ort, restriktive Berechtigungen und eine Struktur, die sich sichern lässt.
sudo install -d -m 0750 -o "$USER" -g "$USER" /opt/stacks/wordpress-prod
cd /opt/stacks/wordpress-prod
umask 077
mkdir -p secrets backups
openssl rand -base64 48 > secrets/db_password.txt
openssl rand -base64 48 > secrets/db_root_password.txt
chmod 600 secrets/*.txt
chmod 700 secrets backups Das Konto, das Compose startet, muss beide Dateien lesen können. Sie bleiben im Klartext auf der Festplatte des VPS: lokale Compose-Secrets werden dem Container als Dateien unter /run/secrets bereitgestellt, ihre Quelle bleibt jedoch eine zu schützende lokale Datei. Dieser Mechanismus beschränkt die Offenlegung auf die berechtigten Dienste und vermeidet, sie direkt in Umgebungsvariablen abzulegen. Er ersetzt keine zentrale Secret-Verwaltung.
Quelle: docs.docker.com, Secrets mit Compose verwalten
Die Datei compose.yaml, Zeile für Zeile kommentiert
Die Datei beginnt bewusst mit services:. Sie enthält keinen Schlüssel version:.
services:
# HTTP-Dienst der Anwendung.
app:
# Die Image-Referenz stammt aus .env und muss vor dem Produktivbetrieb geprüft werden.
image: "${WORDPRESS_IMAGE:?WORDPRESS_IMAGE in .env setzen}"
# Startet beim Reboot neu, außer ein Administrator hat den Dienst bewusst gestoppt.
restart: unless-stopped
# Verhindert Zombie-Prozesse, wenn das Image sie nicht selbst einsammelt.
init: true
# Die Datenbank muss als gesund gemeldet sein, bevor die Anwendung erstellt wird.
depends_on:
db:
condition: service_healthy
# Port 80 des Containers lauscht nur auf der Loopback-Schnittstelle des VPS.
ports:
- "${APP_BIND_IP:-127.0.0.1}:${APP_PORT:-8080}:80"
# Nicht sensible Werte können aus .env interpoliert werden.
environment:
WORDPRESS_DB_HOST: "db:3306"
WORDPRESS_DB_NAME: "${DB_NAME:?DB_NAME in .env setzen}"
WORDPRESS_DB_USER: "${DB_USER:?DB_USER in .env setzen}"
# Das offizielle Image kann das Passwort aus einer Datei lesen.
WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_password
# Die Anwendungsinhalte überdauern den Lebenszyklus des Containers.
volumes:
- wordpress_data:/var/www/html
# Nur das für die Anwendung nötige Passwort wird ihr gewährt.
secrets:
- db_password
# Die Anwendung nimmt den Frontend-Verkehr an und spricht privat mit der Datenbank.
networks:
- frontend
- backend
# Eine lokale Begrenzung verhindert, dass Protokolle die Festplatte des VPS füllen.
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# Gibt dem HTTP-Server Zeit, seine Verbindungen sauber zu schließen.
stop_grace_period: 30s
# Datenbankdienst, ohne veröffentlichten Port auf dem Host.
db:
# Auch diese Image-Referenz wird in .env verwaltet.
image: "${MARIADB_IMAGE:?MARIADB_IMAGE in .env setzen}"
# Ein manueller Stopp wird auch nach Neustart des Daemons oder des VPS respektiert.
restart: unless-stopped
# Initialisiert Datenbank und Benutzer nur beim allerersten Start.
environment:
MARIADB_DATABASE: "${DB_NAME:?DB_NAME in .env setzen}"
MARIADB_USER: "${DB_USER:?DB_USER in .env setzen}"
MARIADB_PASSWORD_FILE: /run/secrets/db_password
MARIADB_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
# Die MariaDB-Datendateien liegen in einem benannten Volume.
volumes:
- mariadb_data:/var/lib/mysql
# Anders als die Anwendung erhält die Datenbank beide Secrets.
secrets:
- db_password
- db_root_password
# Dieses Skript wird vom offiziellen MariaDB-Image bereitgestellt.
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
# Die Datenbank gehört ausschließlich zum privaten internen Netzwerk.
networks:
- backend
# Dieselbe lokale Rotationsrichtlinie wie für die Anwendung.
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# Eine Datenbank kann Zeit brauchen, um ihre Schreibvorgänge abzuschließen.
stop_grace_period: 1m
# Deklaration der von Docker verwalteten persistenten Speicher.
volumes:
wordpress_data:
mariadb_data:
# Deklaration der beiden Netzwerkzonen des Stacks.
networks:
frontend:
driver: bridge
backend:
driver: bridge
# Verhindert, dass dieses Netzwerk direkte externe Konnektivität bereitstellt.
internal: true
# Jedes lokale Secret stammt aus einer eigenen Datei auf dem Host.
secrets:
db_password:
file: ./secrets/db_password.txt
db_root_password:
file: ./secrets/db_root_password.txt Das offizielle WordPress-Image unterstützt die Konvention WORDPRESS_DB_PASSWORD_FILE. Das offizielle MariaDB-Image akzeptiert die Varianten MARIADB_*_FILE und liefert healthcheck.sh mit. Diese Verhaltensweisen gehören zu den Images, nicht zu Compose selbst. Prüfen Sie daher die Dokumentation jedes Images, bevor Sie dieses Modell auf eine andere Anwendung übertragen.
Quellen: offizielles WordPress-Image, offizielles MariaDB-Image, MariaDB-Referenz zu healthcheck.sh
depends_on mit condition: service_healthy hindert Compose daran, die Anwendung zu erstellen, bevor die Gesundheitsprüfung der Datenbank erfolgreich ist. Das verbessert den ersten Start, entbindet die Anwendung aber nicht davon, eine im Betrieb verlorene Verbindung erneut aufbauen zu können. Siehe Startreihenfolge in Compose.
Die Datei .env: Konfiguration, kein Tresor
Compose liest automatisch eine .env-Datei neben der compose.yaml und verwendet sie, um Ausdrücke der Form ${VARIABLE} zu interpolieren.
# Stabiler Projektname. Er beeinflusst unter anderem die realen Namen von Volumes und Netzwerken.
COMPOSE_PROJECT_NAME=wordpress-prod
# Der Dienst bleibt lokal auf dem VPS und wird von einem Reverse Proxy veröffentlicht.
APP_BIND_IP=127.0.0.1
APP_PORT=8080
# Nicht sensible Werte, die an beide Dienste übergeben werden.
DB_NAME=wordpress
DB_USER=wordpress
# Lesbare Selektoren zur Vorbereitung des ersten Pull.
# Ersetzen Sie sie vor dem Produktivbetrieb durch die geprüften Digests, wie unten erläutert.
WORDPRESS_IMAGE=wordpress:apache
MARIADB_IMAGE=mariadb:lts Quelle: docs.docker.com, Variableninterpolation in Compose
Image-Tags sind veränderlich: ein Herausgeber kann denselben Tag auf andere Inhalte zeigen lassen. Für eine streng reproduzierbare Bereitstellung prüfen Sie das Image und fixieren dann seine Referenz mit einem Digest name@sha256:.... Eine Aktualisierung des Digests wird damit zu einer bewussten und überprüfbaren Änderung. Siehe die Docker-Best-Practices zur Fixierung per Digest.
So erhalten Sie die unveränderlichen Referenzen der soeben getesteten Images:
docker pull wordpress:apache
docker pull mariadb:lts
docker image inspect wordpress:apache --format '{{index .RepoDigests 0}}'
docker image inspect mariadb:lts --format '{{index .RepoDigests 0}}' Übertragen Sie jedes vollständige Ergebnis in die entsprechende Variable in .env. Nach dieser Fixierung ändert docker compose pull die Inhalte nicht mehr stillschweigend: die Aktualisierung erfolgt über eine ausdrückliche Änderung des Digests.
Der Digest fixiert das Image, nicht den Zustand des Volumes. Das ist in diesem Beispiel besonders wichtig: das offizielle WordPress-Image weist darauf hin, dass automatische WordPress-Updates den Inhalt von /var/www/html nach der Bereitstellung verändern können. Eine reproduzierbare Update-Politik muss also sowohl die Images als auch den anwendungseigenen Update-Mechanismus abdecken.
Auch wenn das Beispiel kein Passwort in .env ablegt, darf diese Datei nicht committet werden. In vielen Projekten landen dort mit der Zeit ein Token, eine private URL oder ein produktionsspezifischer Wert. Ein Secret, das direkt in compose.yaml oder .env geschrieben und nach Git gepusht wurde, bleibt in der Historie, auch wenn es im letzten Commit entfernt wird.
Eine weitere wichtige Falle: die Variablen MARIADB_DATABASE, MARIADB_USER und MARIADB_*_PASSWORD_FILE dienen dazu, ein leeres Datenverzeichnis zu initialisieren. Das offizielle Image stellt klar, dass sie eine bereits vorhandene Datenbank nicht neu konfigurieren. Den Inhalt einer Secret-Datei zu ersetzen ändert also nicht automatisch das in MariaDB gespeicherte Passwort. Eine Rotation muss das Konto in der Datenbank ändern, das von der Anwendung genutzte Secret aktualisieren und anschließend die Komponenten gemäß ihrer Dokumentation neu starten oder neu laden.
Erstellen Sie eine Datei .gitignore:
.env
secrets/
backups/ Sie können eine .env.example versionieren, die ausschließlich Variablennamen und Platzhalterwerte enthält. Schützen Sie die echte Datei:
chmod 600 .env Vorsicht auch bei docker compose config ohne Option: dieser Befehl gibt die aufgelöste Konfiguration aus. Er kann interpolierte Werte offenlegen, wenn Sie Secrets in Variablen abgelegt haben. docker compose config --quiet validiert, ohne die Konfiguration auszugeben. Siehe die Referenz zu docker compose config.
Benannte Volumes und Bind Mounts: nicht verwechseln
Ein Container ist austauschbar. Alle Daten, die nur in seiner internen Schicht geschrieben werden, verschwinden mit ihm. Persistenz muss ausdrücklich deklariert werden.
| Kriterium | Benanntes Volume | Bind Mount |
|---|---|---|
| Quelle | Von Docker verwaltetes Objekt | Expliziter Pfad auf dem VPS, zum Beispiel /srv/app/config |
| Empfohlen für | Von der Anwendung oder der Datenbank erzeugte Daten | Vom Host verwaltete Konfigurationsdatei, Zertifikat, Inhalte, die der Host direkt bearbeiten muss |
| Portabilität | Kaum an den Verzeichnisbaum des VPS gebunden | Hängt vom Pfad, den Berechtigungen und mitunter vom SELinux-Kontext des Hosts ab |
| Hauptrisiko | Zu vergessen, dass es außerhalb des Stack-Ordners existiert | Den falschen Pfad einhängen, bereits im Image vorhandene Inhalte verdecken oder zu weitreichenden Schreibzugriff gewähren |
| Sicherung | Muss ausdrücklich exportiert werden | Muss ausdrücklich in die Sicherung des Host-Pfades einbezogen werden |
Docker empfiehlt Volumes für persistente Daten, die von Containern erzeugt werden. Ein Volume überlebt das Löschen des Containers, der es genutzt hat. Ein Bind Mount ist vorzuziehen, wenn ein Administrator oder ein Werkzeug des Hosts eine Datei direkt bearbeiten muss.
In diesem Stack:
wordpress_databewahrt/var/www/htmlauf;mariadb_databewahrt/var/lib/mysqlauf;./secrets/*.txtsind Host-Dateien, die separat in die berechtigten Dienste eingehängt werden.
Wo liegt ein benanntes Volume tatsächlich?
Der Name mariadb_data ist der logische Name im Compose-Modell. Mit COMPOSE_PROJECT_NAME=wordpress-prod erzeugt Docker in der Regel einen Namen wie wordpress-prod_mariadb_data. Bauen Sie Ihre Skripte nicht auf dieser Annahme auf. Compose versieht Volumes mit Labels, und docker volume inspect gibt ihren tatsächlichen Einhängepunkt zurück:
docker volume ls \
--filter label=com.docker.compose.project=wordpress-prod
DB_VOLUME="$(docker volume ls \
--filter label=com.docker.compose.project=wordpress-prod \
--filter label=com.docker.compose.volume=mariadb_data \
--format '{{.Name}}')"
docker volume inspect "$DB_VOLUME" --format '{{.Mountpoint}}' Mit dem lokalen Treiber und einem klassischen rootful Docker-Daemon liegt der Pfad häufig unter /var/lib/docker/volumes/. Eine Garantie ist das nicht: der Rootless-Modus, ein angepasstes data-root oder ein entfernter Treiber ändern diesen Ort. Maßgeblich ist die Antwort von docker volume inspect. Bearbeiten Sie die internen Dateien einer Datenbank nicht direkt in diesem Verzeichnis.
Die Befehle, die Daten zerstören
docker compose down entfernt die Container und Netzwerke des Stacks, behält benannte Volumes jedoch standardmäßig bei. Die Option -v verlangt auch deren Löschung.
Verwenden Sie
docker compose down -vniemals als Update- oder Fehlerbehebungsbefehl. Vergewissern Sie sich vor jedem bewussten Löschen eines Volumes, dass eine wiederherstellbare Sicherung vorliegt.
Ebenso löscht docker volume prune Volumes, die als ungenutzt gelten. Ein Produktionsvolume gilt als „ungenutzt", sobald sein Container gelöscht wurde, selbst wenn seine Daten weiterhin unverzichtbar sind. Siehe das Verhalten von docker compose down.
Portveröffentlichung: der Unterschied zwischen lokal und öffentlich
Diese beiden Zeilen sind nicht gleichwertig:
ports:
- "8080:80" Ohne Host-Adresse veröffentlicht Docker den Port auf allen Adressen des VPS, in der Praxis 0.0.0.0 und je nach Konfiguration auch IPv6. Der Dienst kann dann aus dem Internet erreichbar werden, wenn Routing und Netzwerkregeln es zulassen.
ports:
- "127.0.0.1:8080:80" Hier lauscht der Port nur auf der IPv4-Loopback-Adresse. Das ist die richtige Wahl, wenn Nginx oder Apache direkt auf dem VPS läuft und die Anfragen an http://127.0.0.1:8080 weiterleitet. Docker dokumentiert ausdrücklich, dass das Weglassen der Adresse auf allen Adressen veröffentlicht und dass eine Bindung an 127.0.0.1 den Zugriff auf den Host beschränkt.
Quelle: docs.docker.com, Portveröffentlichung
Prüfen Sie das Ergebnis, verlassen Sie sich nicht auf das erneute Lesen des YAML:
docker compose ps
ss -lntp | grep ':8080'
curl --fail --head http://127.0.0.1:8080 Die Datenbank hat keinen Abschnitt ports. Die Direktive expose ist nicht nötig, damit app die db im selben Compose-Netzwerk erreicht.
Docker verwaltet eigene Firewall-Regeln, und ein veröffentlichter Port kann den mit UFW erwarteten Filterpfad umgehen. Dieser Punkt wird in Einen Linux-VPS absichern: die komplette Checkliste und im als Voraussetzung genannten Docker-Installationsleitfaden ausführlich behandelt. Die drei stimmigen Regeln lauten: nichts veröffentlichen, was nicht veröffentlicht werden muss, hinter einem lokalen Proxy an 127.0.0.1 binden und anschließend prüfen, was tatsächlich lauscht.
Ist der Reverse Proxy selbst ein Container, bezeichnet 127.0.0.1 diesen Container, nicht den Host und nicht die Anwendung. Verbinden Sie dann Proxy und Anwendung mit einem gemeinsamen Docker-Netzwerk, ohne den Anwendungsport im Internet zu veröffentlichen. Für einen auf dem Host installierten Proxy siehe Mehrere Websites auf einem VPS mit Nginx hosten und anschließend Certbot: ein SSL Let's Encrypt auf VPS installieren.
always oder unless-stopped beim Neustart des VPS?
Beide Richtlinien starten einen Container nach einem Ausfall und bei der Rückkehr des Docker-Daemons neu. Der Unterschied zeigt sich nach einem administrativen Stopp:
alwaysstartet einen manuell gestoppten Container beim Neustart des Docker-Daemons erneut;unless-stoppedrespektiert diesen manuellen Stopp, auch nach dem Neustart des Daemons oder des VPS.
Für einen manuell administrierten Stack verhindert unless-stopped, dass beim nächsten Reboot ein Dienst wieder online geht, den Sie bewusst für Wartungsarbeiten gestoppt hatten. always eignet sich, wenn ein Dienst grundsätzlich zurückkehren soll und dieses Verhalten ausdrücklich gewünscht ist. Die Richtlinie ersetzt keine Gesundheitsprüfung: sie reagiert auf das Beenden des Hauptprozesses, nicht auf eine laufende, aber blockierte Anwendung.
Quelle: docs.docker.com, Neustartrichtlinien
Den Stack validieren und starten
Prüfen Sie zunächst die CLI:
docker compose version
docker info Validieren und deployen Sie dann aus dem Stack-Verzeichnis heraus:
cd /opt/stacks/wordpress-prod
docker compose config --quiet
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 --timestamps docker compose pull lädt die Images herunter, ersetzt jedoch keine laufenden Container. docker compose up -d vergleicht anschließend den gewünschten mit dem vorhandenen Zustand, erstellt was fehlt und erstellt jene Dienste neu, deren Image oder Konfiguration sich geändert hat. Eingehängte Volumes bleiben bei dieser Neuerstellung erhalten. Siehe das Verhalten von docker compose up.
Prüfen Sie nach dem ersten Start auch die Erreichbarkeit von einer anderen Maschine aus. Ein erfolgreicher lokaler Test beweist nicht, dass Port 8080 öffentlich unerreichbar ist.
Aktualisieren ohne Datenverlust
Ein Produktionsupdate ist ein kleines Änderungsverfahren und kein blind abgesetzter Befehl.
Vor dem Update
- Lesen Sie die Versionshinweise der Anwendung und der Datenbank, insbesondere zu Migrationen und unterstützten Upgrade-Pfaden.
- Prüfen Sie Speicherplatz und Inodes mit
df -hunddf -i. - Erstellen Sie eine Sicherung und testen Sie deren Wiederherstellung regelmäßig.
- Notieren Sie die aktuell bereitgestellten Image-Referenzen mit
docker compose images. - Ändern Sie die genauen Tags oder Digests in
.enverst nach der Prüfung. - Führen Sie
docker compose config --quietaus.
Das Update anwenden
cd /opt/stacks/wordpress-prod
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --since=10m --timestamps Was neu erstellt werden kann:
- der Container
app, wenn sich seine Image-Referenz oder Konfiguration ändert; - der Container
db, wenn sich seine Image-Referenz oder Konfiguration ändert; - die Netzwerke, wenn sich ihre Definition ändert.
Was erhalten bleibt:
- der Inhalt der benannten Volumes
wordpress_dataundmariadb_data; - die als Bind Mounts oder Secret-Quellen genutzten Dateien des VPS;
- die vorherigen Images, solange sie nicht gelöscht wurden.
docker compose restart genügt nach einer Änderung an compose.yaml oder an den Variablen nicht: dieser Befehl startet vorhandene Container neu, ohne die neue Konfiguration anzuwenden. Verwenden Sie docker compose up -d. Siehe die Referenz zu docker compose restart.
Eine Neuerstellung kann eine kurze Unterbrechung verursachen. Compose auf einem einzelnen VPS verspricht kein unterbrechungsfreies Update. Planen Sie für eine kritische Anwendung eine passende Strategie, eine zweite Instanz oder eine Orchestrierungsplattform ein.
Zurückrollen
Für ein Rollback der Anwendung tragen Sie die vorherige Image-Referenz wieder in .env ein und führen dann erneut pull und up -d aus. Diese Methode macht keine Schemamigration rückgängig. Hat die neue Version die Datenbank inkompatibel verändert, hilft nur ein vom Herausgeber dokumentierter Rückfallplan oder eine geprüfte Wiederherstellung. Genau deshalb steht die Sicherung vor dem Update.
Den Stack sichern, nicht nur sein YAML
Das Kopieren der compose.yaml sichert keinerlei Daten aus den Volumes. Umgekehrt garantiert das Kopieren der internen MariaDB-Dateien im laufenden Betrieb keine konsistente Datenbank. Für diesen Fall kombiniert man:
- einen logischen Dump von MariaDB, erzeugt mit dem Werkzeug der Datenbank;
- ein Archiv des Anwendungsvolumes, während die Anwendung gestoppt ist;
- eine Kopie von
compose.yaml,.envund der Liste der Image-Referenzen; - eine getrennte, verschlüsselte Sicherung der Secret-Dateien;
- eine Kopie außerhalb des VPS, mit Aufbewahrungsfristen und Wiederherstellungstests.
MariaDB dokumentiert mariadb-dump in seinem offiziellen Leitfaden zur Containersicherung.
Beispiel für eine konsistente Sicherung
Führen Sie diesen Block mit Bash aus dem Stack-Verzeichnis aus. Die Anwendung wird gestoppt, um Schreibvorgänge während des Dumps und der Archivierung zu verhindern. Die Datenbank bleibt für die Dauer des logischen Dumps aktiv. Der trap startet die Anwendung auch dann neu, wenn ein Befehl fehlschlägt.
set -Eeuo pipefail
cd /opt/stacks/wordpress-prod
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
DEST="backups/$STAMP"
install -d -m 0700 "$DEST"
docker compose config --quiet
docker compose stop app
trap 'docker compose start app' EXIT
docker compose exec -T db sh -c '
exec mariadb-dump \
--user=root \
--password="$(cat /run/secrets/db_root_password)" \
--single-transaction \
--routines \
--triggers \
--events \
"$MARIADB_DATABASE"
' | gzip -9 > "$DEST/database.sql.gz"
docker compose run --rm --no-deps -T \
--entrypoint tar app \
-C /var/www/html -czf - . \
> "$DEST/wordpress-data.tar.gz"
cp compose.yaml .env "$DEST/"
docker compose images > "$DEST/images.txt"
gzip -t "$DEST/database.sql.gz"
tar -tzf "$DEST/wordpress-data.tar.gz" > /dev/null
(cd "$DEST" && sha256sum database.sql.gz wordpress-data.tar.gz compose.yaml .env images.txt > SHA256SUMS)
docker compose start app
trap - EXIT Dieses Skript kopiert secrets/ bewusst nicht in dasselbe Archiv. Exportieren Sie diese Dateien in einen Tresor oder eine verschlüsselte Sicherung mit eigener Zugriffskontrolle. Eine Sicherung, die nur auf demselben VPS liegt, verschwindet mit der Festplatte, dem Konto oder dem Vorfall, der die Produktion zerstört.
Ein Dump mit --single-transaction eignet sich für transaktionale Tabellen. Eine Anwendung mit anderen Engines oder mehreren Speichersystemen erfordert ein herstellerspezifisches Konsistenzverfahren. Prüfen Sie bei großen Datenmengen und strengen Wiederanlaufzielen zusätzlich Werkzeuge zur physischen Sicherung, Replikation und koordinierte Snapshots.
Wenn Sie diese Kette nicht selbst skripten und überwachen möchten: die zu unseren Linux-VPS optional erhältlichen automatischen Backups erstellen eine tägliche Kopie in einem separaten Rechenzentrum, mit rotierender Historie und Wiederherstellung per Klick. Sie ersetzen keinen Test der anwendungsseitigen Wiederherstellung, decken aber den Fall ab, dass der VPS selbst verloren geht.
Eine Wiederherstellung testen
Eine Datei, deren Wiederherstellung nie getestet wurde, ist nur eine Sicherungshypothese. Der Test muss in einem isolierten Projekt erfolgen, mit leeren Volumes und denselben Image-Referenzen wie die Sicherung:
- Kopieren Sie
compose.yaml,.envund die Test-Secrets auf einen anderen VPS oder in ein isoliertes Verzeichnis. - Ändern Sie
COMPOSE_PROJECT_NAMEund den Host-Port, um die Produktion nicht zu berühren. - Erstellen Sie das Anwendungsvolume mit einem einmaligen Befehl und entpacken Sie dann das Archiv.
- Starten Sie nur
db, warten Sie den Zustandhealthyab und importieren Sie dann den Dump in die leere Datenbank. - Starten Sie
app, prüfen Sie die fachlichen Funktionen und halten Sie die tatsächliche Wiederanlaufzeit fest.
Beispiel für einen Import in eine bereits initialisierte, leere Testdatenbank:
gzip -dc backups/DATE/database.sql.gz | \
docker compose exec -T db sh -c '
exec mariadb \
--user=root \
--password="$(cat /run/secrets/db_root_password)" \
"$MARIADB_DATABASE"
' Führen Sie diesen Import nicht auf der bestehenden Produktionsdatenbank aus. Ein vollständiges Wiederherstellungsverfahren muss festlegen, wie eine leere Datenbank bereitgestellt wird, welche Ausfallzeit akzeptiert wird und wie Sie zum vorherigen Zustand zurückkehren, falls die Prüfung fehlschlägt.
Protokolle lesen und eine Neustartschleife diagnostizieren
Der erste Fehler besteht meist darin, sofort down auszuführen. Das löscht die Container und vernichtet einen Teil der nützlichen Zustandsinformationen. Beginnen Sie mit dem Beobachten.
docker compose ps --all
docker compose logs --tail=200 --timestamps app
docker compose logs --tail=200 --timestamps db
docker compose logs --follow --since=10m app Der Befehl logs kann nach Dienst filtern, dem Datenstrom folgen und die Historie mit --tail oder --since begrenzen. Siehe die Referenz zu docker compose logs.
Untersuchen Sie anschließend den genauen Zustand des Containers:
CID="$(docker compose ps -q app)"
docker inspect "$CID" --format \
'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} restarts={{.RestartCount}} error={{.State.Error}}'
docker compose top
docker stats --no-stream
df -h
df -i
free -h
journalctl -u docker --since '30 minutes ago' Häufige Ursachen sind:
- ein fehlendes, nicht lesbares oder unter falschem Namen eingehängtes Secret;
- eine noch nicht verfügbare Datenbank, nicht übereinstimmende Zugangsdaten oder eine fehlgeschlagene Migration;
- ein bereits belegter Host-Port;
- ein an falscher Stelle eingehängtes Volume oder Berechtigungen, die mit dem Container-Benutzer unvereinbar sind;
- ein wegen Speichermangels beendeter Prozess, erkennbar an
OOMKilled=true; - eine volle Festplatte oder erschöpfte Inodes;
- ein mit der Architektur des VPS inkompatibles Image;
- eine interpolierte Konfiguration, die von der erwarteten abweicht.
Validieren Sie das Modell, ohne Werte auszugeben, und sehen Sie sich dann die erwarteten Variablen an:
docker compose config --quiet
docker compose config --variables Startet der Dienst zu schnell neu, um exec zu erlauben, starten Sie einen einmaligen Container ohne seine Abhängigkeiten und mit einer Shell, sofern das Image eine mitbringt:
docker compose run --rm --no-deps --entrypoint sh app Dieser einmalige Container hängt dieselben Volumes und Secrets ein, die für den Dienst deklariert sind. Nehmen Sie keine Änderungen vor, solange die Ursache nicht verstanden ist.
Die in der Datei deklarierte json-file-Rotation begrenzt den Platz, den lokale Protokolle belegen. Docker weist darauf hin, dass max-size standardmäßig -1 beträgt, also unbegrenzt ist. Versenden Sie bei größerem Produktivbetrieb die Protokolle zusätzlich an ein externes System mit passender Aufbewahrung. Siehe den Logging-Treiber json-file.
Betriebsprüfungen, die Sie beibehalten sollten
Bei jeder Änderung
docker compose config --quiet
docker compose up -d
docker compose ps
docker compose logs --since=10m --timestamps Vor jedem Update
- Versionshinweise und Migrationspfad gelesen;
- Speicherplatz geprüft;
- Sicherung erstellt, außerhalb des VPS abgelegt, Wiederherstellung bereits getestet;
- aktuelle Images notiert;
- Wartungsfenster angekündigt, falls nötig.
Nach jedem Neustart des VPS
docker compose ps
docker compose logs --since=30m --timestamps
ss -lntp Prüfen Sie, dass die Anwendung gesund ist, die Datenbank nicht veröffentlicht wird, der Anwendungsport weiterhin an 127.0.0.1 gebunden ist und der Reverse Proxy die Domain über HTTPS ausliefert.
Häufige Fehler, die Sie vermeiden sollten
- die Datei mit
version: "3.8"beginnen; - das historische Binary
docker-composeverwenden; - die Datei in einem neuen Projekt
docker-compose.ymlnennen; 8080:80schreiben und dabei annehmen, der Dienst bleibe lokal;3306:3306veröffentlichen, obwohl nur die Anwendung die Datenbank erreichen muss;- Passwörter in
compose.yamloder im Git-Repository ablegen; - einen Bind Mount für eine Datenbank verwenden, ohne Eigentümer, Berechtigungen, Sicherung und Sicherheitskontext zu beherrschen;
- glauben, das Löschen eines Containers lösche oder sichere sein Volume;
docker compose down -vausführen, um „sauber neu anzufangen";docker compose restartausführen und erwarten, dass eine neue Konfiguration angewendet wird;- eine Datenbank aktualisieren, ohne ihren Migrationspfad zu lesen oder einen Rückweg zu haben;
- ausschließlich auf der Festplatte des VPS sichern;
- einen Container im Zustand
runningmit einer tatsächlich gesunden Anwendung verwechseln.
Häufige Fragen
Muss version in einer Compose-Datei noch geschrieben werden?
Nein. Die Top-Level-Eigenschaft version ist als veraltet dokumentiert: sie ist rein informativ und erzeugt eine Warnung. Compose interpretiert die Datei mit der aktuellen Spezifikation, unabhängig vom geschriebenen Wert.
Was ist der Unterschied zwischen docker compose und docker-compose?
docker-compose mit Bindestrich ist das Python-Werkzeug v1. docker compose in zwei Wörtern ist das Go-Plugin, seit 2020 in v2 und seit 2025 in v5. Nur diese zweite Form sollte heute verwendet werden.
Löscht docker compose down meine Daten?
Standardmäßig nicht: benannte Volumes bleiben erhalten. Die Option -v löscht sie, ebenso docker volume prune bei einem Volume, zu dem kein Container mehr existiert.
Wie aktualisiere ich einen Stack, ohne die Datenbank zu verlieren?
Erstellen Sie eine Sicherung, ändern Sie die Image-Referenz in .env und führen Sie dann docker compose pull und anschließend docker compose up -d aus. Die Container werden neu erstellt, die benannten Volumes bleiben erhalten. docker compose restart genügt nicht: der Befehl liest die Konfiguration nicht neu ein.
Welche VPS-Konfiguration eignet sich für einen Compose-Stack?
Eine Webanwendung und ihre Datenbank passen auf 2 vCPU und 4 GB RAM. Planen Sie mehr Arbeitsspeicher ein, sobald Sie einen Cache, eine Suchmaschine oder mehrere Stacks auf derselben Maschine hinzufügen, und achten Sie vor allem auf den Speicherplatz: Images, Volumes und Protokolle summieren sich schneller als erwartet.
Fazit
Ein zuverlässiger Compose-Stack beruht weniger auf der Menge an YAML als auf einigen Invarianten: eine compose.yaml, die der aktuellen Spezifikation entspricht, bewusst gewählte und bewusst aktualisierte Images, identifizierte Volumes, nur im Notwendigen veröffentlichte Ports, vom Repository getrennte Secrets, eine wiederherstellbare Sicherung und eine Diagnose auf Basis des tatsächlichen Containerzustands.
Der normale Zyklus wird damit vorhersehbar: validieren, sichern, Images ziehen, mit docker compose up -d anwenden, die Gesundheit prüfen und die Möglichkeit zum Zurückrollen bewahren. Genau diese Disziplin macht aus einem Compose-Beispiel eine Betriebsgrundlage für die nächsten Stacks.
