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.
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
sudoprivileges and SSH key authentication
Key Takeaways
EXECUTIVE BRIEFING-
01 Install
docker-compose-pluginexclusively from Docker’s official APT repository so Compose updates atomically alongsidedocker-ceduring security upgrades. -
02 Segment every stack into at least two bridge networks: a routable
frontendnetwork for reverse-proxy ingress and aninternal: trueair-gapped network for databases and caches. -
03 Enforce explicit
deploy.resources.limitsincompose.yamlto prevent a runaway Node.js or Java container from triggering the Linux kernel OOM killer againstsshdor PostgreSQL. -
04 Replace plaintext database passwords in environment variables with file-mounted Docker
secrets:backed bychmod 0400root-owned credential files. -
05 Chain container startup with
depends_on: condition: service_healthyand 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
- 02 Step 2: Establish a Hardened /opt/stacks Directory & Secrets Hierarchy
- 03 Step 3: Multi-Network Segmentation (frontend vs internal: true Bridges)
- 04 Step 4: Cgroupv2 Resource Budgets & Deterministic Healthcheck Chains
- 05 Step 5: Validate Stack Interpolation & Register a systemd Lifecycle Unit
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.
| Feature / Attribute | Legacy Compose v1 (docker-compose) | Modern Compose v2 (docker compose) |
|---|---|---|
| Runtime & Language | Python 3 interpreter + pip dependencies | Statically 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 Specification | Locked to legacy v2.x / v3.x schema splits | Unified Compose Specification (no version: field required) |
| cgroupv2 Resource Limits | Requires Swarm mode for deploy.resources | Natively enforces deploy.resources.limits in standalone mode |
| Build Engine | Legacy synchronous Docker builder | Native BuildKit (docker-buildx-plugin) with parallel layer caching |
| Security Maintenance | End-of-Life (Deprecated since 2023) | Actively maintained via official Docker APT repository |
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.
# 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.
-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.
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.
# 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.
-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.
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: trueAir-Gapped Bridge): Connectsapptopostgresandredis. Settinginternal: trueinstructs Docker’siptables/nftablesdriver to omit outboundMASQUERADENAT rules and insert explicitDROPrules 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 as172.28.10.0/24and172.28.20.0/24) prevents Docker from randomly allocating subnets that collide with WireGuard VPN tunnels (10.x.x.x) or corporate RFC1918 routes.
# 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.
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 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.
| Component / Service | CPU Limit (cpus) | Memory Reservation | Hard Memory Limit (memory) | Engineering Rationale |
|---|---|---|---|---|
Linux Host OS, sshd, systemd-journald | 0.50 Core (Unpinned) | 768M | 1024M | Guarantees SSH responsiveness, netfilter state tables, and OS page cache |
Edge Reverse Proxy (caddy / traefik) | 0.75 Core | 128M | 384M | Handles TLS 1.3 handshakes, HTTP/3 QUIC UDP buffers, and Zstd compression |
Primary Web Application (app) | 1.50 Cores | 512M | 2048M | Caps heap allocation (e.g., NODE_OPTIONS=--max-old-space-size=1536) before host OOM |
PostgreSQL 16 Database (db) | 1.50 Cores | 1024M | 3072M | Accommodates shared_buffers=768MB, work_mem, and /dev/shm parallel workers |
Redis 7 In-Memory Cache (redis) | 0.50 Core | 256M | 768M | Enforces 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.
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.
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.
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.
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 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&ADoes 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
- [1] Official Compose Specification (Docker Docs) (https://docs.docker.com/reference/compose-file/)
- [2] Docker Compose v2 Resource Constraints & Cgroupv2 Guide (https://docs.docker.com/compose/how-tos/use-secrets/)
- [3] Linux Kernel Control Group v2 (cgroupv2) Documentation (https://docs.kernel.org/admin-guide/cgroup-v2.html)