There are three ways to put Docker on an Ubuntu server, and two of them will cause you grief later. apt install docker.io pulls an older, community-packaged build. The snap package runs Docker inside a confinement sandbox that quietly breaks bind mounts and socket paths. The method that actually matches the docs and behaves predictably is Docker’s own apt repository.
This guide walks through that official method end to end: removing any old packages, adding Docker’s GPG key and repo, installing Docker Engine with the Compose plugin, running Docker without sudo, making sure it survives a reboot, and confirming the whole thing works with a test container.
Everything here is for Ubuntu Server (20.04, 22.04, or 24.04). The commands are the same across those releases because the setup reads your version codename automatically. You’ll need a user with sudo rights and a few minutes.
Remove old or conflicting packages first
If this is a brand-new server, you can skip ahead. But if anyone ever ran apt install docker.io or installed the snap, clear it out before adding the official repo. Mixing package sources is where weird, hard-to-debug breakage comes from.
Remove the old apt packages:
sudo apt remove docker docker-engine docker.io containerd runc
Don’t worry if apt says some of those aren’t installed — it’s just making sure. This command removes the packages but leaves /var/lib/docker alone, so any existing images or volumes stay put.
If Docker came in as a snap, remove that too:
sudo snap remove docker
Add the prerequisites and Docker’s GPG key
Docker’s repo is signed, so apt needs the signing key on disk before it’ll trust packages from it. First install the small set of tools used to fetch the key over HTTPS:
sudo apt update
sudo apt install ca-certificates curl gnupg
Now create the keyring directory and download Docker’s official GPG key into it:
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
The /etc/apt/keyrings path matters. Older guides drop keys into apt-key or /etc/apt/trusted.gpg.d, but apt-key is deprecated and the modern, scoped approach is a dedicated keyring referenced directly by the repo entry. That’s what the next step does.
Add the official Docker repository
This one command writes the repo definition, automatically filling in your architecture and Ubuntu codename:
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/null
A few things are happening in that line worth understanding:
arch=$(dpkg --print-architecture)insertsamd64,arm64, etc. so you get packages for your CPU.signed-by=...points at the key you just saved, scoping trust to this repo.$(. /etc/os-release && echo "$VERSION_CODENAME")expands tojammy,noble, or whatever your release is called, so you never hardcode the version.
Then refresh apt so it reads the new repo:
sudo apt update
If apt update errors here, it’s almost always the key or codename. Re-check that /etc/apt/keyrings/docker.asc exists and is readable, and that your Ubuntu release is one Docker actually publishes for.
Install Docker Engine and the Compose plugin
Now the actual install. This pulls Docker Engine, the CLI, containerd, Buildx, and the Compose v2 plugin in one go:
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Here’s what each package is for:
What you're installing
| docker-ce | Docker Engine — the daemon that builds and runs containers. |
|---|---|
| docker-ce-cli | The docker command-line client you actually type into. |
| containerd.io | The container runtime Docker sits on top of. |
| docker-buildx-plugin | Modern image builder; powers docker buildx and faster builds. |
| docker-compose-plugin | Compose v2, used as docker compose (a space, not a hyphen). |
Because docker-compose-plugin is in that list, you do not need to install Compose separately. You’ll call it as docker compose. If you’ve used the old docker-compose (one word, hyphen) before, that was the deprecated Python tool — the plugin replaces it.
Run Docker without sudo
Right after install, docker commands only work as root because the daemon’s socket is owned by root. You can prefix everything with sudo, but the cleaner fix is to add your user to the docker group:
sudo usermod -aG docker $USER
This change doesn’t apply to your current shell session. You have to start a fresh login so the new group membership is picked up:
# Log out and back in (SSH: disconnect and reconnect), or for the current shell:
newgrp docker
Verify the group took effect — you should see docker in the output:
groups
Make sure Docker starts on boot
The apt package registers Docker as a systemd service and enables it, so it should already start at boot. Confirm rather than assume:
systemctl is-enabled docker
systemctl status docker
is-enabled should print enabled, and status should show active (running). If for some reason it isn’t enabled, turn it on and start it in one command:
sudo systemctl enable --now docker
There’s a second layer people forget. Enabling the Docker service means the daemon comes back after a reboot — it does not automatically restart your containers. For a container to survive a reboot, it needs a restart policy. With docker run, add --restart unless-stopped; in Compose, set restart: unless-stopped on the service. If you’re new to Compose, the Docker Compose beginner guide shows exactly where that line goes.
Restart policies worth knowing
| no | Default. Container never restarts on its own. |
|---|---|
| unless-stopped | Restarts on failure and after a reboot, unless you stopped it manually. Good default for services. |
| always | Always restarts, even one you stopped manually (restarts again after daemon restart). |
| on-failure | Restarts only if the container exits with an error code. |
Verify the install with hello-world
The traditional smoke test pulls and runs a tiny image that prints a message and exits:
docker run hello-world
If everything is wired up, Docker pulls the hello-world image, runs it, and you see a message starting with “Hello from Docker!” That single command proves four things at once: the daemon is running, your user can reach the socket, image pulls work, and the runtime can start a container.
Check your versions while you’re here:
docker version
docker compose version
docker compose version should report v2.x.x. If that command works, the Compose plugin installed correctly and you’re set for multi-container setups.
Post-install verification
- docker run hello-world prints the success message
- docker version shows both Client and Server (the daemon is reachable)
- docker compose version reports v2.x.x
- You can run docker without sudo after re-login
- systemctl is-enabled docker says 'enabled'
A quick first container
hello-world proves the plumbing, but it exits immediately. Run something that stays up so you can see Docker actually serving traffic. This starts Nginx on port 8080:
docker run -d --name web --restart unless-stopped -p 8080:80 nginx
Then check it from the server itself:
curl http://localhost:8080
You should get back Nginx’s default HTML. When you’re done, stop and remove it:
docker stop web
docker rm web
From here, most real workloads use more than one container — an app plus a database, say — which is exactly what Compose is for. Once you’ve got a few services running, putting a reverse proxy in front of them lets you host several apps on one server behind ports 80 and 443.
Common install problems
A handful of errors come up again and again on a fresh Ubuntu install.
permission denied while trying to connect to the Docker daemon socket — You haven’t logged out and back in after usermod -aG docker. Group changes need a new login session. Run newgrp docker for the current shell or reconnect your SSH session.
docker: command not found — The install didn’t complete, usually because apt update failed earlier on the repo. Re-check the keyring file and the docker.list repo entry, run apt update again, then reinstall.
Cannot connect to the Docker daemon — The daemon isn’t running. Start it with sudo systemctl start docker and check systemctl status docker for the reason it stopped.
A port conflict when starting a container — Something already holds the port you’re publishing. That’s common enough to have its own walkthrough: see how to fix “port is already allocated”.
Wrapping up
The official apt repository is the install method worth using on Ubuntu Server. It gives you current Docker Engine, the maintained Compose plugin, and behavior that matches every official doc — no snap confinement surprises, no stale docker.io build. The sequence is always the same: clear old packages, add the key and repo, install the docker-ce set, fix the group so you skip sudo, confirm it’s enabled on boot, and test with hello-world.
With Docker running, the natural next step is multi-container apps. Start with the Docker Compose beginner guide, and browse the rest of the Docker & Containers guides when you’re ready to go further.