Docker keeps coming up once you outgrow shared hosting — you need to run a Node app, a self-hosted tool, or a staging environment that doesn't fit cPanel's world of PHP and Apache vhosts. On a VPS you have root, so nothing stops you from installing it. The part that trips people up is everything after apt install docker.io: permission errors, firewall rules that silently punch holes in your security, and disk usage that creeps up until the server is out of space. Here's how to install Docker properly on a fresh VPS and avoid the mistakes that generate support tickets a week later.

Before You Start: Does Docker Belong on This VPS?

One thing first, because it saves a lot of grief: don't install Docker on a VPS that's also running cPanel/WHM. cPanel manages its own iptables and firewall rules (or works alongside CSF), and Docker rewrites iptables NAT rules on every restart. The two fight over the same tables, and the usual symptom is a cPanel firewall rule that "works" until the Docker service restarts and quietly undoes it. If you need both, run them on separate VPS instances — a small unmanaged VPS for containers is cheap and keeps the two systems from colliding.

Docker is a good fit for an unmanaged Ubuntu, Debian, AlmaLinux, or Rocky Linux VPS with at least 1-2GB of RAM. Anything less and the daemon itself, plus even one or two containers, will start fighting your application for memory.

Installing Docker on Ubuntu or Debian

Skip the distro's default docker.io package — it's usually several versions behind. Install straight from Docker's own repository instead:

sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Swap ubuntu for debian in both URLs if you're on Debian. This pulls in the Docker Compose plugin too, so you get docker compose (no hyphen — that's the current syntax; the old standalone docker-compose binary is deprecated).

Installing Docker on AlmaLinux or Rocky Linux

Same idea, different package manager:

sudo dnf install -y dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker

If you followed our firewalld guide and locked the server down with firewall-cmd, keep reading before you start publishing container ports — the interaction between firewalld/UFW and Docker is the single most common source of confusion here.

Symptom: "Cannot Connect to the Docker Daemon"

What you see: running any docker command as your normal SSH user returns permission denied while trying to connect to the Docker daemon socket, even though the service is clearly running.

Cause: the Docker daemon listens on a Unix socket owned by root and the docker group. Your login user isn't in that group, so every command needs sudo — or fails outright if you're scripting something that doesn't expect to need it.

Fix: add your user to the docker group and start a new shell session:

sudo usermod -aG docker $USER
newgrp docker

Log out and back in over SSH if newgrp doesn't pick it up immediately. Verify with docker run hello-world — it should pull and run without sudo.

Worth knowing: membership in the docker group is effectively root-equivalent, since a container can mount the host filesystem. Only add users you'd trust with root anyway — don't add it to a shared deployment account without thinking about who has access to it.

Symptom: Container Ports Are Reachable Even Though UFW Says They're Blocked

What you see: you ran ufw deny 5432 to keep a database port private, ufw status confirms it's denied, but the port answers from the public internet anyway once you start the container with -p 5432:5432.

Cause: Docker manages its own iptables rules directly, and on most setups those rules get inserted before UFW's chain gets evaluated. Publishing a port with -p effectively bypasses UFW entirely — this is a well-known Docker behavior, not a bug in your firewall config.

Fix, in order of what actually works:

ApproachWhen to use it
Bind to localhost only: -p 127.0.0.1:5432:5432The service is only ever needed by another container or a local process — the most common case for databases
Set "iptables": false in /etc/docker/daemon.json and manage rules manuallyYou need Docker to respect your firewall rules and are comfortable writing iptables/nftables rules yourself
Use UFW's after.rules file to insert rules before Docker's chainYou want to keep using ufw allow/deny as your primary interface — more setup, but keeps the familiar workflow

For most SkyServer VPS setups running a handful of containers, binding to 127.0.0.1 and putting Nginx in front as a reverse proxy for anything public-facing is the simplest option and avoids the iptables interaction altogether.

Symptom: VPS Disk Full After a Few Weeks of Docker Use

What you see: the server that had 20GB free a month ago is suddenly reporting 95%+ disk usage, and nothing in your application logs explains it.

Cause: Docker keeps every image layer you've ever pulled, every stopped container, and every build cache entry unless you clean up explicitly. A CI/CD pipeline that rebuilds images on every deploy is the usual culprit — each build leaves the old image layers behind.

Fix: check what's actually eating space, then reclaim it:

docker system df
docker system prune -a --volumes

The -a flag also removes unused images, not just dangling ones — read the confirmation prompt before confirming if the server has containers you rarely start but still need. For automated cleanup, add a weekly cron job with docker system prune -af --filter "until=168h" to remove anything untouched for a week, rather than nuking everything on a schedule.

Prevention

  • Keep Docker and cPanel/WHM on separate VPS instances — don't let them share a firewall.
  • Bind container ports to 127.0.0.1 by default and only publish to 0.0.0.0 when a port genuinely needs to be public.
  • Put a real reverse proxy (Nginx or Traefik) in front of containers instead of exposing app ports directly — it gives you TLS termination and access logs in one place.
  • Set up log rotation for containers early: Docker's default json-file logging driver doesn't rotate on its own and can fill a disk from log output alone. Add "log-opts": {"max-size": "10m", "max-file": "3"} under the json-file driver in /etc/docker/daemon.json.
  • Schedule docker system prune on a cron job if you rebuild images often.
  • Use named volumes for anything you need to survive a container rebuild — a database inside a container with no volume loses all its data the moment you run docker compose down.

Frequently Asked Questions

Can I run Docker on a SkyServer cPanel VPS?

Technically yes if you have root access, but it's not recommended. Docker rewrites iptables rules that CSF and cPanel's own firewall tooling also manage, and the two will conflict — usually after a reboot or a Docker service restart, when rules silently revert. Run Docker on a separate unmanaged VPS instead.

Do I need both Docker and Docker Compose, or does Compose come with it?

Installing docker-compose-plugin alongside the Docker Engine packages (as shown above) gives you the docker compose command built in — no separate installation needed. The old standalone docker-compose binary with a hyphen is a different, now-deprecated tool.

Why does a container survive a reboot but lose its data anyway?

The container itself restarting isn't the issue — data loss happens when a container is removed and recreated (docker compose down followed by up, or a fresh docker run) without a named volume or bind mount backing its data directory. Anything written inside the container's writable layer disappears the moment the container is deleted, not just stopped.

How do I update Docker without breaking running containers?

Running containers keep running through a Docker Engine upgrade in most cases, since the containerd runtime managing them stays up separately from the docker-ce package update. Still, take a moment to check docker compose ps and confirm everything's healthy after: sudo apt update && sudo apt install --only-upgrade docker-ce docker-ce-cli containerd.io (or the equivalent dnf command on AlmaLinux/Rocky).

My VPS only has 1GB of RAM — is that enough for Docker?

It'll run, but budget carefully. The Docker daemon itself uses a modest amount of memory, but each container adds its own footprint on top of your VPS's base OS usage. One lightweight container (a small API or a Redis instance) is usually fine on 1GB; anything with a database and an app container together will feel tight. Consider adding a swap file as a safety net so a memory spike triggers slowdown rather than the OOM killer terminating a container outright.