Zum Inhalt springen
myvpsguide

Docker im echten Betrieb: Compose, Netze, Volumes

Eine Compose-Datei, die in einem Jahr noch trägt: Ports lokal binden, Netze trennen, Secrets auslagern, Healthchecks, Logrotation, Start per systemd.

veröffentlicht 03.06.2026
4 min lesezeit
fortgeschritten

Docker-Anleitungen enden meistens bei docker compose up -d. Danach beginnt der Teil, der über Monate entscheidet: Wie kommt der Stack nach einem Neustart wieder hoch? Wo liegen die Daten? Warum ist die Datenbank aus dem Internet erreichbar, obwohl die Firewall zu ist? Und warum ist die Platte voll?

Die Compose-Datei als Dokumentation#

Eine gute Compose-Datei erklärt sich selbst. Das hier ist das Grundgerüst, an dem sich der Rest des Beitrags entlanghangelt:

name: appstack

services:
  app:
    image: ghcr.io/beispiel/app:1.8.2      # feste Version, kein :latest
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      DATABASE_URL: postgres://app:${DB_PASSWORT}@db:5432/app
    networks: [intern, web]
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:3000/healthz"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 40s
    logging:
      driver: json-file
      options: { max-size: "10m", max-file: "3" }

  db:
    image: postgres:17.4-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORT}
    volumes:
      - db-daten:/var/lib/postgresql/data
    networks: [intern]                      # bewusst kein Zugang zum Web-Netz
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 10s
      timeout: 5s
      retries: 5
    logging:
      driver: json-file
      options: { max-size: "10m", max-file: "3" }

  proxy:
    image: caddy:2.10-alpine
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"                 # nur lokal, davor der Host-Proxy
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-daten:/data
    networks: [web]

networks:
  intern:
    internal: true                          # kein Weg ins Internet
  web:

volumes:
  db-daten:
  caddy-daten:

Ports: der Fehler, der am meisten kostet#

ports: - "5432:5432" bindet auf allen Adressen. Docker schreibt dafür eigene Regeln in die nat-Tabelle und umgeht damit ufw, die Firewall meldet weiterhin, der Port sei zu, während die Datenbank aus dem Internet erreichbar ist.

Drei Regeln, die das Problem beseitigen:

  1. Was nur intern gebraucht wird, bekommt gar kein ports. Container im selben Netz erreichen sich über den Dienstnamen (db:5432). Ein veröffentlichter Port ist dafür nicht nötig.
  2. Was lokal erreichbar sein muss, wird an 127.0.0.1 gebunden. "127.0.0.1:8080:80".
  3. Was von außen erreichbar sein muss, steht hinter einem Reverse-Proxy auf dem Host, dann ist genau ein Port offen, und TLS liegt an einer Stelle.

Prüfen, was tatsächlich lauscht:

sudo ss -tulpn | grep docker
docker compose ps --format 'table {{.Service}}\t{{.Ports}}'

Netze trennen#

internal: true nimmt einem Netz den Weg nach draußen. Container darin erreichen einander, aber nicht das Internet. Für eine Datenbank ist das die richtige Einstellung: Sie muss nichts nachladen, und wenn sie es versucht, will man das wissen.

Der Anwendungscontainer hängt in beiden Netzen und ist damit die einzige Brücke. Das ist keine harte Sicherheitsgrenze, aber es macht aus einem „die Datenbank hat Internet“ ein bewusstes, sichtbares Detail.

Secrets gehören nicht in die Compose-Datei#

Die Compose-Datei gehört ins Versionsverwaltungssystem. Passwörter nicht.

# .env neben der compose.yaml — und in .gitignore
cat > .env <<EOF
DB_PASSWORT=$(openssl rand -base64 32)
EOF
chmod 600 .env
# .gitignore
.env

Für mehr als eine Handvoll Geheimnisse gibt es secrets: in Compose, die als Dateien unter /run/secrets/ eingehängt werden, dann steht das Passwort nicht in der Prozessumgebung, wo es jedes docker inspect zeigt.

.env sichern

Die .env ist die Datei, die beim Wiederherstellen am häufigsten fehlt, sie ist nicht im Git und nicht im Volume. Sie gehört ausdrücklich in die Backup-Liste.

Volumes: benannt statt Bind-Mount#

Für Daten, die dem Container gehören (Datenbankdateien, Caddy-Zertifikate), sind benannte Volumes richtig: Docker verwaltet Rechte und Pfad.

Für Dinge, die du bearbeitest (Konfigurationsdateien), ist ein Bind-Mount richtig, dann liegt die Datei sichtbar neben der Compose-Datei und ist versionierbar. Mit :ro, wenn der Container sie nur lesen soll.

Sichern lässt sich ein Volume ohne Wissen über sein Innenleben:

docker run --rm \
  -v appstack_db-daten:/quelle:ro \
  -v "$PWD":/ziel \
  alpine tar czf /ziel/db-daten-$(date +%F).tar.gz -C /quelle .

Für Datenbanken ist ein Dump trotzdem besser als ein Dateikopie, die Begründung steht in Backup-Strategie 3-2-1:

docker compose exec -T db pg_dump -U app app | zstd -9 > db-$(date +%F).sql.zst

Healthchecks sind nicht Kosmetik#

Ohne Healthcheck weiß Docker nur, ob der Prozess läuft, nicht, ob der Dienst antwortet. Mit Healthcheck bekommt man zwei Dinge:

  • depends_on mit condition: service_healthy wartet wirklich, statt nur die Startreihenfolge zu beeinflussen.
  • Ein hängender Container ist als unhealthy sichtbar, statt scheinbar zu laufen.
docker compose ps          # Spalte STATUS zeigt (healthy) / (unhealthy)

start_period ist der Wert, den man beim ersten Mal vergisst: Eine Anwendung, die 30 Sekunden zum Starten braucht, gilt sonst währenddessen als krank und wird bei aktivem Neustart-Verhalten in einer Schleife neu gestartet.

Logs begrenzen, bevor die Platte voll ist#

Docker schreibt Container-Ausgaben standardmäßig unbegrenzt nach /var/lib/docker/containers/. Ein gesprächiger Dienst füllt damit über Monate die Systemplatte und der Ausfall kommt dann als „nichts geht mehr“, nicht als „Logs zu groß“.

Pro Dienst über logging: wie oben, oder einmal global:

// /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}
sudo systemctl restart docker

Die globale Einstellung wirkt nur auf neu erstellte Container, bestehende behalten ihre alte Konfiguration bis zum nächsten docker compose up --force-recreate.

Beim Booten starten: systemd statt restart: always#

restart: unless-stopped bringt Container nach einem Docker-Neustart zurück. Für einen sauberen Start des ganzen Stacks nach einem Server-Neustart, inklusive Abhängigkeit von gemounteten Dateisystemen, ist ein systemd-Unit ehrlicher:

# /etc/systemd/system/appstack.service
[Unit]
Description=appstack (docker compose)
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/appstack
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
ExecReload=/usr/bin/docker compose up -d --remove-orphans
TimeoutStartSec=0

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now appstack
sudo systemctl status appstack

Der Gewinn: Ein systemctl stop appstack hält den Stack wirklich an, und ein Neustart der Maschine bringt ihn in definierter Reihenfolge zurück.

Aufräumen, aber gezielt#

# Was belegt überhaupt Platz?
docker system df -v

# Ungenutzte Images und Build-Cache, ohne Volumes anzufassen
docker image prune -a --filter "until=168h"
docker builder prune --filter "until=168h"
Nicht mit -a --volumes

docker system prune -a --volumes löscht auch benannte Volumes, die gerade zu keinem laufenden Container gehören. Ein Stack, der zum Aufräumzeitpunkt gestoppt ist, verliert damit seine Datenbank. Das ist einer der wenigen Docker-Befehle, bei denen man die Optionen einzeln lesen sollte.

Der Zustand, den man anstrebt#

  • docker compose ps zeigt alle Dienste als healthy.
  • ss -tulpn zeigt außer dem Reverse-Proxy keinen veröffentlichten Port auf 0.0.0.0.
  • Ein reboot bringt den Stack ohne Handgriff zurück.
  • Die Platte wächst nicht monoton.
  • .env und Volumes liegen im Backup, und das wurde einmal zurückgespielt.

Wie es weitergeht, wenn Images altern und Updates anstehen: Container aktuell halten.

MRMedia

Geschrieben aus dem laufenden Betrieb: MRMedia betreibt eigene Proxmox- Hosts, einen Backup-Server und Kundendienste in der EU. Fehler in einer Anleitung sind Fehler im eigenen Betrieb. Korrekturen bitte über Discord.