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.
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, anddocker-compose-pluginfrom Docker’s official cryptographic APT repository rather than Ubuntu’s staledocker.iopackage or Snap. -
02 Configure
/etc/docker/daemon.jsonwithmax-size: "10m",max-file: "3", andlive-restore: trueso 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:5432injects rules into thePREROUTING/DOCKERnetfilter chains BEFORE UFW—exposing your database to the entire internet! Always bind internal ports to127.0.0.1:5432:5432and enforce the Netcup SCP Edge Firewall.
📑 Quick Jump Table of Contents (3 Sections) ▼
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.
# 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.
Docker version 27.3.1, build ce12230
Docker Compose version v2.29.7 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.
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.
Storage Driver: overlay2
Logging Driver: json-file
Live Restore Enabled: true 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 write127.0.0.1:8080:8080instead of8080:8080for 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 TCP22,80,443and UDP443, and set the default ingress policy toDROP. Because the SCP Firewall operates outside the VM on the host virtual switch, Docker’s guestiptablesrules cannot bypass it! - ▪ Layer 3 —
DOCKER-USERChain Lock in/etc/ufw/after.rules: Add explicit drop rules to theDOCKER-USERnetfilter chain for traffic arriving on your public interface (eth0/ens3) targeting private RFC1918 container subnets.
| Netcup Server Tier | RAM & vCPU / Cores | Storage Volume | Typical Docker Workload Capacity | Recommended Firewall Stack |
|---|---|---|---|---|
| VPS nano G11.5s | 2 GB ECC / 2 vCPU | 60 GB SSD | 3–5 lightweight Alpine / Go / Caddy containers | 127.0.0.1 binding + Netcup SCP Edge Firewall |
| VPS 500 G12.5 | 4 GB ECC / 4 vCPU | 120 GB NVMe | 8–12 containers (WordPress, Redis, Postgres, Nginx) | 127.0.0.1 binding + SCP Edge Firewall + UFW |
| VPS 1000 G12.5 | 8 GB ECC / 6 vCPU | 256 GB NVMe | 15–25 containers (Full SaaS stack + monitoring) | 127.0.0.1 binding + SCP Firewall + DOCKER-USER chain |
| RS 1000 G12.5 | 8 GB DDR5 ECC / 4 Dedicated EPYC | 256 GB NVMe | 25+ high-I/O containers, CI runners & databases | 127.0.0.1 binding + SCP Firewall + DOCKER-USER chain |
# 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.
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.
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&ADoes 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
- [1] Docker Engine Official Ubuntu Installation Guide (https://docs.docker.com/engine/install/ubuntu/)
- [2] Docker Packet Filtering and Firewalls (DOCKER-USER Chain) (https://docs.docker.com/engine/network/packet-filtering-firewalls/)