Skip to main content

COMMUNITY KNOWLEDGE FORUM Independent Hosting Community • Not affiliated with netcup GmbH.

NETCUP GUIDE
FORUM · LIVE VOUCHER POOL
Tutorials LEVEL: Intermediate TIME: 35 minutes // 18 MIN READ • VERIFIED:

How to Install Docker Compose v2 & Architect Hardened Multi-Container Stacks on Netcup

The legacy Python-based docker-compose v1 binary is end-of-life, unmaintained, and incompatible with modern BuildKit and Compose Specification features. On Netcup KVM VPS and Root Server instances, production container orchestration relies on the Go-based Docker Compose v2 CLI plugin (docker compose). This systems engineering runbook covers verified binary installation, multi-tier network segmentation (internal: true backend bridges), deterministic startup ordering via service_healthy probes, cgroupv2 memory and CPU enforcement tailored for 8 GB Netcup nodes, file-based Docker secrets, and systemd unit wrappers.

THREAD OP // AUTHOR Marcus Vance Principal Systems Architect
MOD VERIFIED // CO-MAINTAINER Elena Rostova Senior Network & Storage Verification Editor
THREAD STATUS // UPDATED Community Standards →
MODERATOR NOTE // TEST ENVIRONMENT & TRANSPARENCY Verified against docker-compose-plugin v2.29+ and Linux kernel 6.8 cgroupv2 resource controllers on Netcup KVM instances (Ubuntu 24.04 LTS and Debian 12 Bookworm).

PREREQUISITES // ENVIRONMENT REQUIREMENTS

CHECKLIST
  • ✓ A Netcup KVM VPS (such as a 4 GB VPS 500 G12.5 or 8 GB VPS 1000 G12.5) or Root Server (such as an 8 GB RS 1000 G12.5) running Ubuntu 24.04 LTS or Debian 12
  • ✓ Docker Engine (docker-ce) installed from the official Docker APT repository
  • ✓ Non-root administrative user with sudo privileges and SSH key authentication

Key Takeaways

EXECUTIVE BRIEFING
  • 01 Install docker-compose-plugin exclusively from Docker’s official APT repository so Compose updates atomically alongside docker-ce during security upgrades.
  • 02 Segment every stack into at least two bridge networks: a routable frontend network for reverse-proxy ingress and an internal: true air-gapped network for databases and caches.
  • 03 Enforce explicit deploy.resources.limits in compose.yaml to prevent a runaway Node.js or Java container from triggering the Linux kernel OOM killer against sshd or PostgreSQL.
  • 04 Replace plaintext database passwords in environment variables with file-mounted Docker secrets: backed by chmod 0400 root-owned credential files.
  • 05 Chain container startup with depends_on: condition: service_healthy and active CLI readiness probes (pg_isready, redis-cli ping) rather than naive container boot order.
📑 Quick Jump Table of Contents (5 Sections) ▼
§ 01

Step 1: Purge Legacy v1 Binaries and Install the Docker Compose v2 CLI Plugin

Before installing Docker Compose v2 on a Netcup VPS or Root Server, audit your host for remnants of the deprecated v1 Python script (/usr/local/bin/docker-compose) or Debian/Ubuntu’s outdated universe package (docker-compose). Leaving both binaries on /usr/bin and /usr/local/bin causes subtle schema parsing failures in CI/CD pipelines and cron maintenance jobs when $PATH precedence varies between interactive shells and non-interactive automation.

Docker Compose v2 is compiled in Go and integrates directly into the Docker CLI plugin architecture under /usr/libexec/docker/cli-plugins/docker-compose. Because it communicates natively with the containerd and dockerd API sockets without an intermediate Python wrapper, stack convergence, parallel image pulls, and real-time log streaming execute with significantly lower CPU and memory overhead.

Additionally, Compose v2 delegates image builds directly to docker-buildx-plugin (BuildKit). When you run docker compose build on a multi-core Netcup instance, BuildKit executes independent Dockerfile stages concurrently, mounts build-time secrets into ephemeral tmpfs paths without baking credentials into intermediate image layers, and persists deterministic build caches across deployments.

Architectural Comparison: Legacy docker-compose v1 vs Modern Docker Compose v2 Plugin
Feature / AttributeLegacy Compose v1 (docker-compose)Modern Compose v2 (docker compose)
Runtime & LanguagePython 3 interpreter + pip dependenciesStatically compiled Go binary CLI plugin
Binary Path on Ubuntu/Debian/usr/bin/docker-compose or /usr/local/bin//usr/libexec/docker/cli-plugins/docker-compose
Schema SpecificationLocked to legacy v2.x / v3.x schema splitsUnified Compose Specification (no version: field required)
cgroupv2 Resource LimitsRequires Swarm mode for deploy.resourcesNatively enforces deploy.resources.limits in standalone mode
Build EngineLegacy synchronous Docker builderNative BuildKit (docker-buildx-plugin) with parallel layer caching
Security MaintenanceEnd-of-Life (Deprecated since 2023)Actively maintained via official Docker APT repository
Drop the Obsolete version: Top-Level Key

In Docker Compose v2, the top-level version: "3.8" or version: "3.9" line in compose.yaml is officially obsolete and triggers a deprecation warning. Compose v2 uses the rolling Compose Specification—always start your compose.yaml directly with name: or services:.

Verify Official APT Repository & Install docker-compose-plugin

Remove any conflicting distribution packages, ensure Docker’s official GPG keyring is registered in /etc/apt/keyrings/docker.asc, and install both docker-compose-plugin and docker-buildx-plugin. Verify the binary permissions and runtime version output immediately after installation.

Remove Legacy Binaries & Install Official Docker Compose v2 Plugin
bash
# 1. Remove any legacy v1 packages or manual curl binaries
sudo apt remove -y docker-compose || true
sudo rm -f /usr/local/bin/docker-compose

# 2. Update APT index and install Compose v2 + Buildx plugins
sudo apt update
sudo apt install -y docker-compose-plugin docker-buildx-plugin

# 3. Verify plugin binary location and runtime version
ls -la /usr/libexec/docker/cli-plugins/
docker compose version
docker buildx version

Command Explanation: Purges legacy v1 installations, installs the official Go-based Compose v2 and Buildx CLI plugins via APT, and validates the installed binary paths.

EXPECTED OUTPUT
-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

SECURITY NOTE: Never install Docker Compose by piping an unverified curl command from GitHub Releases into /usr/local/bin. Using the signed APT repository ensures unattended security upgrades (unattended-upgrades) patch CVE vulnerabilities automatically.

§ 02

Step 2: Establish a Hardened /opt/stacks Directory & Secrets Hierarchy

Ad-hoc container deployments scattered across /home/ubuntu or /root quickly become unmaintainable and break automated Restic backup scripts. On production Netcup servers, standardize all Docker Compose workloads under /opt/stacks/<stack-name> with strict POSIX ownership (root:docker, 0750) so both interactive administrators and systemd service units know exactly where every stack resides.

Furthermore, passing high-value credentials (such as PostgreSQL superuser passwords, JWT signing keys, or S3 API tokens) via inline environment: variables is a common security anti-pattern. Any process or user with access to the Docker socket can run docker inspect to read plaintext environment variables from the container metadata JSON, and crash dumps frequently serialize process.env or /proc/self/environ directly into external log aggregators.

Instead, separate non-sensitive runtime configuration (such as image tags, timezones, and loopback ports) into .env for YAML interpolation, and mount sensitive credentials as read-only files under /run/secrets/ using the Compose secrets: top-level element. When you generate secret files with openssl rand -base64 32 | tr -d "\n", stripping the trailing newline prevents subtle authentication failures in services that read the raw byte stream of the mounted secret file.

Provision /opt/stacks/core-app with Locked-Down Secret Files
bash
# Create standardized directory structure for the stack
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

# Generate cryptographically random 32-byte passwords for Postgres and JWT
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

# Create non-secret runtime configuration in .env (for Compose interpolation)
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/.env /opt/stacks/core-app/secrets/

Command Explanation: Establishes /opt/stacks/core-app, generates CSPRNG secret files without trailing newlines (tr -d "\n"), locks secret files to 0400 (r--------), and verifies file permissions on disk.

EXPECTED OUTPUT
-rw------- 1 root docker  148 Oct 24 14:22 /opt/stacks/core-app/.env

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

SECURITY NOTE: Official PostgreSQL, MariaDB, and MySQL images natively support the _FILE suffix convention (e.g., POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password), reading the secret directly from the mount without exposing it in docker inspect.

§ 03

Step 3: Multi-Network Segmentation (frontend vs internal: true Bridges)

When you define multiple services in a single compose.yaml without custom networks:, Docker Compose places every container onto a single shared default bridge (<project>_default). Every container on that bridge can reach every TCP and UDP port of every other container via internal DNS (127.0.0.11), and every container has unrestricted outbound NAT access to the public internet.

To harden your Netcup stack against lateral movement and Server-Side Request Forgery (SSRF) exploits, partition your architecture into two isolated Linux bridge networks with explicit IPAM subnet blocks. Under the hood, Docker programmes the Linux kernel netfilter DOCKER-ISOLATION-STAGE-1 and DOCKER-ISOLATION-STAGE-2 chains to drop any packet attempting to cross between frontend_net and backend_internal unless a container is explicitly attached to both bridges.

  • ▪ frontend_net (External/Edge Bridge): Shared only between your reverse proxy (Caddy/Traefik/Nginx) and the upstream application server (app). Allows outbound internet connectivity if the application calls external webhooks or APIs.
  • ▪ backend_internal (internal: true Air-Gapped Bridge): Connects app to postgres and redis. Setting internal: true instructs Docker’s iptables/nftables driver to omit outbound MASQUERADE NAT rules and insert explicit DROP rules for non-local traffic on this bridge. Even if an attacker achieves SQL command execution inside PostgreSQL or Lua execution inside Redis, the database container cannot initiate outbound connections to download payloads or exfiltrate data.
  • ▪ Explicit Subnet Pinning (ipam): Defining explicit CIDR blocks (such as 172.28.10.0/24 and 172.28.20.0/24) prevents Docker from randomly allocating subnets that collide with WireGuard VPN tunnels (10.x.x.x) or corporate RFC1918 routes.
Verify Linux Bridge Isolation & internal: true Drop Rules
bash
# After starting the stack (Step 5), inspect generated Linux bridges and iptables isolation rules:
docker network ls
sudo iptables -S DOCKER-ISOLATION-STAGE-1
sudo iptables -S DOCKER-USER

Command Explanation: Lists active Docker networks and inspects the netfilter isolation chains that enforce traffic separation between custom bridges.

EXPECTED OUTPUT
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 DROP
-A DOCKER-ISOLATION-STAGE-1 -i br-c41e9021a77b ! -o br-c41e9021a77b -j DOCKER-ISOLATION-STAGE-2
§ 04

Step 4: Cgroupv2 Resource Budgets & Deterministic Healthcheck Chains

On an 8 GB Netcup VPS 1000 G12.5 (4 shared vCores) or Root Server RS 1000 G12.5 (4 dedicated AMD EPYC cores, 8 GB ECC RAM), running unthrottled containers is the most common cause of unplanned host lockups. If a sudden traffic spike or an unbounded memory leak causes a Node.js, Python, or Java application container to allocate 7.8 GB of RAM, the Linux kernel Out-Of-Memory (OOM) killer is forced to free memory globally based on /proc/<pid>/oom_score—frequently terminating postgres or dockerd mid-transaction.

Ubuntu 24.04 and Debian 12 use the unified cgroupv2 hierarchy mounted at /sys/fs/cgroup. When you declare deploy.resources.limits and reservations inside compose.yaml, Docker Compose v2 translates those limits directly into cgroupv2 memory.max, memory.low, and cpu.max controller files under /sys/fs/cgroup/system.slice/docker-<id>.scope/. Any memory spike is contained strictly inside that container’s cgroup slice, leaving at least 1 GB of uncontested physical RAM for the Linux kernel page cache, systemd-journald, and sshd.

Recommended Container Memory & CPU Allocation Budget for an 8 GB Netcup VPS 1000 G12.5 / RS 1000 G12.5 (4 Cores)
Component / ServiceCPU Limit (cpus)Memory ReservationHard Memory Limit (memory)Engineering Rationale
Linux Host OS, sshd, systemd-journald0.50 Core (Unpinned)768M1024MGuarantees SSH responsiveness, netfilter state tables, and OS page cache
Edge Reverse Proxy (caddy / traefik)0.75 Core128M384MHandles TLS 1.3 handshakes, HTTP/3 QUIC UDP buffers, and Zstd compression
Primary Web Application (app)1.50 Cores512M2048MCaps heap allocation (e.g., NODE_OPTIONS=--max-old-space-size=1536) before host OOM
PostgreSQL 16 Database (db)1.50 Cores1024M3072MAccommodates shared_buffers=768MB, work_mem, and /dev/shm parallel workers
Redis 7 In-Memory Cache (redis)0.50 Core256M768MEnforces maxmemory 512mb with allkeys-lru eviction inside a 768M container ceiling

Complete Production compose.yaml with Healthchecks, Secrets & Resource Limits

Save the following complete specification to /opt/stacks/core-app/compose.yaml. Notice how app uses depends_on with condition: service_healthy rather than the default service_started. Without service_healthy, Docker starts app the millisecond the postgres container process spawns—often while PostgreSQL is still replaying WAL segments or running initdb scripts, causing your application’s connection pool to crash on boot.

/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

Command Explanation: Defines a hardened 3-tier Compose v2 architecture featuring YAML log-rotation anchors (x-logging-defaults), NVMe-tuned Postgres flags (random_page_cost=1.1), file-based secrets, service_healthy startup dependencies, cgroupv2 memory/CPU ceilings, and an air-gapped internal: true database subnet.

SECURITY NOTE: Binding ports to 127.0.0.1:8080:80 ensures Docker’s DOCKER iptables chain does not expose port 8080 to the public internet, bypassing UFW.

§ 05

Step 5: Validate Stack Interpolation & Register a systemd Lifecycle Unit

Before launching any modified compose.yaml, run docker compose config to render the fully interpolated YAML—resolving all .env variables, YAML anchors (*default-logging), and relative secret paths. If there is a syntax or indentation error, or if a referenced secret file is missing on disk, docker compose config --quiet catches it immediately without disrupting running containers.

When starting the stack in production scripts, pass docker compose up -d --wait. Unlike a basic up -d (which returns exit code 0 as soon as containers are created), the --wait flag blocks the CLI until every service in the dependency graph passes its healthcheck probe—returning a non-zero exit code to your deployment script if any container enters an unhealthy or crash-loop state.

Finally, while restart: unless-stopped instructs dockerd to restart individual containers after a host reboot, wrapping the entire stack in a parameterized systemd template unit (/etc/systemd/system/compose-stack@.service) gives systems administrators native integration with systemctl status, journalctl boot ordering (After=docker.service network-online.target), and clean pre-shutdown docker compose stop --timeout 30 hooks so PostgreSQL flushes dirty buffers before the VM powers off.

Validate Configuration, Launch Stack, and Inspect Health Status
bash
cd /opt/stacks/core-app

# 1. Validate YAML syntax and variable interpolation
sudo docker compose config --quiet && echo "Compose syntax OK"

# 2. Pull images in parallel and start stack in detached mode, waiting for healthchecks
sudo docker compose up -d --wait

# 3. Inspect running containers, health status, and live cgroupv2 resource limits
sudo docker compose ps
sudo docker stats --no-stream

Command Explanation: Validates compose.yaml, starts the stack with --wait (which blocks until all healthcheck probes report healthy), and verifies the enforced memory ceilings in docker stats.

EXPECTED OUTPUT
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 (Reusable 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

Command Explanation: Creates a reusable parameterized systemd template unit (compose-stack@<folder>.service). Enable it with sudo systemctl enable --now compose-stack@core-app.service to manage any stack in /opt/stacks/<folder> via standard systemctl commands.

Common Troubleshooting Scenarios

DIAGNOSTIC RUNBOOK

[SYMPTOM] Container Port Published with 8080:80 Bypasses UFW and Is Publicly Accessible on Netcup IPv4

Root Cause: By default, Docker’s netfilter integration inserts port-forwarding rules into the DOCKER chain in the nat and filter tables, which Linux evaluates before UFW’s ufw-before-input chain.

Resolution: Always prefix host port bindings with the loopback address (127.0.0.1:8080:80) in compose.yaml when the service sits behind a local reverse proxy. Only bind 0.0.0.0 on ports 80 and 443 of your edge reverse proxy.

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

Root Cause: PostgreSQL failed initialization—most commonly because /run/secrets/postgres_password contained a trailing newline or was unreadable by UID 70, or an existing pgdata volume was initialized with a different major Postgres version.

Resolution: Run sudo docker compose logs db --tail=50 and sudo docker inspect --format "{{json .State.Health}}" core_app_db to read the exact healthcheck exit code and stderr output.

[SYMPTOM] Pool overlaps with other one on this address space When Starting a New Compose Stack

Root Cause: The ipam.config.subnet CIDR in compose.yaml collides with an existing Docker bridge network or Docker’s default address pool (172.17.0.0/16–172.31.0.0/16).

Resolution: Inspect existing allocations with docker network inspect $(docker network ls -q) | grep -E "Name|Subnet" and assign a non-overlapping /24 subnet to the new stack.

[SYMPTOM] Container Exits Immediately with Exit Code 137 Under Heavy Load

Root Cause: Exit code 137 (128 + 9 SIGKILL) indicates the cgroupv2 memory controller killed the container because its RSS memory exceeded deploy.resources.limits.memory.

Resolution: Verify the OOM event with sudo dmesg -T | grep -i "oom-kill" and docker inspect core_app_web --format "{{.State.OOMKilled}}". Either tune your application’s heap size (e.g., NODE_OPTIONS or JVM -Xmx) to stay 25% below the container limit or raise deploy.resources.limits.memory in compose.yaml.

[SYMPTOM] Containers Cannot Resolve External Domains or APT Mirrors During docker compose build

Root Cause: Netcup KVM instances using systemd-resolved bind a stub listener to 127.0.0.53, causing Docker containers on custom bridge networks or differing MTUs to fail UDP DNS lookups.

Resolution: Configure explicit recursive DNS servers (46.38.225.230, 1.1.1.1, 9.9.9.9) in /etc/docker/daemon.json under "dns": ["46.38.225.230", "1.1.1.1"] and run sudo systemctl restart docker.

Frequently Asked Questions

VERIFIED Q&A

Does deploy.resources.limits work without Docker Swarm mode in Docker Compose v2?

Yes. While legacy docker-compose v1 ignored the deploy: block outside of Swarm mode unless --compatibility was passed, Docker Compose v2 natively enforces deploy.resources.limits and reservations (cpus and memory) on standalone Linux hosts via cgroupv2.

Where are named Docker volumes physically stored on a Netcup VPS or Root Server?

Named volumes defined under the top-level volumes: key are stored in /var/lib/docker/volumes/<project>_<volume_name>/_data on your Netcup root NVMe/SSD filesystem. Unlike bind mounts, Docker automatically initializes file ownership on named volumes when a new container starts.

How do I update a single service image in a multi-container stack without stopping the database?

Update the image tag in /opt/stacks/core-app/.env, then run sudo docker compose pull app && sudo docker compose up -d --no-deps app. The --no-deps flag recreates only the app container while leaving db and redis running uninterrupted.

Why should I set shm_size: 256mb on PostgreSQL containers?

By default, Docker mounts /dev/shm inside every container with a hardcoded 64 MB ceiling. When PostgreSQL 16 executes complex analytical queries or parallel hash joins across multiple vCores on a Netcup RS 1000 G12.5 or RS 2000 G12.5, exceeding 64 MB of shared memory causes ERROR: could not resize shared memory segment: No space left on device. Setting shm_size: 256mb (or 512mb) eliminates this bottleneck.

REFERENCES // VERIFIED TECHNICAL SOURCES

FURTHER READING // ARCHITECTURE AUDITS & GUIDES

Active Coupons →