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.
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-pluginausschließlich aus dem offiziellen APT-Repository von Docker, damit Compose bei Sicherheitsupdates atomar zusammen mitdocker-ceaktualisiert wird. -
02 Segmentieren Sie jeden Stack in mindestens zwei getrennte Bridge-Netzwerke: ein routbares
frontend-Netzwerk für den Reverse-Proxy-Ingress und ein abgeschottetesinternal: true-Netzwerk (Air-Gapped) für Datenbanken und Caches. -
03 Erzwingen Sie explizite
deploy.resources.limitsin Ihrercompose.yaml, um zu verhindern, dass ein außer Kontrolle geratener Node.js- oder Java-Container den Linux-Kernel-OOM-Killer gegensshdoder PostgreSQL auslöst. -
04 Ersetzen Sie Klartext-Datenbankpasswörter in Umgebungsvariablen durch dateibasierte Docker
secrets:, die aufchmod 0400geschützten Root-Credential-Dateien basieren. -
05 Verketten Sie den Container-Start mit
depends_on: condition: service_healthyund 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
- 02 Schritt 2: Gehärtete /opt/stacks-Verzeichnisstruktur & Secrets-Architektur aufbauen
- 03 Schritt 3: Multi-Netzwerk-Segmentierung (frontend vs. internal: true Bridges)
- 04 Schritt 4: Cgroupv2-Ressourcenbudgets & deterministische Healthcheck-Ketten
- 05 Schritt 5: Stack-Interpolation validieren & eine systemd-Lifecycle-Unit registrieren
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.
| Merkmal / Attribut | Legacy Compose v1 (docker-compose) | Modernes Compose v2 (docker compose) |
|---|---|---|
| Laufzeitumgebung & Sprache | Python-3-Interpreter + pip-Abhängigkeiten | Statisch 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-Spezifikation | Aufgeteilt in starre v2.x- / v3.x-Schemata | Einheitliche Compose Specification (kein version:-Feld nötig) |
| cgroupv2-Ressourcenlimits | Erfordert Swarm-Modus für deploy.resources | Erzwingt deploy.resources.limits nativ im Standalone-Modus |
| Build-Engine | Alter synchroner Docker-Builder | Natives BuildKit (docker-buildx-plugin) mit parallelem Caching |
| Sicherheitswartung | End-of-Life (Abgekündigt seit 2023) | Aktiv gepflegt über das offizielle Docker-APT-Repository |
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.
# 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.
-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.
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.
# 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.
-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.
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: trueAir-Gapped-Bridge): Verbindetappmitpostgresundredis. Die Optioninternal: trueweist deniptables/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 (wie172.28.10.0/24und172.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.
# 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.
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 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.
| Komponente / Dienst | CPU-Limit (cpus) | RAM-Reservierung | Hartes RAM-Limit (memory) | Engineering-Begründung |
|---|---|---|---|---|
Linux-Host-OS, sshd, systemd-journald | 0.50 Kerne (Ungepinnt) | 768M | 1024M | Garantiert SSH-Erreichbarkeit, Netfilter-State-Tabellen und OS-Page-Cache |
Edge Reverse Proxy (caddy / traefik) | 0.75 Kerne | 128M | 384M | Verarbeitet TLS-1.3-Handshakes, HTTP/3-QUIC-UDP-Puffer und Zstd-Kompression |
Primäre Webanwendung (app) | 1.50 Kerne | 512M | 2048M | Deckelt die Heap-Allokation (z. B. NODE_OPTIONS=--max-old-space-size=1536) vor Host-OOM |
PostgreSQL 16 Datenbank (db) | 1.50 Kerne | 1024M | 3072M | Deckt shared_buffers=768MB, work_mem sowie parallele Worker unter /dev/shm ab |
Redis 7 In-Memory-Cache (redis) | 0.50 Kerne | 256M | 768M | Setzt 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.
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.
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.
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.
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 [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 ANTWORTENFunktioniert 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
- [1] Offizielle Compose Specification (Docker-Dokumentation) (https://docs.docker.com/reference/compose-file/)
- [2] Docker Compose v2 Ressourcenlimits & Secrets-Leitfaden (https://docs.docker.com/compose/how-tos/use-secrets/)
- [3] Linux-Kernel Control Group v2 (cgroupv2) Dokumentation (https://docs.kernel.org/admin-guide/cgroup-v2.html)