Zum Hauptinhalt springen

UNABHÄNGIGES WISSENSFORUM Unabhängige Hosting-Community • Keine offizielle Verbindung zur netcup GmbH.

NETCUP GUIDE
FORUM · GUTSCHEINPOOL
Tutorials Stufe: Mittelstufe Dauer: 35 Minuten // 18 Min. Lesezeit • Geprüft:

Docker Compose v2 auf Netcup installieren & gehärtete Multi-Container-Stacks architekturell aufbauen

Das veraltete, auf Python basierende docker-compose v1-Binary hat sein End-of-Life längst erreicht, erhält keinerlei Sicherheitsupdates mehr und ist inkompatibel mit modernen BuildKit-Funktionen sowie der aktuellen Compose Specification. Auf produktiven Netcup KVM VPS-Instanzen und dedizierten Root Servern setzt moderne Container-Orchestrierung ausschließlich auf das in Go geschriebene Docker Compose v2 CLI-Plugin (docker compose). Dieses Systems-Engineering-Runbook behandelt die kryptografisch signierte Installation, mehrschichtige Netzwerksegmentierung (internal: true Backend-Bridges), deterministische Startreihenfolgen über service_healthy-Probes, cgroupv2-Speicher- und CPU-Grenzwerte für 8-GB-Netcup-Knoten, dateibasierte Docker Secrets sowie systemd-Lifecycle-Units.

THREAD-AUTOR // OP Marcus Vance Lead Cloud Infrastructure Architect (EU KVM & Netzwerk-Engineering)
MOD-PRÜFUNG // CO-MAINTAINER Elena Rostova Senior Linux Systems & Security Auditor
BEITRAGSSTATUS // AKTUALISIERT Community-Standards →
MODERATOR-HINWEIS // TESTUMGEBUNG & TRANSPARENZ Verifiziert gegen docker-compose-plugin v2.29+ und Linux-Kernel 6.8 cgroupv2-Ressourcencontroller auf Netcup KVM-Instanzen (Ubuntu 24.04 LTS und Debian 12 Bookworm).

VORAUSSETZUNGEN // ANFORDERUNGEN

CHECKLISTE
  • ✓ Ein Netcup VPS (z. B. VPS 500 G12.5 oder VPS 1000 G12.5) oder Root Server (z. B. RS 1000 G12.5) mit Ubuntu 24.04 LTS oder Debian 12
  • ✓ Aus dem offiziellen Docker-APT-Repository installierte Docker Engine (docker-ce)
  • ✓ Administrativer Nicht-Root-Benutzer mit sudo-Berechtigungen und SSH-Zugang

Wichtige Erkenntnisse (Key Takeaways)

KURZFAZIT
  • 01 Installieren Sie docker-compose-plugin ausschließlich aus dem offiziellen APT-Repository von Docker, damit Compose bei Sicherheitsupdates atomar zusammen mit docker-ce aktualisiert wird.
  • 02 Segmentieren Sie jeden Stack in mindestens zwei getrennte Bridge-Netzwerke: ein routbares frontend-Netzwerk für den Reverse-Proxy-Ingress und ein abgeschottetes internal: true-Netzwerk (Air-Gapped) für Datenbanken und Caches.
  • 03 Erzwingen Sie explizite deploy.resources.limits in Ihrer compose.yaml, um zu verhindern, dass ein außer Kontrolle geratener Node.js- oder Java-Container den Linux-Kernel-OOM-Killer gegen sshd oder PostgreSQL auslöst.
  • 04 Ersetzen Sie Klartext-Datenbankpasswörter in Umgebungsvariablen durch dateibasierte Docker secrets:, die auf chmod 0400 geschützten Root-Credential-Dateien basieren.
  • 05 Verketten Sie den Container-Start mit depends_on: condition: service_healthy und aktiven CLI-Readiness-Probes (pg_isready, redis-cli ping) anstelle einer bloßen Prozess-Startreihenfolge.
📑 Inhaltsverzeichnis (5 Abschnitte) ▼
§ 01

Schritt 1: Veraltete v1-Binaries entfernen und das Docker Compose v2 CLI-Plugin installieren

Bevor Sie Docker Compose v2 auf einem Netcup VPS oder Root Server installieren, sollten Sie Ihr System zwingend auf Überreste des abgekündigten v1-Python-Skripts (/usr/local/bin/docker-compose) oder des veralteten Debian/Ubuntu-Universe-Pakets (docker-compose) überprüfen. Wenn beide Binärdateien gleichzeitig unter /usr/bin und /usr/local/bin existieren, führt dies in CI/CD-Pipelines und Cron-Wartungsskripten regelmäßig zu schwer auffindbaren Schema-Parsing-Fehlern.

Docker Compose v2 ist vollständig in Go kompiliert und integriert sich direkt in die Docker-CLI-Plugin-Architektur unter /usr/libexec/docker/cli-plugins/docker-compose. Da es ohne einen zwischengeschalteten Python-Interpreter nativ mit den containerd- und dockerd-API-Sockets kommuniziert, laufen Stack-Konvergenzen, parallele Image-Downloads (Parallel Pulls) und Log-Streams mit deutlich geringerem CPU- und RAM-Overhead ab.

Darüber hinaus arbeitet Compose v2 Hand in Hand mit docker-buildx-plugin (BuildKit). Jeder Aufruf von docker compose build nutzt automatisch paralleles Layer-Caching, sichere Build-Time-Secret-Mounts und optimierte Multi-Stage-Pipelines.

Architekturvergleich: Veraltetes docker-compose v1 vs. Modernes Docker Compose v2 Plugin
Merkmal / AttributLegacy Compose v1 (docker-compose)Modernes Compose v2 (docker compose)
Laufzeitumgebung & SprachePython-3-Interpreter + pip-AbhängigkeitenStatisch kompiliertes Go-Binary als CLI-Plugin
Binärpfad unter Ubuntu/Debian/usr/bin/docker-compose oder /usr/local/bin//usr/libexec/docker/cli-plugins/docker-compose
Schema-SpezifikationAufgeteilt in starre v2.x- / v3.x-SchemataEinheitliche Compose Specification (kein version:-Feld nötig)
cgroupv2-RessourcenlimitsErfordert Swarm-Modus für deploy.resourcesErzwingt deploy.resources.limits nativ im Standalone-Modus
Build-EngineAlter synchroner Docker-BuilderNatives BuildKit (docker-buildx-plugin) mit parallelem Caching
SicherheitswartungEnd-of-Life (Abgekündigt seit 2023)Aktiv gepflegt über das offizielle Docker-APT-Repository
Entfernen Sie den veralteten Top-Level-Schlüssel version:

In Docker Compose v2 ist die oberste Zeile version: "3.8" oder version: "3.9" in der compose.yaml offiziell obsolet und erzeugt eine Deprecation-Warnung. Compose v2 nutzt die fortlaufende Compose Specification – beginnen Sie Ihre compose.yaml immer direkt mit name: oder services:.

Offizielles APT-Repository prüfen & docker-compose-plugin installieren

Entfernen Sie zunächst kollidierende Distributionspakete, stellen Sie sicher, dass der offizielle GPG-Schlüsselbund von Docker unter /etc/apt/keyrings/docker.asc registriert ist, und installieren Sie sowohl docker-compose-plugin als auch docker-buildx-plugin. Überprüfen Sie unmittelbar nach der Installation die Binärpfade und Versionsausgaben.

Veraltete Binaries entfernen & offizielles Docker Compose v2 Plugin installieren
bash
# 1. Veraltete v1-Pakete oder manuell per curl heruntergeladene Binaries entfernen
sudo apt remove -y docker-compose || true
sudo rm -f /usr/local/bin/docker-compose

# 2. APT-Paketindex aktualisieren und Compose-v2- sowie Buildx-Plugins installieren
sudo apt update
sudo apt install -y docker-compose-plugin docker-buildx-plugin

# 3. Speicherort des Plugin-Binaries und Laufzeitversion überprüfen
ls -la /usr/libexec/docker/cli-plugins/
docker compose version
docker buildx version

Befehlserklärung: Bereinigt alte v1-Installationen restlos, installiert die offiziellen Go-basierten Compose-v2- und Buildx-CLI-Plugins über APT und validiert die bereitgestellten Binärpfade.

ERWARTETE AUSGABE
-rwxr-xr-x 1 root root 64231808 Oct 20 14:12 docker-compose
Docker Compose version v2.29.7
github.com/docker/buildx v0.17.1

SICHERHEITSHINWEIS: Installieren Sie Docker Compose niemals durch ungeprüftes Herunterladen per curl aus GitHub-Releases nach /usr/local/bin. Die Nutzung des GPG-signierten APT-Repositorys stellt sicher, dass unattended-upgrades Sicherheitslücken (CVEs) automatisch schließt.

§ 02

Schritt 2: Gehärtete /opt/stacks-Verzeichnisstruktur & Secrets-Architektur aufbauen

Unstrukturierte Container-Deployments, die über /home/ubuntu oder /root verstreut sind, werden im Betrieb schnell unwartbar und führen dazu, dass automatisierte Restic-Backups wichtige Konfigurationsdateien übersehen. Auf produktiven Netcup-Servern sollten sämtliche Docker-Compose-Workloads unter /opt/stacks/<stack-name> mit strikten POSIX-Dateiberechtigungen standardisiert werden.

Darüber hinaus ist die Übergabe hochsensibler Zugangsdaten (wie PostgreSQL-Superuser-Passwörter, JWT-Signaturschlüssel oder S3-API-Tokens) über Inline-environment:-Variablen ein gravierendes Sicherheitsrisiko: Jeder Prozess oder Benutzer mit Zugriff auf docker inspect kann sämtliche Umgebungsvariablen im Klartext aus dem Container-Metadaten-JSON auslesen. Zudem werden Umgebungsvariablen an Kindprozesse vererbt, erscheinen in /proc/<pid>/environ und landen bei unbehandelten Ausnahmen häufig ungefiltert in Crash-Dumps oder APM-Telemetrie-Logs.

Trennen Sie stattdessen unkritische Laufzeitparameter (wie Zeitzonen, Ports und Image-Tags) in die .env-Datei ab und binden Sie sensible Zugangsdaten über das Top-Level-Element secrets: ein. Zur Laufzeit hängt Docker Compose jede Secret-Datei als schreibgeschütztes In-Memory-Dateisystem (tmpfs) unter /run/secrets/<secret_name> in den Zielcontainer ein. Wenn der Container gestoppt wird, wird der tmpfs-Mount sofort aus dem RAM entfernt, während die Quell-Passwortdateien auf dem Host durch chmod 0400 (root:root) vor unbefugtem Zugriff geschützt bleiben.

/opt/stacks/core-app mit strikt abgesicherten Secret-Dateien bereitstellen
bash
# Standardisierte Verzeichnisstruktur für den Stack anlegen
sudo mkdir -p /opt/stacks/core-app/{secrets,config,data}
sudo chown -R root:docker /opt/stacks/core-app
sudo chmod 0750 /opt/stacks/core-app
sudo chmod 0700 /opt/stacks/core-app/secrets

# Kryptografisch zufällige 32-Byte-Passwörter für Postgres und JWT-Signatur generieren
openssl rand -base64 32 | tr -d '\n' | sudo tee /opt/stacks/core-app/secrets/postgres_password.txt > /dev/null
openssl rand -base64 32 | tr -d '\n' | sudo tee /opt/stacks/core-app/secrets/app_jwt_secret.txt > /dev/null
sudo chmod 0400 /opt/stacks/core-app/secrets/*.txt

# Unkritische Laufzeitkonfiguration in .env speichern (für Compose-Variableninterpolation)
sudo tee /opt/stacks/core-app/.env > /dev/null <<'EOF'
COMPOSE_PROJECT_NAME=core_app
TZ=Europe/Berlin
POSTGRES_VERSION=16.4-alpine3.20
REDIS_VERSION=7.4-alpine
APP_IMAGE=ghcr.io/example-org/core-api:1.14.2
APP_PORT=127.0.0.1:8080
EOF
sudo chmod 0600 /opt/stacks/core-app/.env
sudo ls -la /opt/stacks/core-app/secrets/ /opt/stacks/core-app/.env

Befehlserklärung: Erstellt /opt/stacks/core-app, generiert kryptografisch zufällige Secret-Dateien ohne abschließenden Zeilenumbruch (tr -d "\n"), sperrt die Secret-Dateien auf 0400 (r--------) und speichert Image-Tags sowie Loopback-Bind-Adressen in .env.

ERWARTETE AUSGABE
-rw------- 1 root docker   158 Nov  4 14:22 /opt/stacks/core-app/.env

/opt/stacks/core-app/secrets/:
total 16
drwx------ 2 root docker 4096 Nov  4 14:22 .
drwxr-x--- 5 root docker 4096 Nov  4 14:22 ..
-r-------- 1 root root     44 Nov  4 14:22 app_jwt_secret.txt
-r-------- 1 root root     44 Nov  4 14:22 postgres_password.txt

SICHERHEITSHINWEIS: Offizielle PostgreSQL-, MariaDB- und MySQL-Images unterstützen nativ die _FILE-Suffix-Konvention (z. B. POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password). Dadurch liest der Datenbankprozess das Passwort direkt aus dem In-Memory-Mount (tmpfs), ohne dass es jemals in docker inspect auftaucht.

§ 03

Schritt 3: Multi-Netzwerk-Segmentierung (frontend vs. internal: true Bridges)

Wenn Sie mehrere Dienste in einer einzigen compose.yaml ohne eigene networks:-Definition deklarieren, platziert Docker Compose jeden Container in einer gemeinsamen Standard-Bridge (<projekt>_default). Jeder Container in dieser Bridge kann jeden Port jedes anderen Containers erreichen, und alle Container besitzen standardmäßig ausgehenden NAT-Zugang ins öffentliche Internet.

Unter der Haube setzt die Linux-Netzwerk-Engine von Docker die Bridge-Isolierung über zwei dedizierte Netfilter-Ketten durch: DOCKER-ISOLATION-STAGE-1 und DOCKER-ISOLATION-STAGE-2. Pakete, die ein benutzerdefiniertes Bridge-Interface (br-<id>) verlassen und für ein anderes Bridge-Interface bestimmt sind, werden auf Kernel-Ebene sofort verworfen (DROP). Gleichzeitig betreibt Docker in jedem benutzerdefinierten Bridge-Netzwerk einen eingebetteten DNS-Resolver unter 127.0.0.11, der ausschließlich diejenigen Servicenamen auflöst, die explizit in derselben Bridge registriert sind.

Um Ihren Netcup-Stack wirksam gegen Lateral Movement und SSRF-Angriffe (Server-Side Request Forgery) zu härten, unterteilen Sie Ihre Architektur in zwei voneinander isolierte Linux-Bridge-Netzwerke:

  • ▪ frontend_net (Externe/Edge-Bridge): Wird ausschließlich zwischen Ihrem Reverse Proxy (Caddy/Traefik/Nginx) und dem Anwendungsserver (app) geteilt. Erlaubt ausgehende Internetverbindungen, falls die Webanwendung externe Webhooks oder Zahlungs-APIs aufrufen muss.
  • ▪ backend_internal (internal: true Air-Gapped-Bridge): Verbindet app mit postgres und redis. Die Option internal: true weist den iptables/nftables-Treiber von Docker an, jegliches ausgehende Default-Gateway-NAT-Routing aus diesem Subnetz komplett zu blockieren. Selbst wenn ein Angreifer eine SQL-Command-Execution in PostgreSQL oder eine Lua-Ausführung in Redis erreicht, kann der Datenbank-Container keine ausgehenden Verbindungen aufbauen, um Schadcode nachzuladen oder Daten direkt auszuleiten.
  • ▪ Explizites Subnetz-Pinning (ipam): Durch die Vergabe fester CIDR-Blöcke (wie 172.28.10.0/24 und 172.28.20.0/24) verhindern Sie, dass Docker zufällige Subnetze zuweist, die später mit WireGuard-VPN-Tunneln (10.x.x.x) oder internen Firmenrouten kollidieren.
Linux-Bridge-Isolierung & internal: true-Sperrregeln überprüfen
bash
# Nach dem Start des Stacks (Schritt 5) die erzeugten Linux-Bridges und iptables-Isolationsregeln prüfen:
docker network ls
sudo iptables -S DOCKER-ISOLATION-STAGE-1
sudo iptables -S DOCKER-USER

Befehlserklärung: Listet aktive Docker-Netzwerke auf und inspiziert die Netfilter-Isolationsketten, die die strikte Trennung zwischen benutzerdefinierten Bridges im Linux-Kernel durchsetzen.

ERWARTETE AUSGABE
NETWORK ID     NAME                          DRIVER    SCOPE
8f3a1c92b104   core_app_backend_internal     bridge    local
c41e9021a77b   core_app_frontend_net         bridge    local
-P DOCKER-ISOLATION-STAGE-1 ACCEPT
-A DOCKER-ISOLATION-STAGE-1 -i br-8f3a1c92b104 ! -o br-8f3a1c92b104 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -i br-c41e9021a77b ! -o br-c41e9021a77b -j DOCKER-ISOLATION-STAGE-2
§ 04

Schritt 4: Cgroupv2-Ressourcenbudgets & deterministische Healthcheck-Ketten

Auf einem Netcup VPS 1000 G12.5 (4 vCores, 8 GB RAM) oder einem Root Server RS 1000 G12.5 (4 dedizierte AMD EPYC-Kerne, 8 GB ECC-RAM) gehören ungedrosselte Container zu den häufigsten Ursachen für ungeplante Server-Ausfälle. Wenn eine plötzliche Lastspitze oder ein Speicherleck dazu führt, dass Ihr Anwendungscontainer 7,8 GB RAM belegt, beendet der Out-Of-Memory-(OOM)-Killer des Linux-Kernels Prozesse anhand ihres oom_score – und trifft dabei häufig postgres oder dockerd mitten in einer laufenden Transaktion.

Ubuntu 24.04 und Debian 12 nutzen die einheitliche cgroupv2-Hierarchie unter /sys/fs/cgroup. Wenn Sie deploy.resources.limits und reservations in Ihrer compose.yaml definieren, übersetzt Docker Compose v2 diese Vorgaben direkt in die Kernel-Parameter memory.max, memory.high und cpu.max. Dadurch wird jeder Speicheranstieg strikt innerhalb des verursachenden Containers abgefangen, während das Host-Betriebssystem und sshd jederzeit reaktionsfähig bleiben.

Ebenso wichtig ist die Dimensionierung des POSIX-Shared-Memory-Segments (/dev/shm) für Datenbank-Container. Docker begrenzt /dev/shm standardmäßig auf lediglich 64 MB. Sobald PostgreSQL 16 auf einem Multi-Core-Server parallele Hash-Joins oder große Bitmap-Index-Scans ausführt, überschreiten die gemeinsamen Arbeitsspeicher-Segmente diese Grenze sofort und brechen Abfragen mit ERROR: could not resize shared memory segment: No space left on device ab. Das Setzen von shm_size: 256mb zusammen mit random_page_cost=1.1 und effective_io_concurrency=200 stimmt den Datenbank-Container optimal auf das lokale NVMe-Speicher-Subsystem von Netcup ab.

Empfohlenes Container-Speicher- & CPU-Budget für einen 8 GB Netcup VPS 1000 G12.5 / RS 1000 G12.5 (4 Kerne)
Komponente / DienstCPU-Limit (cpus)RAM-ReservierungHartes RAM-Limit (memory)Engineering-Begründung
Linux-Host-OS, sshd, systemd-journald0.50 Kerne (Ungepinnt)768M1024MGarantiert SSH-Erreichbarkeit, Netfilter-State-Tabellen und OS-Page-Cache
Edge Reverse Proxy (caddy / traefik)0.75 Kerne128M384MVerarbeitet TLS-1.3-Handshakes, HTTP/3-QUIC-UDP-Puffer und Zstd-Kompression
Primäre Webanwendung (app)1.50 Kerne512M2048MDeckelt die Heap-Allokation (z. B. NODE_OPTIONS=--max-old-space-size=1536) vor Host-OOM
PostgreSQL 16 Datenbank (db)1.50 Kerne1024M3072MDeckt shared_buffers=768MB, work_mem sowie parallele Worker unter /dev/shm ab
Redis 7 In-Memory-Cache (redis)0.50 Kerne256M768MSetzt maxmemory 512mb mit allkeys-lru-Eviction innerhalb einer 768M-Obergrenze durch

Vollständige Produktions-compose.yaml mit Healthchecks, Secrets & Ressourcenlimits

Speichern Sie die folgende vollständige Spezifikation unter /opt/stacks/core-app/compose.yaml. Beachten Sie, wie der Dienst app die Direktive depends_on mit condition: service_healthy nutzt: Der Webcontainer startet erst dann, wenn PostgreSQL das WAL-Recovery abgeschlossen hat und Redis aktiv auf PING-Befehle antwortet.

/opt/stacks/core-app/compose.yaml
yaml
name: ${COMPOSE_PROJECT_NAME:-core_app}

x-logging-defaults: &default-logging
  driver: "json-file"
  options:
    max-size: "25m"
    max-file: "4"
    compress: "true"

services:
  db:
    image: postgres:${POSTGRES_VERSION:-16.4-alpine3.20}
    container_name: core_app_db
    restart: unless-stopped
    user: "70:70"
    shm_size: 256mb
    environment:
      TZ: ${TZ:-UTC}
      POSTGRES_DB: core_prod
      POSTGRES_USER: core_user
      POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
    secrets:
      - postgres_password
    command:
      - "postgres"
      - "-c"
      - "shared_buffers=768MB"
      - "-c"
      - "effective_cache_size=2048MB"
      - "-c"
      - "work_mem=16MB"
      - "-c"
      - "maintenance_work_mem=192MB"
      - "-c"
      - "random_page_cost=1.1"
      - "-c"
      - "effective_io_concurrency=200"
      - "-c"
      - "log_min_duration_statement=250"
    volumes:
      - pgdata:/var/lib/postgresql/data
    networks:
      - backend_internal
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U core_user -d core_prod -h 127.0.0.1"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 20s
    deploy:
      resources:
        limits:
          cpus: "1.50"
          memory: 3072M
        reservations:
          cpus: "0.50"
          memory: 1024M
    security_opt:
      - no-new-privileges:true
    logging: *default-logging

  redis:
    image: redis:${REDIS_VERSION:-7.4-alpine}
    container_name: core_app_redis
    restart: unless-stopped
    command:
      - "redis-server"
      - "--appendonly"
      - "yes"
      - "--appendfsync"
      - "everysec"
      - "--maxmemory"
      - "512mb"
      - "--maxmemory-policy"
      - "allkeys-lru"
      - "--save"
      - "900 1"
      - "--save"
      - "300 10"
    volumes:
      - redisdata:/data
    networks:
      - backend_internal
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 10s
    deploy:
      resources:
        limits:
          cpus: "0.50"
          memory: 768M
        reservations:
          cpus: "0.15"
          memory: 256M
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    logging: *default-logging

  app:
    image: ${APP_IMAGE:-nginx:1.27-alpine}
    container_name: core_app_web
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    ports:
      - "${APP_PORT:-127.0.0.1:8080}:80"
    environment:
      TZ: ${TZ:-UTC}
      DB_HOST: db
      DB_PORT: 5432
      DB_NAME: core_prod
      DB_USER: core_user
      DB_PASSWORD_FILE: /run/secrets/postgres_password
      JWT_SECRET_FILE: /run/secrets/app_jwt_secret
      REDIS_URL: redis://redis:6379/0
    secrets:
      - postgres_password
      - app_jwt_secret
    networks:
      - frontend_net
      - backend_internal
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:80/ > /dev/null || exit 1"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 15s
    deploy:
      resources:
        limits:
          cpus: "1.50"
          memory: 2048M
        reservations:
          cpus: "0.25"
          memory: 512M
    security_opt:
      - no-new-privileges:true
    logging: *default-logging

secrets:
  postgres_password:
    file: ./secrets/postgres_password.txt
  app_jwt_secret:
    file: ./secrets/app_jwt_secret.txt

volumes:
  pgdata:
    driver: local
  redisdata:
    driver: local

networks:
  frontend_net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.10.0/24
  backend_internal:
    driver: bridge
    internal: true
    ipam:
      config:
        - subnet: 172.28.20.0/24

Befehlserklärung: Definiert eine gehärtete 3-Tier-Compose-v2-Architektur mit YAML-Log-Rotation-Ankern (x-logging-defaults), auf Netcup-NVMe abgestimmten Postgres-Flags (random_page_cost=1.1), dateibasierten Secrets, service_healthy-Startabhängigkeiten, cgroupv2-Speicher-/CPU-Obergrenzen und einem abgeschotteten internal: true-Datenbank-Subnetz.

SICHERHEITSHINWEIS: Das Binden von ports an 127.0.0.1:8080:80 stellt sicher, dass die DOCKER-iptables-Kette von Docker den Port 8080 nicht unter Umgehung von UFW im öffentlichen Internet freigibt.

§ 05

Schritt 5: Stack-Interpolation validieren & eine systemd-Lifecycle-Unit registrieren

Führen Sie vor dem Start jeder geänderten compose.yaml stets docker compose config aus, um das vollständig interpolierte YAML zu prüfen – inklusive aller .env-Variablen, YAML-Anker (*default-logging) und relativen Secret-Dateipfade. Bei Tipp- oder Einrückungsfehlern bricht docker compose config sofort mit einer klaren Fehlermeldung ab, ohne laufende Container zu beeinträchtigen.

Während restart: unless-stopped den dockerd-Daemon anweist, einzelne Container nach einem Host-Reboot neu zu starten, bietet die Kapselung des gesamten Stacks in einer parametrisierten systemd-Service-Unit (/etc/systemd/system/compose-stack@.service) eine saubere Integration in systemctl status, journalctl-Boot-Abhängigkeiten (After=docker.service network-online.target) sowie kontrollierte docker compose stop-Hooks beim Herunterfahren.

Konfiguration validieren, Stack starten und Health-Status prüfen
bash
cd /opt/stacks/core-app

# 1. YAML-Syntax und Variableninterpolation vorab prüfen
sudo docker compose config --quiet && echo "Compose syntax OK"

# 2. Images parallel herunterladen und Stack im Hintergrund starten (wartet auf Healthchecks)
sudo docker compose up -d --wait

# 3. Laufende Container, Health-Status und aktive cgroupv2-Ressourcenlimits prüfen
sudo docker compose ps
sudo docker stats --no-stream

Befehlserklärung: Validiert die compose.yaml, startet den Stack mit --wait (blockiert, bis alle healthcheck-Probes den Status healthy melden) und überprüft die aktiven Speicherlimits in docker stats.

ERWARTETE AUSGABE
Compose syntax OK
NAME             IMAGE                       STATUS                   PORTS
core_app_db      postgres:16.4-alpine3.20    Up 28 seconds (healthy)  5432/tcp
core_app_redis   redis:7.4-alpine            Up 28 seconds (healthy)  6379/tcp
core_app_web     nginx:1.27-alpine           Up 16 seconds (healthy)  127.0.0.1:8080->80/tcp

CONTAINER ID   NAME             CPU %     MEM USAGE / LIMIT   MEM %     NET I/O
a1b2c3d4e5f6   core_app_db      0.18%     64.2MiB / 3GiB      2.09%     4.1kB / 3.2kB
b2c3d4e5f6a1   core_app_redis   0.22%     9.8MiB / 768MiB     1.28%     2.8kB / 1.9kB
c3d4e5f6a1b2   core_app_web     0.05%     14.1MiB / 2GiB      0.69%     1.5kB / 1.1kB
/etc/systemd/system/compose-stack@.service (Wiederverwendbare Template-Unit)
ini
[Unit]
Description=Docker Compose v2 Stack (%i)
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/stacks/%i
ExecStartPre=/usr/bin/docker compose config --quiet
ExecStart=/usr/bin/docker compose up -d --remove-orphans --wait
ExecReload=/usr/bin/docker compose up -d --remove-orphans --wait
ExecStop=/usr/bin/docker compose stop --timeout 30
TimeoutStartSec=300
TimeoutStopSec=60

[Install]
WantedBy=multi-user.target

Befehlserklärung: Erstellt eine wiederverwendbare parametrisierte systemd-Template-Unit (compose-stack@<ordner>.service). Aktivieren Sie sie mit sudo systemctl enable --now compose-stack@core-app.service, um jeden beliebigen Stack unter /opt/stacks/<ordner> über Standard-systemctl-Befehle zu verwalten.

Häufige Fehlerbehebung

DIAGNOSE-GUIDE

[SYMPTOM] Mit 8080:80 veröffentlichter Container-Port umgeht UFW und ist öffentlich über die Netcup-IPv4 erreichbar

Ursache: Standardmäßig fügt die Netfilter-Integration von Docker Port-Forwarding-Regeln in die DOCKER-Kette der nat- und filter-Tabellen ein, die vom Linux-Kernel vor der ufw-before-input-Kette von UFW ausgewertet werden.

Lösung: Stellen Sie Host-Port-Bindings in der compose.yaml immer die Loopback-Adresse voran (127.0.0.1:8080:80), wenn der Dienst hinter einem lokalen Reverse Proxy läuft. Binden Sie 0.0.0.0 ausschließlich an die Ports 80 und 443 Ihres Edge-Reverse-Proxys.

[SYMPTOM] dependency failed to start: container core_app_db is unhealthy während docker compose up --wait

Ursache: Die Initialisierung von PostgreSQL ist fehlgeschlagen – meist weil /run/secrets/postgres_password einen Zeilenumbruch enthielt, für UID 70 nicht lesbar war oder ein bestehendes pgdata-Volume mit einer anderen Postgres-Hauptversion erstellt wurde.

Lösung: Führen Sie sudo docker compose logs db --tail=50 und sudo docker inspect --format "{{json .State.Health}}" core_app_db aus, um den exakten Exit-Code und die Fehlerausgabe des Healthchecks auszulesen.

[SYMPTOM] Pool overlaps with other one on this address space beim Starten eines neuen Compose-Stacks

Ursache: Das in compose.yaml definierte ipam.config.subnet-CIDR kollidiert mit einem bereits existierenden Docker-Bridge-Netzwerk oder dem Standard-Adresspool von Docker (172.17.0.0/16–172.31.0.0/16).

Lösung: Prüfen Sie bestehende Zuweisungen mit docker network inspect $(docker network ls -q) | grep -E "Name|Subnet" und weisen Sie dem neuen Stack ein überschneidungsfreies /24-Subnetz zu.

[SYMPTOM] Container beendet sich unter hoher Last sofort mit Exit-Code 137

Ursache: Exit-Code 137 (128 + 9 SIGKILL) zeigt an, dass der cgroupv2-Speichercontroller den Container beendet hat, weil sein RSS-Speicherverbrauch den Wert deploy.resources.limits.memory überschritten hat.

Lösung: Verifizieren Sie das OOM-Ereignis mit sudo dmesg -T | grep -i "oom-kill" und docker inspect core_app_web --format "{{.State.OOMKilled}}". Begrenzen Sie entweder die Heap-Größe Ihrer Anwendung (z. B. über NODE_OPTIONS oder JVM -Xmx) auf ca. 75 % des Container-Limits oder erhöhen Sie deploy.resources.limits.memory in der compose.yaml.

[SYMPTOM] Container können während docker compose build keine externen Domains oder APT-Mirrors auflösen

Ursache: Netcup-KVM-Instanzen mit systemd-resolved binden einen Stub-Listener an 127.0.0.53, wodurch Docker-Container in benutzerdefinierten Bridge-Netzwerken oder mit abweichender MTU bei UDP-DNS-Abfragen fehlschlagen können.

Lösung: Tragen Sie explizite rekursive DNS-Server (46.38.225.230, 1.1.1.1, 9.9.9.9) in /etc/docker/daemon.json unter "dns": ["46.38.225.230", "1.1.1.1"] ein und starten Sie den Daemon mit sudo systemctl restart docker neu.

Häufig gestellte Fragen (FAQ)

GEPRÜFTE ANTWORTEN

Funktioniert deploy.resources.limits in Docker Compose v2 auch ohne aktivierten Docker-Swarm-Modus?

Ja. Während das alte docker-compose v1 den deploy:-Block außerhalb des Swarm-Modus ignorierte (sofern nicht --compatibility übergeben wurde), setzt Docker Compose v2 sowohl deploy.resources.limits als auch reservations (cpus und memory) auf einzelnen Linux-Hosts nativ über cgroupv2 durch.

Wo werden benannte Docker-Volumes (Named Volumes) physisch auf einem Netcup VPS oder Root Server gespeichert?

Benannte Volumes, die unter dem Top-Level-Schlüssel volumes: definiert sind, liegen unter /var/lib/docker/volumes/<projekt>_<volume_name>/_data auf Ihrem Netcup-Root-NVMe/SSD-Dateisystem. Im Gegensatz zu Bind-Mounts initialisiert Docker beim ersten Start eines Containers automatisch die korrekten Dateiberechtigungen auf einem neuen Named Volume.

Wie aktualisiere ich das Image eines einzelnen Dienstes in einem Multi-Container-Stack, ohne die Datenbank zu stoppen?

Aktualisieren Sie den Image-Tag in /opt/stacks/core-app/.env und führen Sie anschließend sudo docker compose pull app && sudo docker compose up -d --no-deps app aus. Das Flag --no-deps erstellt ausschließlich den app-Container neu, während db und redis ohne jede Unterbrechung weiterlaufen.

Warum sollte ich bei PostgreSQL-Containern shm_size: 256mb setzen?

Standardmäßig bindet Docker /dev/shm in jedem Container mit einer fest codierten Obergrenze von nur 64 MB ein. Wenn PostgreSQL 16 auf einem Netcup RS 1000 G12.5 oder RS 2000 G12.5 komplexe analytische Abfragen oder parallele Hash-Joins über mehrere vCores ausführt, führt das Überschreiten dieser 64 MB sofort zum Fehler ERROR: could not resize shared memory segment: No space left on device. Das Setzen von shm_size: 256mb (oder 512mb) beseitigt diesen Flaschenhals vollständig.

QUELLEN // GEPRÜFTE TECHNISCHE DOKUMENTATION

WEITERFÜHREND // EMPFOHLENE ARCHITEKTUR-AUDITS

Gutscheine →