Install Docker on Ubuntu 24.04/22.04 VPS: The Official Way
- October 3, 2026
- 9:00 am
- By Adrien Roche
- Tutorials

Before you start: a KVM VPS, a supported Ubuntu, and a sudo user
Docker Engine does not ship its own kernel. Every container shares the host kernel and relies on Linux namespaces, cgroups and netfilter, so the daemon needs a VPS where you own the kernel. That is what a KVM (full-virtualization) VPS gives you. On container-based plans built on OpenVZ or LXC, the kernel belongs to the provider and Docker either refuses to start or only works if the host enabled nesting. Check before installing anything:
systemd-detect-virt # expect: kvm
. /etc/os-release && echo "$VERSION_CODENAME" # noble (24.04) or jammy (22.04)
uname -m # x86_64 or aarch64
Docker publishes packages for 64-bit Ubuntu 24.04 LTS (noble) and 22.04 LTS (jammy), on x86_64 and arm64. You also need a user with sudo rights; the docker group and rootless sections below assume a regular account rather than root.
Install Docker Engine from the official apt repository
There are four common ways to install Docker on Ubuntu, and only one is the method Docker documents and supports for servers:
| Method | Package | Verdict |
|---|---|---|
| Docker's apt repository | docker-ce from download.docker.com | Recommended: current releases, updates through apt upgrade, Compose and Buildx plugins included. |
| Ubuntu universe | docker.io | Works, but Ubuntu's own build lags upstream and lacks the plugins. |
| Snap | snap install docker | Confined install with its own paths; frequent source of "permission denied" surprises. Avoid on servers. |
| Convenience script | get.docker.com | Same packages, but piped unreviewed into a root shell. Throwaway machines only. |
Step 1: remove conflicting packages
If the VPS image already contains Ubuntu's docker.io, an old docker-compose, or podman-docker, remove them so the two builds do not fight over /usr/bin/docker. This loop comes from Docker's documentation and is harmless on a clean system; it leaves /var/lib/docker untouched:
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do
sudo apt-get remove $pkg
done
Step 2: add Docker's GPG key and apt source
The key goes in /etc/apt/keyrings/ and is referenced with signed-by=, which scopes it to this one repository instead of trusting it globally through the deprecated apt-key. The codename is read from /etc/os-release, so the same block works on 24.04 and 22.04:
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
A signature error on that last apt-get update almost always means the key file is unreadable; re-run the chmod line.
Step 3: install the engine and its plugins
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
docker-ce is the daemon, docker-ce-cli the client, containerd.io the runtime underneath, and the two plugins provide docker buildx and docker compose. On Ubuntu the package post-install script starts docker.service immediately and enables it at boot.
Step 4: verify with hello-world
sudo docker run hello-world
docker --version
sudo docker info | head -n 20
hello-world is the definitive test: the client talks to the daemon, the daemon pulls a tiny image from Docker Hub, creates a container, runs it and streams its output back. If you see "Hello from Docker!", every layer works. On a healthy 24.04 or 22.04 install, docker info reports Storage Driver: overlay2 and Cgroup Version: 2.
Run Docker without sudo: the docker group
The daemon listens on the Unix socket /var/run/docker.sock, owned by root:docker, so members of the docker group can use the CLI without sudo:
sudo groupadd docker # usually exists already; harmless if it does
sudo usermod -aG docker $USER
newgrp docker # or log out and back in
docker run hello-world
Understand what you just granted. Anyone who can reach that socket can run docker run -v /:/host --privileged ... and read or rewrite any file on the VPS, which makes docker group membership equivalent to root, with no password prompt and no sudo log entry. Treat it exactly like sudo access; our guide on switching users and privilege escalation on Ubuntu explains why that distinction matters on a multi-user box.
Docker Compose plugin
The docker-compose-plugin package provides Compose v2 as the docker compose subcommand (with a space). The old Python docker-compose binary is end-of-life and should not be installed alongside it. Check the version, then test with a minimal compose.yaml:
docker compose version
mkdir -p ~/web && cd ~/web
cat > compose.yaml <<'EOF'
services:
web:
image: nginx:alpine
ports:
- "127.0.0.1:8080:80"
restart: unless-stopped
EOF
docker compose up -d
docker compose ps
curl -I http://127.0.0.1:8080
docker compose down
Two details are deliberate: restart: unless-stopped brings the container back after a reboot, and the port is bound to 127.0.0.1 rather than all interfaces, for reasons the firewall section explains.
Rootless mode: a daemon that is not root
Rootless mode runs the daemon and the containers inside a user namespace, so a container escape lands in an unprivileged account instead of root. It is the right choice when several people share a VPS. Prerequisites are subordinate UID/GID ranges (Ubuntu creates them for normal users in /etc/subuid and /etc/subgid), the uidmap tools, a user D-Bus session, and Docker's rootless extras:
sudo apt-get install -y uidmap dbus-user-session docker-ce-rootless-extras
grep "^$USER:" /etc/subuid /etc/subgid # each should show a range of 65536 IDs
# optional: stop the system-wide daemon if you only want rootless
sudo systemctl disable --now docker.service docker.socket
dockerd-rootless-setuptool.sh install
The setup tool creates a per-user systemd unit and prints the two variables your shell needs. Add them to ~/.bashrc, then make the user daemon start at boot without a login:
export PATH=/usr/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
systemctl --user enable docker
sudo loginctl enable-linger $(whoami)
Rootless mode has real limits: ports below 1024 cannot be published unless you run sudo setcap cap_net_bind_service=ep $(which rootlesskit) and restart the user daemon, overlay networks are unavailable, and data lives under ~/.local/share/docker. On Ubuntu 24.04, AppArmor restricts unprivileged user namespaces; the deb packages ship the profile rootlesskit needs, but a static-binary install requires adding it by hand as described in the rootless documentation.
Boot behaviour, storage and log rotation
Two separate things decide whether your containers are up after a reboot: the daemon must be enabled, and each container must carry a restart policy. Ubuntu's packages enable the service for you; confirm it, and if the unit states look unfamiliar, our reference on listing services with systemctl covers every column:
systemctl is-enabled docker.service containerd.service
sudo systemctl enable docker.service containerd.service # only if the line above said disabled
Containers started without --restart stay down after a reboot; use --restart unless-stopped on the command line or restart: unless-stopped in Compose for anything permanent.
Everything Docker stores (image layers, container filesystems, volumes, logs) lives under /var/lib/docker, and on a small VPS this is the directory that fills the disk. Watch it with docker system df, reclaim dangling layers with docker system prune, and read the prompt before adding -a, which also deletes every image not used by a running container. If the VPS has a second disk, point data-root at it in /etc/docker/daemon.json before pulling anything large.
Logs deserve a specific warning. The default json-file driver writes every line of container stdout to /var/lib/docker/containers/<id>/<id>-json.log and performs no rotation unless you configure it. A chatty container can eat the whole disk in a week. Set daemon-wide limits on day one:
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" },
"live-restore": true
}
EOF
sudo systemctl restart docker
Log options apply to containers created after the restart, so recreate long-running ones once. live-restore lets the daemon restart for upgrades without killing running containers.
Docker and ufw: published ports bypass your firewall
This is the part that catches most people on a public VPS. Docker manages its own iptables rules: when you publish a port with -p 8080:80, the daemon inserts a DNAT rule in the nat table's PREROUTING chain and an accept rule in FORWARD. ufw's rules live in INPUT, which forwarded traffic never crosses. So ufw deny 8080 has no effect, and a database container published on 0.0.0.0:5432 is reachable from the whole internet while ufw reports the port as blocked. Look for 0.0.0.0: in the PORTS column of docker ps, or run ss -tlnp; our guide on checking open ports on Linux shows how to read that output and confirm exposure from outside.
Two clean fixes, in order of preference:
- Bind published ports to loopback.
-p 127.0.0.1:8080:80keeps the service reachable only from the VPS itself, typically behind a reverse proxy such as nginx or Caddy that you expose deliberately. Containers that only talk to each other need no-pat all. - Filter in the
DOCKER-USERchain. Docker evaluates this chain before its ownFORWARDrules and never rewrites it. Because DNAT has already happened when a packet reaches it, match the original port with conntrack:
sudo iptables -I DOCKER-USER -i eth0 ! -s 203.0.113.10 -p tcp \
-m conntrack --ctorigdstport 8080 --ctdir ORIGINAL -j DROP
Replace eth0 with your public interface and 203.0.113.10 with the address allowed in. These rules are not persistent; save them with the iptables-persistent package. Setting "iptables": false in daemon.json breaks container networking in ways that are hard to debug; leave it alone.
Uninstall Docker completely
Purge the packages first, then delete the data directories, which apt leaves in place on purpose:
sudo apt-get purge -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras
sudo rm -rf /var/lib/docker /var/lib/containerd
sudo rm -f /etc/apt/sources.list.d/docker.list /etc/apt/keyrings/docker.asc
sudo apt-get autoremove -y
For a rootless setup, run dockerd-rootless-setuptool.sh uninstall as the user first and delete ~/.local/share/docker afterwards.
All of this assumes a server where you hold root, control the kernel, and can rewrite iptables without asking anyone. If you still need that machine, rdp.monster's KVM Linux VPS with full root access ships Ubuntu 24.04 or 22.04 with dedicated CPU and RAM, unlimited (fair-use) bandwidth, no KYC, crypto accepted, from $8.99/month, and is delivered about 10 seconds after payment confirms, so you can be running the hello-world check above on your own server right after checkout.
Frequently Asked Questions
Which Docker version does apt install on Ubuntu, and how do I update it?
apt-get install docker-ce pulls the newest stable release available for your Ubuntu codename, and every later sudo apt-get update && sudo apt-get upgrade moves you forward. Run docker version to see both the client and server versions. To pin a specific release, list candidates with apt-cache madison docker-ce and install docker-ce=<version> together with the matching docker-ce-cli, then apt-mark hold both packages.Why do I get "permission denied while trying to connect to the Docker daemon socket"?
root:docker, so a plain user gets permission denied. Either prefix commands with sudo or add yourself with sudo usermod -aG docker $USER, then start a fresh login session (or run newgrp docker) because group changes only apply to new sessions. If the error persists, check groups to confirm membership, verify the daemon is running with systemctl status docker, and make sure DOCKER_HOST is not pointing at a rootless socket that does not exist.Should I install Docker with snap or apt on Ubuntu Server?
daemon.json and data, and its update cadence is controlled by snapd rather than by you. Those quirks show up as mysterious permission denied and bind-mount failures. The apt packages follow standard Ubuntu conventions, integrate with systemd normally, and receive upstream updates through the same apt upgrade you already run.How much RAM and disk does a Docker VPS need?
/var/lib/docker, and 20 GB fills quickly without docker system prune and log rotation. Prefer dedicated CPU and RAM over burstable shares, since builds spike hard and containers cannot swap gracefully.Adrien Roche, Infrastructure & Hosting Editor
Systems engineer with 10+ years operating Windows Server and Linux fleets. Adrien runs the rdp.monster infrastructure documentation and writes our guides on RDP, VPS hosting, server administration, networking and privacy tooling.




