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.
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.
- Add the repo and install Docker Engine plus the Compose plugin.
- Verify with
hello-worldand a real nginx container. - Drop the sudo: add your user to the
dockergroup and log back in. - 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.
- 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
dockerwith nosudo - A real container serving traffic, not just
hello-world - The four errors that trip people up, and the one-line fix for each
- 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 nosudo. On a normal user account, putsudoin 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
nginxorubuntu. You pull it once, run it many times. - Compose plugin
- The
docker composecommand. It runs a whole stack of containers from onecompose.yamlfile instead of longdocker runlines. - 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
dockerwithoutsudo. 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
dockergroup 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:~# 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:~# 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.
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:~$ 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.
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:
| Method | Docker version | Compose plugin? | The catch |
|---|---|---|---|
apt install docker.ioUbuntu'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.comthe 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:~# 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:~# ufw status Status: active To Action From -- ------ ---- 22/tcp (OpenSSH) ALLOW IN Anywhere # nothing about 8080
From my laptop · what answered
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.
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.
Related
Get the free Vibe Engineering Blocks guide
The exact building blocks I use to ship real products with AI — yours as a free PDF.
Questions & Discussion
Ask a question about this guide →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…