Install Docker on Ubuntu

Docker on Ubuntu is a five minute job, and there is exactly one path I use on a real server: Docker's official apt repository, with the Compose plugin, a non-root user, and a real check at the end. I ran every command below on a fresh Ubuntu 24.04 box, then broke it on purpose to catch the errors you will hit, so this page is the tested version, not the theory.

This is the guide the rest of my self-hosting guides point back to. If you are here to put an app on your own server, Docker is step one, and it is the same first step I teach in my Self Hosting 2.0 course.

TL;DR

Add Docker's official repository, then install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin. Add your user to the docker group and log back in so you can drop the sudo. Verify with docker run hello-world and a real container. Skip apt install docker.io (no Compose plugin) and snap (its sandbox blocks bind mounts). On my fresh box the official repo delivered Docker 29.7.2 and Compose v5.4.0.

The fast path
  1. Add the repo and install Docker Engine plus the Compose plugin.
  2. Verify with hello-world and a real nginx container.
  3. Drop the sudo: add your user to the docker group and log back in.
  4. Understand why the official repo beats docker.io, snap and the convenience script.

Every command is copy-paste. Already have Coolify or another Docker-based panel? It installed Docker for you already. Jump to the verify step to confirm, and skip the rest.

What you'll have at the end
  • Docker Engine, the Compose plugin (docker compose) and buildx, all current
  • The daemon running and set to start on every boot, confirmed
  • Your own user running docker with no sudo
  • A real container serving traffic, not just hello-world
  • The four errors that trip people up, and the one-line fix for each
Before you start
  • What you need: a machine running Ubuntu 22.04 or 24.04 and a user with sudo. A VPS, a home server, or a desktop all work the same way. My lab box was a fresh DigitalOcean droplet.
  • Tested on: Ubuntu 24.04 (Noble), Docker 29.7.2, Compose v5.4.0, on 11 August 2026.
  • Root vs sudo: I ran my lab as root, so the commands below show no sudo. On a normal user account, put sudo in front of each one.
New to Docker? The words on this page, in plain terms
Docker Engine
The background service (a "daemon") that builds and runs containers. It is the thing you are installing.
Container
One app plus everything it needs to run, boxed up so it behaves the same on any machine. A running copy of an image.
Image
The frozen template a container is started from, like nginx or ubuntu. You pull it once, run it many times.
Compose plugin
The docker compose command. It runs a whole stack of containers from one compose.yaml file instead of long docker run lines.
apt repository
A source Ubuntu's package manager installs from. Adding Docker's own means you get Docker's packages, not Ubuntu's older repackaged ones.
GPG key
A signature apt checks so it knows the packages really came from Docker and were not tampered with in transit.
docker group
A Unix group whose members can run docker without sudo. Being in it is as good as being root, so add trusted users only.
docker.sock
The socket file the Docker command talks to the daemon through. "Permission denied" on it means your user is not in the docker group yet.

Install Docker from the official repository

Five commands, in order. They add Docker's signing key, add Docker's repository, and install Docker Engine with the Compose plugin. Run them on the server. Add sudo in front of each if you are not root.

Step 1: prerequisites and the GPG key

Install the two tools apt needs to fetch over HTTPS, then download Docker's signing key into a folder apt trusts:

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

The key is what lets apt verify the packages are really Docker's. Skip it and the next apt update refuses the repository. That is the first error I reproduce below.

Step 2: add the repository

This one line writes Docker's repository into apt's source list. It fills in your architecture and your Ubuntu codename automatically, so the same command works on 22.04 and 24.04:

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" | \
  tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update

On my 24.04 box $VERSION_CODENAME resolved to noble, and apt update pulled the Docker index. If you copy a codename by hand from an old guide and get it wrong, apt throws a 404. That is the second error I cover later.

Step 3: install Docker Engine and Compose

Five packages: the engine, its CLI, the container runtime, the buildx plugin and the Compose plugin. This is the line that matters:

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

That is the whole install. The service starts itself and is set to come back on every reboot, so there is nothing else to enable. Here is exactly what my fresh box reported right after:

What the official repo installed

Straight off the lab box, seconds after the install finished.

root@ubuntu-24-04: ~
root@ubuntu:~# docker --version
Docker version 29.7.2, build a7dcaa6
root@ubuntu:~# docker compose version
Docker Compose version v5.4.0
root@ubuntu:~# docker buildx version
github.com/docker/buildx v0.36.1 1d8dde89b8aba914e05e45366770736fea1fd690
root@ubuntu:~# systemctl is-active docker; systemctl is-enabled docker
active
enabled

Engine, Compose and buildx in one install, and the daemon is already active and enabled on boot. Nothing to start by hand.

Verify it actually works

Two checks. The first proves the daemon runs. The second proves it runs something real, which is the check most guides skip.

Start with Docker's built-in smoke test. It pulls a tiny image and runs it:

docker run hello-world

You want the line Hello from Docker! in the output. If you see it, the daemon is up, the socket works, and image pulls work. If instead you get "permission denied", your user is not in the docker group yet, which is the next section.

Now run something that actually serves traffic, because hello-world exits instantly and never opens a port. Start nginx, publish it on port 8080, and curl it:

docker run -d --name web -p 8080:80 nginx:alpine
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://localhost:8080

A real container, not just hello-world

nginx running and answering, from the lab box.

root@ubuntu-24-04: ~
root@ubuntu:~# docker ps --format 'table \t\t\t'
NAMES     IMAGE          STATUS         PORTS
web       nginx:alpine   Up 3 seconds   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp
root@ubuntu:~# curl -s -o /dev/null -w "HTTP %{http_code}\n" http://localhost:8080
HTTP 200
root@ubuntu:~# docker info --format ' /  / cgroup '
29.7.2 / overlayfs / cgroup 2

A container came up, published a port, and returned HTTP 200. That is a working Docker install. Remove it with docker rm -f web when you are done.

Already running Coolify or Dokploy?

Those install Docker as part of their own setup, so you do not run this guide first. If you want the managed-panel path instead of raw Docker, start with my Coolify install guide. This page is for when you want Docker directly, which is what most app guides on this site assume.

Run Docker without sudo

Out of the box, only root talks to Docker. As a normal user your first command fails like this:

The permission-denied error, and its fix

A real non-root user on the lab box, before and after.

demo@ubuntu-24-04: ~
demo@ubuntu:~$ docker ps
permission denied while trying to connect to the docker API at unix:///var/run/docker.sock

The fix is to add your user to the docker group. Run this (with sudo if you are not root), swapping $USER for a name if you mean another account:

usermod -aG docker $USER

Then comes the part that trips almost everyone. The group change does not apply to a shell you already have open. Your current terminal keeps the group list it started with, so docker ps still fails right after you run usermod. You have to start a fresh login session: log out and back in, or close the SSH connection and reconnect.

If you do not want to log out this second, newgrp docker (or sg docker -c '...') opens a shell with the new group applied. I proved both on the box: the stale shell stayed denied, a new login worked, and sg docker worked without logging out.

The docker group is root, said plainly

Adding a user to the docker group gives that user root on the whole machine, not just "permission to run Docker". A group member can mount any file from the host into a container and read it. To prove it is not hand-waving, I had my non-root demo user read /etc/shadow, the file that holds every password hash, with no sudo anywhere:

demo@ubuntu (in the docker group, no sudo)$ docker run --rm -v /etc/shadow:/host-shadow:ro alpine head -1 /host-shadow
root:*:16176:0:99999:7:::

So only add people you would hand root to anyway. If that is not acceptable, look at rootless Docker, which runs the daemon as your user instead.

Why the official repo, not docker.io or snap

Optional depth. The install above is done. This section is the "why", for when someone tells you to just run apt install docker.io. Skip it if you only came for the commands.

There are four common ways to get Docker onto Ubuntu. I installed each one on its own fresh 24.04 box so nothing carried over, and wrote down exactly what landed. Here is the whole thing in one table:

MethodDocker versionCompose plugin?The catch
apt install docker.io
Ubuntu's own package
29.1.3 No No Compose plugin and no buildx. docker compose is "unknown command"; you install a separate docker-compose-v2 package for it. Packaged by Ubuntu, not Docker.
snap install docker 29.6.1 Yes (v5.3.1) The snap sandbox blocks bind mounts outside your home. Mounting /opt failed with read-only file system. Data lives under /var/snap/docker/, not /var/lib/docker, which surprises every tutorial's paths.
get.docker.com
the convenience script
29.7.2 Yes (v5.4.0) Installs the same packages, because it configures the official repo for you. But its own header says "not recommended for production", it never asks before installing, and it always pulls latest.
Official apt repo
the steps above
29.7.2 Yes (v5.4.0) Engine, Compose and buildx together, packaged by Docker, on Docker's release cadence. You control exactly what is installed and can pin a version. This is the one.

Two honest notes on that table. First, the old "Ubuntu ships an ancient Docker" line is overstated in 2026: docker.io was 29.1.3 against the official 29.7.2, a small gap. The real reason to skip it is the missing Compose plugin and buildx, not the version. Second, the snap sandbox failure is not theoretical. Here it is on the box:

Why I don't use the snap on a server

The same two bind mounts, one inside home, one in /opt.

root@snap-box: ~
root@snap-box:~# docker run --rm -v /root/test:/data alpine cat /data/f.txt
from-home
root@snap-box:~# docker run --rm -v /opt/test:/data alpine cat /data/f.txt
docker: Error response from daemon: error while creating mount source path
'/opt/test': mkdir /opt/test: read-only file system

A bind mount under /root works; the identical mount under /opt is refused. On a server where your data lives in /opt or /srv, that is a wall you hit on day one.

Knowing which package actually lands, and where it puts your data, is most of self-hosting. It is why Self Hosting 2.0 builds the stack from the base up instead of handing you a script to paste: a tool you installed on purpose is one you can fix later.

Common errors, reproduced and fixed

I triggered each of these on the box on purpose, so the error text below is real, not remembered. If you hit one, match the message and apply the fix.

docker: permission denied on /var/run/docker.sock

Your user is not in the docker group, or you have not started a new session since you were added. Fix: usermod -aG docker $USER, then log out and back in. See the non-root section for the stale-shell catch.

docker-compose: command not found

You are typing the old v1 name. docker-compose with a hyphen was the standalone Python tool and it is not installed any more. The current command is docker compose with a space, which ships in docker-compose-plugin:

the v1 name is gone, the v2 plugin is there$ docker-compose version
bash: docker-compose: command not found
$ docker compose version
Docker Compose version v5.4.0

apt: 404 Not Found on the Docker repository

The Ubuntu codename in your docker.list is wrong, usually copied from an old guide. I set it to a codename that does not exist and apt answered plainly:

apt update with a bad codenameErr:8 https://download.docker.com/linux/ubuntu nobleee Release
  404  Not Found [IP: 143.204.181.22 443]

Fix: open /etc/apt/sources.list.d/docker.list and set the codename to your real one (. /etc/os-release && echo $VERSION_CODENAME prints it, e.g. noble), then apt update again.

NO_PUBKEY: the repository is not signed

You added the repo but the GPG key is missing or unreadable. apt refuses to trust it:

apt update with the key missingErr:2 https://download.docker.com/linux/ubuntu noble InRelease
  The following signatures couldn't be verified because the public key
  is not available: NO_PUBKEY 7EA0A9C3F273FCD8

Fix: re-run the key step from Step 1 so /etc/apt/keyrings/docker.asc exists and is world-readable, then apt update.

Cannot connect to the Docker daemon

This one has a twist worth knowing. On a systemd Ubuntu, stopping only docker.service does not actually stop Docker: a socket unit restarts the daemon the moment you run a command. You only get the classic dead-daemon error when the daemon is genuinely down:

daemon actually stopped$ docker ps
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

Fix: systemctl start docker. If it will not stay up, journalctl -u docker --no-pager -n 50 shows why (usually a bad /etc/docker/daemon.json).

One security note before you expose anything

One thing I want you to know before you publish a container port, because it surprises people and no install guide mentions it. When you run -p 8080:80, Docker opens that port to the whole internet by writing its own firewall rule, and Ubuntu's firewall does not stop it.

On the lab box I turned ufw on, allowed only SSH, and left the nginx container published on 8080. ufw says nothing is allowed on 8080. Then I probed the box from my own laptop, from outside:

ufw says closed. The internet says open.

The firewall's guest list next to what a laptop actually reached.

On the server · what ufw allows

root@ubuntu: ~
root@ubuntu:~# ufw status
Status: active

To                Action    From
--                ------    ----
22/tcp (OpenSSH)  ALLOW IN  Anywhere

# nothing about 8080

From my laptop · what answered

you@laptop: ~
you@laptop:~$ probe 167.71.36.229
port 22   : OPEN
port 8080 : OPEN
port 9999 : closed
http://167.71.36.229:8080 -> HTTP 200

Port 8080 was never allowed in ufw, yet it answered from the public internet with the nginx page. Docker's published-port rules run before ufw's, so ufw never sees the packet.

This matters because a fresh server is found fast. On another run I left a brand-new droplet exposed and the first uninvited SSH attempt arrived 2 minutes and 35 seconds after boot. Bots scan the whole internet constantly; nobody has to know your box exists.

The short version: only publish ports you mean to publish, and if you run Docker behind ufw and expect it to filter container ports, it will not. I wrote the full fix, proven on a real box, in the guide on securing a Docker/Coolify VPS. Read it before you put anything public on your server.

Uninstall Docker cleanly

If you want Docker gone, purging the packages is only half of it. The images, containers and volumes live in a data directory that the purge leaves untouched. Remove both:

apt-get purge -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
rm -rf /var/lib/docker /var/lib/containerd

On my box the purge removed the binaries but /var/lib/docker and /var/lib/containerd were still on disk afterwards, holding every image and volume, so the rm -rf is the line that actually frees the space. To remove the repository too, delete /etc/apt/sources.list.d/docker.list and /etc/apt/keyrings/docker.asc.

What this guide does not cover

To keep this the tight prerequisite it is meant to be, I left some things for their own pages:

  • Using Docker (images, volumes, networks, writing a compose.yaml) gets its own guides. This one stops at "installed and verified".
  • Docker Desktop, the GUI. On a server you want the Engine, which is what this installs.
  • Other distros. This is Ubuntu. Debian is close but the repo URL and codenames differ.
  • Rootless Docker beyond the pointer above, and custom daemon or TCP-socket configuration.

Questions people actually ask

Short answers to the things that come up most, all backed by the same lab run.

Do I need sudo to run Docker?

Only until your user is in the docker group. After usermod -aG docker $USER and a fresh login, you run docker with no sudo. Remember that group membership is root-equivalent, so add trusted users only.

Which Ubuntu versions work?

Docker's official packages cover the supported LTS releases and the latest interim one, which today means 24.04, 22.04 and still-supported 20.04, on 64-bit x86 and arm. The repository line in this guide detects your codename, so the same steps work across them.

Can I install a specific Docker version?

Yes. List what the repo offers with apt-cache madison docker-ce, then install a pinned version, for example apt-get install docker-ce=<version-string> docker-ce-cli=<version-string>. This is one reason to use the apt repo over the convenience script: the script always installs latest.

docker-compose or docker compose?

docker compose with a space, every time. The hyphenated docker-compose is the retired v1 and is not installed by the current packages.

Is get.docker.com safe?

It comes from Docker and installs the same official packages, but Docker's own script header says it is not recommended for production. For a server you keep, run the apt steps yourself so you control the install and can pin versions.

Tested on: Ubuntu 24.04 (Noble) · Docker 29.7.2 · Compose v5.4.0 · buildx 0.36.1 · containerd 2.3.3 · DigitalOcean droplet, 1 vCPU / 2 GB, Frankfurt.
Lab run: 2026-08-11. Last verified: 2026-08-11. Every version number, error message and port result on this page came off that server. The four install methods were each measured on their own fresh box.

Docker is step one. Here's the rest of the path.

You have Docker. In Self Hosting 2.0 I take an empty VPS all the way to a stack you run yourself: Docker, a panel to manage it, real apps, backups, email, and the firewall work that makes it safe to expose, in the order it should actually happen. 34 lessons, nothing skipped between them.

Hasan Aboul Hasan giving a thumbs up

Use the official repo. Verify from outside.

Hasan Aboul Hasan builds open-source tools and teaches solo developers how to build, host, and sell AI-powered products. Founder of LearnWithHasan.com, creator of SimplerLLM and PyRunner.

Vibe Engineering Blocks — free guide
Free guide

Get the free Vibe Engineering Blocks guide

The exact building blocks I use to ship real products with AI — yours as a free PDF.

Free PDF · double opt-in · unsubscribe anytime.

Have a question? Ask it in the community — it's tagged #guide and linked back here. Reading is open to everyone; posting needs a free account.

Loading questions…