Anwendungen mit Docker Compose auf einem VPS bereitstellen
Praktische Produktionsanleitung für Docker Compose v2 auf Ubuntu: Engine installieren, Compose-Datei mit Netzwerken und Volumes schreiben, Secrets behandeln, Reverse-Proxy davorsetzen, Updates ohne Panik ausrollen und Named Volumes sichern.

Docker Compose ist der übliche Weg, einen kleinen Stack auf einem VPS zu betreiben: Web-App, Datenbank, Redis-Cache und Reverse-Proxy, beschrieben in einer YAML-Datei. Das ist nicht Kubernetes. Sie bekommen kein Multi-Node-Failover. Sie bekommen ein wiederholbares, reviewbares Setup, das Sie auf einem neuen Hiddence-VPS in Minuten nachbauen — genau das, was die meisten Nebenprojekte und kleinen Produkte brauchen.
Dieser Artikel geht davon aus, dass Sie näher an Produktion wollen als docker run an einem Freitagabend. Wir installieren Docker Engine und das Compose-Plugin, legen einen Deploy-Benutzer ohne Root an, schreiben eine Compose-Datei mit Healthchecks und Restart-Policies, halten Secrets aus Git heraus, veröffentlichen nur die Reverse-Proxy-Ports ins Internet und definieren Update- und Backup-Routine. Der Beispielstack ist eine typische Web-App plus PostgreSQL plus Caddy; dasselbe Muster gilt für Node, PHP, Python oder eine Reihe Worker.
Warum Compose auf einem einzelnen VPS weiter Sinn ergibt
Viele springen zu Kubernetes, weil es modern wirkt, und verbringen dann das Wochenende mit YAML für eine Website. Compose bleibt lesbar. Sie versionieren die Datei, dokumentieren Umgebungsvariablen und können eine Änderung diffen, bevor Sie sie anwenden. Isolation reicht: ein kompromittierter App-Container sollte den UNIX-Socket der Datenbank nicht halten, wenn Sie Netzwerke und Least-Privilege-User genutzt haben. Ressourcenlimits verhindern, dass ein Memory Leak den ganzen VPS einfriert.
- Eine Datei beschreibt den gesamten Stack
- Named Volumes überleben das Neuanlegen von Containern
- Interne Docker-Netzwerke halten Postgres vom öffentlichen Internet fern
- restart: unless-stopped deckt die meisten Reboots ab
- Leicht auf einen zweiten VPS zu kopieren, wenn der erste zu klein wird
Voraussetzungen
Nutzen Sie einen VPS mit genug RAM für App plus Postgres. Ein 2-GB-Tarif ist ein realistisches Minimum für App + Datenbank + Proxy. Es wird Ubuntu 24.04 angenommen. Installieren Sie Docker nicht aus einem beliebigen Snap, ohne zu lesen, was das mit cgroups macht; die Schritte unten nutzen Dockers offizielles apt-Repository.
- Ubuntu 22.04/24.04, root oder sudo
- 2 GB RAM empfohlen (1 GB nur für Mini-Stacks ohne Postgres)
- Eine Domain, die auf den VPS zeigt, wenn Sie automatisches HTTPS wollen
- Git oder ein anderer Weg, das Projekt auf den Server zu kopieren
Schritt 1: Docker Engine und Compose v2 installieren
Compose v2 ist ein Plugin, das als docker compose (mit Leerzeichen) aufgerufen wird, nicht das alte docker-compose-Python-Binary. Installieren Sie beides aus Dockers Repository, damit Sie aktuelle Sicherheitspatches bekommen.
ssh root@YOUR_VPS_IP
apt update && apt -y install ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
docker version
docker compose versionSchritt 2: Deploy-Benutzer und Verzeichnisstruktur
Compose als root funktioniert, ein eigener Benutzer in der Gruppe docker ist aber sauberer. Legen Sie das Projekt unter /opt oder /srv ab, nicht unter /root, damit Backups und Rechte klar sind. Das Verzeichnis mit .env niemals weltbeschreibbar machen.
adduser --disabled-password --gecos '' deploy
usermod -aG docker deploy
mkdir -p /srv/app
chown deploy:deploy /srv/app
chmod 750 /srv/app
# Log in as deploy for the rest of the file editing:
# su - deploy
# cd /srv/appSchritt 3: Eine produktionsnahe Compose-Datei schreiben
Die Datei unten ist eine Vorlage. Ersetzen Sie das App-Image durch Ihres. Postgres wird nicht auf 0.0.0.0:5432 veröffentlicht — nur Caddy auf 80 und 443. Healthchecks hindern den Reverse-Proxy daran, Traffic an eine App zu schicken, die noch die Datenbank migriert. Image-Tags pinnen; latest ist der Weg zu Überraschungsausfällen.
# /srv/app/compose.yaml
services:
caddy:
image: caddy:2.8-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
app:
condition: service_healthy
app:
image: ghcr.io/example/webapp:1.4.2
restart: unless-stopped
env_file: .env
environment:
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/health"]
interval: 10s
timeout: 3s
retries: 10
networks:
- frontend
- backend
db:
image: postgres:16-alpine
restart: unless-stopped
env_file: .env
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 10
networks:
- backend
networks:
frontend: {}
backend: {}
volumes:
pgdata:
caddy_data:
caddy_config:Schritt 4: Secrets, .env und Caddyfile
Passwörter in .env auf dem Server ablegen, chmod 600, und die Datei niemals committen. Passwörter mit openssl rand -base64 32 erzeugen. Die Caddyfile muss nur zum App-Service-Namen im Docker-Netz reverse-proxyn.
# /srv/app/.env (permissions 600)
POSTGRES_USER=app
POSTGRES_PASSWORD=change-me-long-random
POSTGRES_DB=app
# plus any APP_SECRET / NEXTAUTH_SECRET your image needs
# /srv/app/Caddyfile
app.example.com {
encode gzip
reverse_proxy app:8000
}
chmod 600 /srv/app/.env
cd /srv/app
docker compose up -d
docker compose ps
docker compose logs -f --tail=100Schritt 5: Firewall, Logs und Ressourcenlimits
UFW sollte nur 22, 80 und 443 erlauben. Docker umgeht UFW für veröffentlichte Ports manchmal; brauchen Sie eine strenge Host-Firewall, prüfen Sie das aktuelle Docker-plus-UFW-Verhalten Ihrer Ubuntu-Version oder veröffentlichen Sie Ports nur auf 127.0.0.1 und stellen Sie ein Host-Caddy davor. Memory-Limits in Compose setzen, damit Postgres den VPS nicht auffrisst.
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
# Optional in compose.yaml under app or db:
# deploy:
# resources:
# limits:
# memory: 512M
# Logs (do not let json-file grow forever):
# { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }
# in /etc/docker/daemon.json then: systemctl restart dockerSchritt 6: Updates und Rollbacks
Das langweilige Update, das funktioniert: neue Images pullen, up -d, Healthchecks beobachten, den vorherigen Tag in Git behalten, damit Sie zurückpinnen können. docker compose down auf einer Produktionsdatenbank nicht ausführen, außer Sie wollen den Traffic stoppen. down -v löscht Volumes — das ist ein Wipe, kein Neustart.
cd /srv/app
git pull # if the Compose file lives in git
# Change the image tag in compose.yaml, then:
docker compose pull
docker compose up -d
docker compose ps
# Rollback: set the old tag, pull, up -d again
# NEVER: docker compose down -v # this deletes named volumesNamed Volumes sichern
Ein VPS-Snapshot ist gut. Ein logischer Postgres-Dump ist besser, wenn Sie auf eine andere Maschine wiederherstellen müssen. Das per cron planen. Ein Restore einmal testen, sonst haben Sie keine Backups — nur Dateien, von denen Sie hoffen, dass sie Backups sind.
# Postgres dump while the db container is running:
docker compose exec -T db pg_dump -U app app | gzip > /var/backups/app-$(date +%F).sql.gz
# Copy off-box
# scp /var/backups/app-*.sql.gz backup-host:~
# Restore sketch (maintenance window):
# gunzip -c app-2026-08-19.sql.gz | docker compose exec -T db psql -U app appFehlerbehebung
Lesen Sie docker compose ps und docker compose logs service. Fehlgeschlagene Image-Pulls sind meist ghcr/Docker-Hub-Rate-Limits oder ein privates Image ohne Login. 502 von Caddy heißt, die App ist nicht healthy oder der Proxy-Hostname ist falsch (im Caddy-Container den Compose-Service-Namen nutzen, nicht localhost). Rechtefehler auf Volumes bedeuten oft, dass das Image als uid 1000 läuft, während das Host-Verzeichnis root gehört.
- compose: command not found — Sie haben den alten Binary-Namen installiert; docker compose verwenden
- port is already allocated — etwas anderes besitzt 80/443 (Apache, Nginx, ein anderes Caddy)
- database connection refused — App startete vor bereitem Postgres; healthcheck plus depends_on-Condition nutzen
- disk full — docker system df, dann ungenutzte Images vorsichtig prune-en
- permission denied on docker.sock — Benutzer nicht in der Gruppe docker, oder neue Login-Sitzung nötig
Sicherheit
Datenbankports nicht veröffentlichen. privileged: true nicht setzen. /var/run/docker.sock nicht in einen App-Container mounten, außer Sie schreiben absichtlich einen Container-Manager. Die Engine aktuell halten. Eigene Images scannen, wenn Sie sie bauen. .env auf der Platte bleibt eine Geheimdatei — einschränken, wer per SSH auf die Box darf.
- Kein öffentliches 5432 / 3306 / 6379
- Image-Digests oder zumindest unveränderliche Tags pinnen
- chmod 600 .env
- Unattended-upgrades für das Host-OS bleiben wichtig
- Ein Compose-Projekt pro App hält den Schaden kleiner
Tipps
- compose.yaml in Git; .env.example ohne echte Secrets
- Profiles für optionale Worker (docker compose --profile workers up -d)
- docker stats beobachten, wenn Sie den VPS dimensionieren
- Bind-Mounts von Quellcode sind nicht das Produktionsmuster — ein Image bauen
- Wächst der Stack auf mehrere VPS, dann Orchestrierung ansehen — nicht vorher
Ein produktionsnahes Compose-Setup auf einem VPS heißt: offizielle Docker-Pakete, abgesicherte .env, YAML, das die Datenbank nicht veröffentlicht, Healthchecks, Reverse-Proxy auf 80/443, UFW, Image-Tags zum Zurückrollen und Postgres-Dumps, die Sie einmal wirklich wiederhergestellt haben. Von der Vorlage in diesem Artikel starten, das App-Image ersetzen und docker compose down -v mit demselben Respekt behandeln wie rm -rf.