Zurück zum Blog
August 19, 2026Anleitungen

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.

Anwendungen mit Docker Compose auf einem VPS bereitstellen

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.

bash
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 version

Schritt 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.

bash
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/app

Schritt 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.

bash
# /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.

bash
# /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=100

Schritt 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.

bash
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 docker

Schritt 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.

bash
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 volumes

Named 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.

bash
# 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 app

Fehlerbehebung

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.