Skip to main content

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

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

How to Install Docker Engine on a Netcup VPS or Root Server (And Fix the UFW Firewall Bypass)

Because every Netcup VPS and Root Server runs on full QEMU/KVM hardware virtualization rather than shared-kernel LXC/OpenVZ containers, Docker Engine runs at native speed with zero cgroupv2 or overlay2 restrictions. However, simply running apt install docker.io leaves you with outdated distribution packages, unbounded container JSON logs that will eventually fill your SSD/NVMe disk, and a dangerous security flaw where Docker silently bypasses UFW firewall rules. This guide installs the official upstream Docker CE suite and hardens both daemon logging and network port binding.

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 official Docker CE (docker-ce v27+) packages, overlay2 storage driver on ext4/XFS, and Netcup SCP edge firewall rules.

PREREQUISITES // ENVIRONMENT REQUIREMENTS

CHECKLIST
  • ✓ A Netcup VPS or Root Server running Ubuntu 24.04 LTS or Debian 12 with at least 2 GB ECC RAM
  • ✓ A non-root sudo user configured with SSH access

Key Takeaways

EXECUTIVE BRIEFING
  • 01 Always install docker-ce, docker-ce-cli, containerd.io, and docker-compose-plugin from Docker’s official cryptographic APT repository rather than Ubuntu’s stale docker.io package or Snap.
  • 02 Configure /etc/docker/daemon.json with max-size: "10m", max-file: "3", and live-restore: true so container stdout logs never exhaust your disk and containers keep running during Docker daemon upgrades.
  • 03 Critical Security Rule: Publishing a container port with -p 5432:5432 injects rules into the PREROUTING/DOCKER netfilter chains BEFORE UFW—exposing your database to the entire internet! Always bind internal ports to 127.0.0.1:5432:5432 and enforce the Netcup SCP Edge Firewall.
📑 Quick Jump Table of Contents (3 Sections) ▼
§ 01

Step 1: Remove Legacy Packages & Add the Official Docker GPG Repository

Every Netcup VPS and Root Server is provisioned as a full QEMU/KVM hardware virtual machine with its own isolated Linux 6.8+ kernel, dedicated cgroupv2 hierarchy, and native overlay2 union filesystem support. Unlike shared-kernel LXC or OpenVZ virtual servers—where nested mount namespaces, custom sysctl network tunables, or ip_tables kernel modules frequently fail—KVM virtualization guarantees that dockerd and containerd operate identically to bare-metal hardware.

Before adding Docker’s upstream repository, purge any conflicting distribution-packaged container runtimes (docker.io, podman-docker, containerd, or the legacy Python v1 docker-compose script) and verify that the Ubuntu docker Snap package is not installed. Snap-packaged Docker runs inside a confined AppArmor mount namespace that breaks bind-mounts outside /home or /opt and triggers unscheduled daemon restarts whenever snapd refreshes.

Next, register Docker’s official GPG key inside /etc/apt/keyrings/docker.asc with world-readable 0644 permissions (chmod a+r) and configure the signed APT source list. Sourcing /etc/os-release variables ($ID and $VERSION_CODENAME) directly in the shell command ensures the exact repository stanza is generated cleanly whether your Netcup KVM node runs Ubuntu 24.04 LTS (noble) or Debian 12 (bookworm).

Finally, install docker-ce, docker-ce-cli, containerd.io, docker-buildx-plugin (which replaces the deprecated legacy builder with concurrent BuildKit caching), and docker-compose-plugin (which provides the native Go docker compose v2 subcommand). On multi-container hosts like a 4 GB VPS 500 G12.5 or 8 GB RS 1000 G12.5, this modern stack also lets you enforce per-container memory (mem_limit) and CPU (cpus) ceilings via cgroupv2.

Configure Official Docker CE APT Repository & Install Docker Engine + Compose v2
bash
# 1. Remove any conflicting distro packages
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do
  sudo apt-get remove -y $pkg 2>/dev/null || true
done

# 2. Install prerequisites and register Docker's official GPG key
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
. /etc/os-release
sudo curl -fsSL "https://download.docker.com/linux/${ID}/gpg" -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# 3. Add the official Docker APT repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 4. Install Docker Engine, CLI, containerd, Buildx, and Compose v2
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
docker --version && docker compose version

Command Explanation: Automatically detects whether your Netcup server runs ubuntu or debian via /etc/os-release, configures the signed APT source, and installs the complete modern Docker Engine and Compose v2 toolchain.

EXPECTED OUTPUT
Docker version 27.3.1, build ce12230
Docker Compose version v2.29.7
§ 02

Step 2: Harden /etc/docker/daemon.json (Log Rotation, Live Restore & IPAM Pools)

By default, Docker’s json-file logging driver retains every line written to stdout and stderr indefinitely. On a busy production Nginx, Traefik, or Node.js container running on a Netcup VPS or Root Server, unrotated container logs stored under /var/lib/docker/containers/<container-id>/ can quietly accumulate 40+ GB of JSON text over a few months and crash PostgreSQL or MariaDB with ENOSPC (No space left on device). Setting "max-size": "10m" and "max-file": "3" with "compress": "true" caps each container’s log footprint at 30 MB.

Enabling "live-restore": true decouples running container processes (containerd-shim) from the management daemon (dockerd). When apt unattended-upgrades patches docker-ce overnight and restarts docker.service, your database, cache, and web containers remain up and serving traffic without dropping active TCP connections.

Setting "userland-proxy": false disables the legacy docker-proxy userland executable that Docker otherwise spawns for every single published port. Relying purely on Linux kernel netfilter hairpin NAT eliminates redundant user-space packet copying and saves 30–80 MB of RAM across multi-container Compose stacks.

Lastly, defining "default-address-pools" (172.20.0.0/14 carved into /24 subnets) prevents Docker from allocating massive /16 blocks (172.17.0.0/16 through 172.31.0.0/16) every time you run docker compose up, avoiding routing table collisions with corporate WireGuard, Tailscale, or OpenVPN tunnels.

Write Production /etc/docker/daemon.json & Restart Docker
bash
cat << 'EOF' | sudo tee /etc/docker/daemon.json
{
  "storage-driver": "overlay2",
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3",
    "compress": "true"
  },
  "live-restore": true,
  "userland-proxy": false,
  "default-address-pools": [
    {
      "base": "172.20.0.0/14",
      "size": 24
    }
  ]
}
EOF

sudo systemctl daemon-reload
sudo systemctl restart docker
sudo docker info | grep -E "Storage Driver|Logging Driver|Live Restore"

Command Explanation: Caps per-container logs at 30 MB total (3 × 10m), disables the memory-hungry docker-proxy userland process in favor of native kernel hairpin NAT ("userland-proxy": false), enables live-restore, and defines clean /24 bridge subnets.

EXPECTED OUTPUT
 Storage Driver: overlay2
 Logging Driver: json-file
 Live Restore Enabled: true
§ 03

Step 3: Prevent the Notorious Docker vs UFW Firewall Bypass on Netcup

This is the most common security vulnerability on self-hosted Linux servers: when you enable ufw (ufw default deny incoming) and then start a container with ports: - "5432:5432" or - "8080:8080", Docker modifies the kernel PREROUTING nat table and routes traffic through the FORWARD filter chain before UFW’s ufw-before-input (INPUT chain) is ever evaluated. As a result, port 5432 or 8080 becomes reachable from the entire public internet despite sudo ufw status reporting only ports 22, 80, and 443 as open!

Why does the Linux kernel process packets this way? When an external packet arrives on eth0 destined for a published container port, the nat PREROUTING chain rewrites the destination IP from your Netcup public IPv4 address to the container’s internal bridge IP (172.20.0.x). Because the destination IP is no longer the host itself, the kernel routes the packet down the FORWARD chain (DOCKER-USER → DOCKER) instead of the INPUT chain where UFW places its host rules.

To eliminate accidental database or admin-port exposure on Netcup, enforce a three-layer network defense: bind internal container ports explicitly to loopback (127.0.0.1:5432:5432) in compose.yaml, activate the Netcup SCP Hypervisor Stateful Firewall outside the VM (which filters packets on the host virtual switch before the guest kernel’s PREROUTING table even sees them), and lock down the DOCKER-USER chain.

Whenever you deploy a new Compose stack, always run sudo ss -tulnp on the host OS to audit active listening sockets. Only sshd (port 22) and your reverse proxy (nginx or caddy on ports 80 and 443) should ever bind to 0.0.0.0 or [::]; every application worker, Redis instance, and SQL database should either remain unexposed inside an internal Docker bridge network or bind strictly to 127.0.0.1.

  • ▪ Layer 1 — Explicit Loopback Binding in compose.yaml: Always write 127.0.0.1:8080:8080 instead of 8080:8080 for backend services accessed through a host Nginx/Caddy reverse proxy.
  • ▪ Layer 2 — Netcup SCP Hypervisor Stateful Firewall: In the Netcup Server Control Panel (servercontrolpanel.de → Network → Firewall), allow inbound TCP 22, 80, 443 and UDP 443, and set the default ingress policy to DROP. Because the SCP Firewall operates outside the VM on the host virtual switch, Docker’s guest iptables rules cannot bypass it!
  • ▪ Layer 3 — DOCKER-USER Chain Lock in /etc/ufw/after.rules: Add explicit drop rules to the DOCKER-USER netfilter chain for traffic arriving on your public interface (eth0/ens3) targeting private RFC1918 container subnets.
Docker Container Capacity & Firewall Architecture by Netcup Hardware Tier
Netcup Server TierRAM & vCPU / CoresStorage VolumeTypical Docker Workload CapacityRecommended Firewall Stack
VPS nano G11.5s2 GB ECC / 2 vCPU60 GB SSD3–5 lightweight Alpine / Go / Caddy containers127.0.0.1 binding + Netcup SCP Edge Firewall
VPS 500 G12.54 GB ECC / 4 vCPU120 GB NVMe8–12 containers (WordPress, Redis, Postgres, Nginx)127.0.0.1 binding + SCP Edge Firewall + UFW
VPS 1000 G12.58 GB ECC / 6 vCPU256 GB NVMe15–25 containers (Full SaaS stack + monitoring)127.0.0.1 binding + SCP Firewall + DOCKER-USER chain
RS 1000 G12.58 GB DDR5 ECC / 4 Dedicated EPYC256 GB NVMe25+ high-I/O containers, CI runners & databases127.0.0.1 binding + SCP Firewall + DOCKER-USER chain
Verify Exposed Container Sockets & Test Loopback Binding
bash
# Always bind internal app ports strictly to 127.0.0.1:
docker run -d --name test-redis --restart unless-stopped -p 127.0.0.1:6379:6379 redis:7-alpine

# Audit all listening TCP/UDP sockets on your Netcup server:
# Ensure only sshd (22) and your reverse proxy (80/443) listen on 0.0.0.0 or [::]
sudo ss -tulnp

Command Explanation: Verifies that redis-server binds exclusively to 127.0.0.1:6379 rather than 0.0.0.0:6379.

EXPECTED OUTPUT
tcp   LISTEN 0      4096       127.0.0.1:6379      0.0.0.0:*    users:(("dockerd",pid=1120,fd=14))

SECURITY NOTE: Note about the docker UNIX group: adding a user via sudo usermod -aG docker deployadmin is root-equivalent because any member of the docker group can mount / into an alpine container. Only grant docker group membership to trusted administrators.

Never Publish Unbound Container Ports (-p 5432:5432) on a Public VPS

Writing -p 5432:5432 binds 0.0.0.0:5432 and routes traffic through the netfilter FORWARD chain, completely bypassing UFW INPUT rules. Always prefix internal ports with 127.0.0.1: (e.g., 127.0.0.1:5432:5432).

Common Troubleshooting Scenarios

DIAGNOSTIC RUNBOOK

[SYMPTOM] Containers cannot resolve external DNS names (Temporary failure in name resolution)

Root Cause: Ubuntu’s systemd-resolved stub listener runs on 127.0.0.53, which is unreachable inside an isolated container network namespace if /etc/resolv.conf only lists loopback addresses.

Resolution: Add "dns": ["46.38.225.230", "1.1.1.1", "2a03:4000:0:1::e1e6"] (Netcup’s low-latency recursive DNS + Cloudflare DNS) into /etc/docker/daemon.json and run sudo systemctl restart docker.

[SYMPTOM] docker restart fails after changing /etc/docker/daemon.json

Root Cause: Trailing commas or syntax errors in /etc/docker/daemon.json, or conflicting flags passed in /lib/systemd/system/docker.service.

Resolution: Validate the JSON syntax using jq . /etc/docker/daemon.json and inspect sudo journalctl -u docker.service -n 30 --no-pager.

[SYMPTOM] Docker bridge network (172.17.0.0/16) conflicts with corporate WireGuard or OpenVPN subnet

Root Cause: Docker allocates 172.17.0.0/16 through 172.31.0.0/16 by default if default-address-pools is not specified.

Resolution: Define a non-overlapping CIDR block under "default-address-pools" in /etc/docker/daemon.json (as shown in Step 2) and recreate existing custom bridge networks with docker network prune.

Frequently Asked Questions

VERIFIED Q&A

Does Docker work on the entry-level Netcup VPS nano G11.5s (2 GB ECC RAM)?

Yes! Because VPS nano G11.5s uses full KVM virtualization with 2 GB ECC RAM and 60 GB SSD storage, it easily runs 3 to 5 lightweight Alpine/Go/Nginx Docker containers. For larger stacks with MariaDB/PostgreSQL and Redis, step up to the 4 GB VPS 500 G12.5 or 8 GB VPS 1000 G12.5.

Should I format my Netcup server with ext4 or XFS for Docker overlay2?

Both ext4 (Ubuntu/Debian default) and XFS (with ftype=1, AlmaLinux/Rocky default) are fully supported Tier-1 backing filesystems for Docker’s overlay2 storage driver.

How do I safely prune unused Docker images and build cache on a Netcup disk?

Run docker system df to inspect disk consumption, and run docker builder prune -af && docker image prune -af --filter "until=168h" weekly to reclaim dangling build layers without touching named volumes.

Why did my container port stay reachable from the internet even after I ran ufw deny 8080?

Because Docker inserts port-forwarding rules into the netfilter PREROUTING and FORWARD chains, whereas ufw deny 8080 only filters the INPUT chain. Always bind internal ports to 127.0.0.1:8080:8080 and enable the Netcup SCP Stateful Firewall.

REFERENCES // VERIFIED TECHNICAL SOURCES

FURTHER READING // ARCHITECTURE AUDITS & GUIDES

Active Coupons →