How to Install Docker on Ubuntu 24.04 — Production-Ready Container Runtime
Docker turned containers from a kernel feature into a workflow. With a single docker run you pull an image, spin up a sandboxed process, wire up networking and volumes, and tear it down again — with none of the drift, dependency hell, or "works on my machine" pain of traditional installs. On a fresh Ubuntu 24.04 VPS, a properly installed Docker engine becomes the foundation for everything from a single self-hosted app to a full microservices stack orchestrated by Docker Compose or managed through Portainer.
This guide takes you from a bare Ubuntu 24.04 server to a hardened, production-tuned Docker installation. You will remove the older docker.io snapshot that ships in the Ubuntu archive, add Docker's official apt repository with its GPG key, install docker-ce alongside the Compose and Buildx plugins, apply the post-install recommendations, tune daemon.json for live-restore and disciplined logging, optionally enable rootless mode, and finish with a pruning policy that keeps /var/lib/docker from eating your disk.
Prefer to roll your own runtime rather than pay for a managed container platform? Our Starter VPS gives you full root on bare metal–grade KVM hardware from EUR 7.99/month — run Docker, Compose and Portainer without the per-container premium of ECS or Cloud Run.
Table of Contents
Why Self-Host Docker on a VPS?
Managed container platforms — AWS ECS, Google Cloud Run, Azure Container Apps, Fly.io, Railway — are excellent at hiding the runtime. You push an image, they schedule it. The trade-off is cost, egress, lock-in, and a surprisingly thin layer of control.
Running Docker yourself on a VPS gives you:
- Flat, predictable pricing — One monthly VPS fee covers unlimited containers, unlimited builds, unlimited pulls and unlimited internal traffic. Managed platforms charge per vCPU-second, per GB-second of memory, per request, and per GB of egress. A stack that costs EUR 7/month on a Starter VPS regularly bills at USD 40-80 on Cloud Run.
- No cold starts — Serverless container runtimes spin down idle instances and add 200-2000 ms latency on the next request. A long-running
dockerdkeeps your containers hot 24/7. - Full kernel and networking access — You can use
--network host, raw sockets, privileged containers, customiptablesrules, WireGuard sidecars, and kernel modules. Managed platforms block most of these. - Runtime choice — You pick the containerd version, the logging driver, the cgroup driver, the storage driver, the address pools, and whether to enable BuildKit or Buildx by default.
- One tool, everywhere — The same
docker compose up -dworks on your laptop, your CI runner, your staging VPS and your production VPS. Managed platforms each have their own deployment model. - Data locality — Volumes live on your disk. Backups are
rsyncorresticsnapshots. No object storage API calls, no egress bills when you restore. - Zero per-seat billing — Team members get SSH access to the same host. No
users × environments × regionsmultiplier.
Prerequisites
Before you begin, make sure you have:
- A VPS running Ubuntu 24.04 LTS (Noble Numbat) with root or sudo access
- SSH access to your server
- At least 2 GB RAM and 20 GB of free disk for the daemon, images and a handful of containers (4 GB / 40 GB recommended)
- A non-root user with sudo privileges (the examples below assume the user
admin)
Recommended Plan: Starter VPS>
For a typical Docker host running 3-8 containers — reverse proxy, database, cache, one or two web apps — we recommend the Starter VPS plan:>
- 4 vCPU cores
- 8 GB RAM
- 100 GB NVMe SSD
- Unmetered bandwidth
- From EUR 7.99/month>
Need more headroom for databases or a dozen-plus containers? Step up to our Professional or Performance plans on the same page.
Connect to your server via SSH:
ssh admin@your-server-ipStep 1: Update the System
Refresh the package index and apply any pending security updates before installing a new repository.
sudo apt update && sudo apt upgrade -yExpected output (abbreviated):
Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
Hit:2 http://archive.ubuntu.com/ubuntu noble-updates InRelease
Reading package lists... Done
Building dependency tree... Done
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.If the kernel was updated, reboot and reconnect:
sudo rebootInstall a few helpers that the Docker apt setup depends on:
sudo apt install -y ca-certificates curl gnupg lsb-releaseStep 2: Remove Older Docker Packages
Ubuntu's archive ships docker.io, docker-doc, docker-compose (v1, Python), podman-docker, and older containerd/runc packages. These conflict with the official Docker CE packages and — worse — often get preferred by apt's dependency solver if they stay installed. Purge them first.
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do
sudo apt-get remove -y $pkg
doneExpected output (when nothing is installed, which is normal on a fresh VPS):
Package 'docker.io' is not installed, so not removed
Package 'docker-doc' is not installed, so not removed
...If any of those packages were installed and you want to also wipe their config and data, run:
sudo apt purge -y docker.io docker-compose runc containerd
sudo rm -rf /var/lib/docker /var/lib/containerdCareful: rm -rf /var/lib/docker destroys every image, container and named volume on the host. Only run it on a fresh system or after you have backed up your volumes.Step 3: Add Docker's Official Apt Repository
Docker signs its apt repository with a GPG key hosted at download.docker.com. Install the key into /etc/apt/keyrings — the modern location that avoids the deprecated apt-key tool — then add a signed-by repo entry so apt verifies every package against it.
Create the keyrings directory and download the GPG key:
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.ascAdd the repository. The command below resolves your Ubuntu codename (noble on 24.04) and architecture (amd64, arm64, etc.) automatically:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/nullRefresh the package index so apt picks up the new source:
sudo apt updateExpected output:
Get:5 https://download.docker.com/linux/ubuntu noble InRelease [48.8 kB]
Get:6 https://download.docker.com/linux/ubuntu noble/stable amd64 Packages [15.4 kB]
Reading package lists... DoneIf you see NO_PUBKEY or signatures couldn't be verified, the GPG key download failed — rerun the curl command above and double-check that /etc/apt/keyrings/docker.asc exists and is readable.
Step 4: Install Docker Engine, CLI and Plugins
With the repository wired up, install the full Docker CE stack:
sudo apt install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-pluginWhat each package provides:
docker-ce— the Docker daemon (dockerd) and the systemd unitdocker-ce-cli— thedockerCLI you run on the command linecontainerd.io— the lower-level container runtime thatdockerddelegates todocker-buildx-plugin— multi-platform builds via BuildKit (docker buildx build)docker-compose-plugin— Compose v2 as a subcommand (docker compose up, no hyphen)
docker --version
docker compose version
docker buildx versionExpected output:
Docker version 27.5.1, build 9f9e405
Docker Compose version v2.32.4
github.com/docker/buildx v0.19.3Check that the daemon is running:
sudo systemctl status dockerExpected output:
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; preset: enabled)
Active: active (running) since Thu 2026-04-16 10:12:03 UTC; 30s agoRun the canonical smoke test:
sudo docker run --rm hello-worldExpected output:
Hello from Docker!
This message shows that your installation appears to be working correctly.Step 5: Post-Install — docker Group and usermod
Out of the box, only root can talk to /var/run/docker.sock. Prefixing every command with sudo works but clutters your history and defeats shell completion. The fix is to add your user to the docker group.
Security note: Membership in thedockergroup is effectively equivalent to root on the host. Any user who can talk to the Docker socket can mount/into a privileged container and rewrite any file. Only grant it to trusted administrators. If that worries you, skip to Step 7 and use rootless Docker instead.
Create the group (the package install may already have created it — groupadd with || true makes the command idempotent):
sudo groupadd docker || trueAdd your current user:
sudo usermod -aG docker $USERApply the new group membership without logging out:
newgrp dockerVerify by running docker without sudo:
docker run --rm hello-worldIf you see a permission denied while trying to connect to the Docker daemon socket error, log out of your SSH session completely and reconnect — newgrp only affects the current shell.
Finally, make sure Docker starts on boot (this is the default, but worth confirming on older images):
sudo systemctl enable --now docker.service containerd.serviceStep 6: Configure daemon.json for Production
Out of the box, dockerd uses the json-file log driver with no size limit — meaning a chatty container can silently fill /var/lib/docker/containers//.log until the disk is full. It also restarts all containers on every systemctl restart docker, which is disruptive. A good production daemon.json fixes both.
Create /etc/docker/daemon.json:
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"live-restore": true,
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "5",
"compress": "true"
},
"default-address-pools": [
{ "base": "10.200.0.0/16", "size": 24 },
{ "base": "10.201.0.0/16", "size": 24 }
],
"default-ulimits": {
"nofile": { "Name": "nofile", "Hard": 65535, "Soft": 65535 }
},
"no-new-privileges": true,
"userland-proxy": false,
"features": { "buildkit": true },
"metrics-addr": "127.0.0.1:9323"
}
EOFWhat each option does:
live-restore: true— Keeps running containers alive when the daemon is restarted or upgraded. Without this, everyapt upgrade docker-cetakes every container offline.log-driver: json-file+log-opts— Caps per-container log files at 10 MB and keeps 5 rotations (so 50 MB max per container).compress: truegzips rotated files. You can swap this forjournald,syslogorlokilater.default-address-pools— Docker carves subnets for user-defined bridge networks from these pools. The default (172.17.0.0/16and172.18.0.0/16) collides with many VPN and corporate networks. Overriding to10.200.0.0/16avoids that pain.default-ulimits.nofile— Raises the open-file limit inside containers from 1024 to 65535 — important for databases, reverse proxies and anything handling thousands of connections.no-new-privileges: true— Prevents processes inside containers from gaining new privileges via setuid binaries. A free hardening win.userland-proxy: false— Uses iptables DNAT for published ports instead of the userlanddocker-proxyhelper. Lower CPU overhead on high-throughput workloads.features.buildkit: true— Makes BuildKit the default builder fordocker build. Faster, parallel, with better caching.metrics-addr: 127.0.0.1:9323— Exposes Prometheus-format daemon metrics on localhost for scraping.
sudo dockerd --validate
sudo systemctl restart docker
sudo systemctl status docker --no-pagerConfirm the address pools took effect:
docker network create test-net
docker network inspect test-net | grep Subnet
docker network rm test-netExpected output:
"Subnet": "10.200.0.0/24",Step 7: Enable Rootless Mode (Optional)
Rootless Docker runs the daemon as an unprivileged user inside a user namespace. A container escape cannot pivot to host root because the "root" inside the container maps to your unprivileged UID on the host. The trade-offs: slightly slower networking via slirp4netns, no binding to ports below 1024 without extra capabilities, and a few storage drivers are unavailable.
Install the rootless helpers and uidmap utilities:
sudo apt install -y docker-ce-rootless-extras uidmap dbus-user-session fuse-overlayfsStop the system-wide daemon (optional — you can keep both, running on different sockets):
sudo systemctl disable --now docker.service docker.socketRun the setup tool as your regular user (not with sudo):
dockerd-rootless-setuptool.sh installExpected output:
[INFO] Creating /home/admin/.config/systemd/user/docker.service
[INFO] starting systemd service docker.service
[INFO] Installed docker.service successfully.
[INFO] To control docker.service, run: systemctl --user (start|stop|restart) docker.service
[INFO] Make sure the following environment variables are set (or add them to ~/.bashrc):
export PATH=/usr/bin:$PATH
export DOCKER_HOST=unix:///run/user/1000/docker.sockAdd the exports to your shell profile:
echo 'export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock' >> ~/.bashrc
source ~/.bashrcEnable lingering so the user daemon survives logout:
sudo loginctl enable-linger $USERVerify rootless operation:
docker info | grep -i rootlessExpected output:
Security Options:
rootlessFor a deeper comparison of rootless runtimes, see our guide on Podman vs Docker.
Step 8: Docker Networking — bridge vs host
Docker ships with three built-in network drivers that cover 99% of single-host workloads:
bridge — the default, isolated
Every container gets its own IP on an internal Linux bridge (docker0 by default, or a user-defined bridge). Ports you want reachable from outside must be explicitly published with -p.
# User-defined bridge with DNS-based container name resolution docker network create app-net docker run -d --name web --network app-net -p 8080:80 nginx docker run -d --name api --network app-net my-api:latest
From inside the api container,curl http://webjust works
User-defined bridges (not the legacy docker0) give you automatic DNS for container names, better isolation and the ability to connect/disconnect on the fly. Use a user-defined bridge for every multi-container app.
host — no isolation, maximum performance
The container shares the host's network namespace. No -p flag, no NAT, no iptables hop. The container binds directly to host ports.
docker run -d --network host nginx
nginx is now listening on the host's :80, exactly as if you'd apt installed it
Use host networking for ultra-low-latency workloads (game servers, DNS servers, load balancers handling millions of connections), WireGuard, or anything that needs to see the real client IP without reverse-proxy shenanigans. The downside is zero isolation between the container and the host network, and you lose Docker's port-publishing abstraction.
none — fully isolated
No network at all. Useful for batch jobs that must not reach the internet.
docker run --rm --network none alpine ip addr
Only lo is present
For multi-host networking, the built-in overlay driver works with Swarm mode, but if you are at that scale you are probably better served by Kubernetes.
Inspect your networks:
docker network lsExpected output:
NETWORK ID NAME DRIVER SCOPE
a1b2c3d4e5f6 bridge bridge local
7g8h9i0j1k2l host host local
3m4n5o6p7q8r none null localStep 9: Install Portainer (Optional GUI)
Portainer gives you a web UI for managing containers, images, volumes, networks and Compose stacks — useful if you prefer clicking over SSHing, or if non-developers on your team need read-only visibility.
Create a persistent volume and launch the community edition:
docker volume create portainer_data
docker run -d \ --name portainer \ --restart=always \ -p 9443:9443 \ -v /var/run/docker.sock:/var/run/docker.sock \ -v portainer_data:/data \ portainer/portainer-ce:latest
Open https://your-server-ip:9443 in a browser (the CE image generates a self-signed cert on first boot), create the admin user within five minutes of starting the container, and pick "Docker Socket" as the environment to manage.
For a walkthrough on hardening the Portainer install behind a reverse proxy with a real TLS certificate, see our dedicated guide on installing Portainer.
Step 10: Set Up a Pruning Policy
Docker's append-only model means /var/lib/docker grows forever unless you clean it. Stopped containers, dangling images, unused volumes and build cache accumulate quickly on a busy CI host.
Manual pruning
Inspect what Docker is using:
docker system dfExpected output:
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 18 5 3.2GB 2.1GB (65%)
Containers 7 4 112MB 43MB (38%)
Local Volumes 9 6 845MB 210MB (24%)
Build Cache 41 0 1.4GB 1.4GBAggressive one-shot cleanup — removes stopped containers, unused networks, dangling images and build cache older than a week:
docker system prune -af --filter "until=168h"Drop unused volumes separately (volumes are never auto-pruned by docker system prune because they usually hold data):
docker volume ls -qf dangling=true | xargs -r docker volume rmAutomated weekly pruning
Create a systemd service and timer so you never have to think about it.
sudo tee /etc/systemd/system/docker-prune.service > /dev/null <<'EOF' [Unit] Description=Weekly Docker prune After=docker.service Requires=docker.service
[Service] Type=oneshot ExecStart=/usr/bin/docker system prune -af --filter "until=168h" ExecStart=/bin/sh -c "docker volume ls -qf dangling=true | xargs -r docker volume rm" EOF
sudo tee /etc/systemd/system/docker-prune.timer > /dev/null <<'EOF' [Unit] Description=Run docker-prune weekly[Timer] OnCalendar=Sun 03:00 Persistent=true
[Install] WantedBy=timers.target EOF
Enable the timer:
sudo systemctl daemon-reload
sudo systemctl enable --now docker-prune.timer
sudo systemctl list-timers docker-prune.timerExpected output:
NEXT LEFT LAST PASSED UNIT ACTIVATES
Sun 2026-04-19 03:00:00 UTC 2 days left - - docker-prune.timer docker-prune.serviceA sensible policy for most VPS hosts: prune weekly, keep images used in the last 7 days, drop dangling volumes, and alert only if docker system df shows reclaimable space above 10 GB.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
permission denied while trying to connect to the Docker daemon socket | Your user is not in the docker group, or the group change has not taken effect in this shell | Run sudo usermod -aG docker $USER, then log out and back in — or run newgrp docker |
Cannot connect to the Docker daemon at unix:///var/run/docker.sock | Daemon is not running | Check with sudo systemctl status docker and start with sudo systemctl start docker; check logs via sudo journalctl -u docker -n 100 --no-pager |
could not find an available, non-overlapping IPv4 address pool | Default Docker pools conflict with a VPN or host route | Set default-address-pools in /etc/docker/daemon.json to a free range like 10.200.0.0/16 |
pull access denied for <image>, repository does not exist or may require 'docker login' | Typo in image name, private repo, or rate limit from an anonymous Docker Hub pull | Verify the image name; run docker login for private registries; consider a mirror or registry proxy if you hit Hub's anonymous pull limits |
hello-world runs but iptables rules are missing | Daemon started before nftables/iptables backend was chosen | On 24.04 ensure iptables is in legacy mode: sudo update-alternatives --set iptables /usr/sbin/iptables-legacy, then restart docker |
| Disk filling up with no obvious cause | /var/lib/docker accumulating logs, images, build cache | Run docker system df to see the breakdown, then docker system prune -af --filter "until=168h" and enable the weekly timer from Step 10 |
Rootless daemon fails to start with newuidmap: write to uid_map failed | Missing uidmap package or /etc/subuid / /etc/subgid not configured for your user | Install uidmap and verify your user has ranges: grep $USER /etc/subuid /etc/subgid |
sudo journalctl -u docker -fAnd the per-container log:
docker logs -f --tail 200 <container>FAQ
Should I install docker.io from Ubuntu or Docker CE from docker.com?
Always install Docker CE from docker.com on a server. The docker.io package in the Ubuntu archive is a community snapshot that typically lags upstream by 3-9 months, ships without the Compose v2 and Buildx plugins, and has no rootless tooling. When a CVE lands in containerd or runc, the upstream Docker CE repo gets the patched build on day one while Ubuntu can take weeks. For development laptops docker.io is fine; for a VPS hosting real workloads, Docker CE is the only reasonable choice.
Do I need to reboot after installing Docker?
No. The Docker engine starts immediately after apt install and is ready to run containers. You only need to log out and back in (or run newgrp docker) after adding your user to the docker group — and that's a shell-level requirement, not a reboot. The sole exception is if you also upgraded the kernel during Step 1; new cgroup or overlay features may only take effect after a reboot.
What is the difference between rootless Docker and the default daemon?
The default daemon runs as root. Anyone in the docker group can mount the host root filesystem into a container and effectively has root access. Rootless Docker runs dockerd as an unprivileged user inside a user namespace — a container escape maps back to your unprivileged UID, not host root. The costs: slirp4netns networking is slightly slower than the bridge driver, binding to ports below 1024 needs extra capabilities, and some storage drivers (like devicemapper) are unavailable. For multi-tenant CI runners or anything running untrusted code, rootless is worth it. For a single-developer VPS, the default daemon plus a firewall is usually enough.
Docker vs Podman — which should I pick on Ubuntu 24.04?
Docker has the larger ecosystem, first-party Compose v2, BuildKit/Buildx, the widest third-party tooling, and is the format every CI/CD product understands. Podman is daemonless and rootless-by-default, has a Docker-compatible CLI, and groups containers into pods without needing Kubernetes. If you are deploying Compose stacks, using Portainer, or running anything that expects /var/run/docker.sock, pick Docker. Pick Podman when you need strict rootless semantics, no long-running daemon, or pod-style grouping and you control the whole toolchain.
How much RAM do I really need to run Docker on a VPS?
The Docker daemon itself idles at around 100-150 MB of RAM. Real memory usage is entirely driven by your containers. A minimal stack — Caddy or Nginx, a Postgres database, a single Node or Python app, maybe Redis — fits comfortably in 2 GB. Add a Grafana/Prometheus observability stack and you want 4 GB. Heavier workloads (Elasticsearch, Mautic, Jitsi, GitLab) push you to 8-16 GB. Disk is the other dimension: plan for 20 GB minimum and watch /var/lib/docker over time.
How do I clean up unused images and volumes safely?
The one-liner for a weekly sweep is docker system prune -af --filter "until=168h" — this removes stopped containers, unused networks, dangling images and build cache older than seven days. It does not touch named volumes, which almost always hold data you care about. For volumes, use docker volume ls -qf dangling=true | xargs -r docker volume rm — this only removes volumes not currently attached to any container. The pruning policy in Step 10 wraps both into a systemd timer that runs every Sunday at 03:00.
Why does Docker fail with "could not find an available, non-overlapping IPv4 address pool"?
Docker allocates subnets for user-defined networks from its default-address-pools (out of the box, 172.17.0.0/16 and 172.18.0.0/16). If your host already has a route for that range — a corporate VPN, a WireGuard tunnel, a provider's private-network interface — Docker refuses to create overlapping networks. The fix is to move Docker's pools somewhere safe. Add "default-address-pools": [{"base": "10.200.0.0/16", "size": 24}] to /etc/docker/daemon.json and restart the daemon. You get the same behaviour, just on a range that does not collide.
Next Steps
Now that Docker is installed and hardened, here is how to build on it:
- Learn Docker Compose v2 — Our guide on installing and using Docker Compose covers multi-service stacks,
.envfiles, profiles, healthchecks, and production deploys viadocker compose up -d --remove-orphans. - Put a UI on it — Install Portainer to manage containers, stacks and volumes through a browser, with role-based access for your team.
- Consider Podman for untrusted workloads — If you are running third-party or user-submitted code, our Podman vs Docker comparison shows when the daemonless, rootless-by-default design pays off.
- Graduate to Kubernetes when the time comes — Once you have a dozen servers or need true multi-host orchestration, read our introduction to Kubernetes on a VPS to see what changes and what carries over.
- Follow the upstream docs — Docker's official documentation at docs.docker.com is the canonical reference for the engine, CLI, Compose, BuildKit and API.
Need a VPS that's ready for Docker today?>
Our Starter VPS gives you full root on NVMe-backed KVM hardware from EUR 7.99/month — perfect for Docker Compose stacks, self-hosted apps, and small-team CI runners. Scale up to Professional or Performance plans as your container count grows.>
- 4 vCPU, 8 GB RAM, 100 GB NVMe on the Starter tier
- Unmetered bandwidth, no per-container fees
- Ubuntu 24.04 LTS available at deploy time
- SSH key provisioning and IPv6 on every plan>
Deploy your Docker VPS now.