How do you install Docker on Ubuntu or Debian so that it updates along with the system and holds no surprises? There is one reliable way: the official Docker repository. This guide walks through installing Docker Engine, Compose and Buildx on Ubuntu 26.04, 24.04 and Debian 13 step by step, the settings worth applying right after installation, rootless mode, version pinning and complete removal.
Tested on October 11, 2026 on Ubuntu 24.04, Ubuntu 26.04 and Debian 13: Docker Engine 29.9.0, containerd 2.4.1, Docker Compose 5.6.0, Buildx 0.38.0. The commands come from the Docker documentation for Ubuntu and for Debian.
Prerequisites
- A server running 64-bit Ubuntu 24.04, 26.04 or Debian 13 (amd64 or arm64) and an
adminuser withsudoprivileges; see initial server setup. - KVM virtualization. On container-based VPS (OpenVZ, LXC) Docker often does not work; how to check is described in VPS vs VDS: what is the difference.
Which installation method to choose
There are four ways to install Docker. Versions as of October 11, 2026:
| Source | Ubuntu 24.04 | Ubuntu 26.04 | Debian 13 |
|---|---|---|---|
Docker repository (docker-ce) |
29.9.0 | 29.9.0 | 29.9.0 |
Distribution repository (docker.io) |
29.1.3 | 29.1.3 | 26.1.5 |
Snap (docker, stable channel) |
29.8.0 | 29.8.0 | — |
The docker.io package from the distribution repository is built by Ubuntu and Debian maintainers. It gets security fixes but not new versions: on Debian 13 it is Docker 26.1, three major versions behind the current one. Compose for it is a separate package (docker-compose-v2 on Ubuntu, docker-compose on Debian) and lags behind too. Documentation, forum answers and the images themselves assume a recent Docker; with an old one, some features will not work.
Snap is built by Canonical. The version is recent, but the package has its own limitations (package README): Docker only sees files inside the home directory, its configuration lives in /var/snap/docker/current/config/daemon.json instead of /etc/docker/daemon.json, and --security-opt no-new-privileges prevents a container from starting. On top of that, snaps update automatically, and the Docker service restarts at a moment you did not choose.
The get.docker.com script installs packages from the same Docker repository, but Docker itself does not recommend it for production: the script does not let you choose a version and performs many actions without asking.
The rest of this guide installs from the Docker repository: recent versions, updates via apt, standard paths and vendor support.
Step 1. Remove conflicting packages
If the server already has docker.io, containerd or runc from the distribution repository, they will conflict with the official packages. Remove them.
On Ubuntu:
sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc | cut -f1)
On Debian:
sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-doc docker-buildx podman-docker containerd runc | cut -f1)
dpkg --get-selections keeps only the installed packages in the list. On a server with docker.io, the command shows:
The following packages will be REMOVED:
containerd docker-compose-v2 docker.io runc
If none of the packages are installed, dpkg prints no packages found matching … and apt reports there is nothing to remove. That is expected.
Images, containers and volumes in /var/lib/docker are not deleted. If you do not need the old data, remove it too; the commands are in the "How to uninstall Docker" section below.
Step 2. Add the Docker apt repository
The key that signs the packages goes to /etc/apt/keyrings, and the repository definition goes to /etc/apt/sources.list.d/docker.sources in deb822 format. The Signed-By line allows this key to sign only the Docker repository, not any package on the system.
Ubuntu 24.04 and 26.04:
sudo apt update
sudo apt 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
sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
Debian 13 is the same, but with the linux/debian URL:
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
The commands inside $( … ) fill in the system codename and architecture. Check the result:
cat /etc/apt/sources.list.d/docker.sources
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: noble
Components: stable
Architectures: arm64
Signed-By: /etc/apt/keyrings/docker.asc
Suites should be noble for Ubuntu 24.04, resolute for 26.04 and trixie for Debian 13; Architectures should be amd64 or arm64. The apt update output will include lines with download.docker.com and no errors.
Step 3. Install Docker Engine
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
What gets installed:
docker-ce: Docker Engine itself, thedockerdservice;docker-ce-cli: thedockercommand;containerd.io: the runtime that runs containers (containerd and runc);docker-buildx-plugin: image builds (docker buildx, also used bydocker build);docker-compose-plugin: Compose as thedocker composesubcommand.
The Docker service starts right after installation. Check the version and run a test container:
sudo docker version --format '{{.Server.Version}}'
sudo docker run --rm hello-world
29.9.0
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
…
Hello from Docker!
This message shows that your installation appears to be working correctly.
If you see Hello from Docker!, Docker pulled the image from Docker Hub, created a container and ran it. The installation works.
Start Docker on boot
Packages from the Docker repository enable their services at boot on their own:
systemctl is-enabled docker containerd
enabled
enabled
We verified this with a reboot on all three systems: after startup, systemctl is-active docker returns active. If you see disabled, enable them manually:
sudo systemctl enable --now docker.service containerd.service
Containers come back after a reboot only if they have a restart policy (--restart unless-stopped or restart: in Compose); see Docker Compose in production for details.
Run docker without sudo: the docker group
Without sudo, a regular user gets an error:
permission denied while trying to connect to the docker API at unix:///var/run/docker.sock
To work without sudo, add the user to the docker group (the package creates it):
sudo usermod -aG docker admin
The new group only takes effect in a new session: log out of the server and log back in. Check:
id # the group list should include docker
docker run --rm hello-world
The docker group is equivalent to root. Any member can start a container, mount any system directory into it and read or change anything there. For example, a group member without sudo reads the password hash file that only root can access:
docker run --rm -v /etc:/host-etc alpine head -c 40 /host-etc/shadow
root:*:20713:0:99999:7:::
daemon:*:20713
The same way, one can modify /etc/sudoers or add an SSH key for root. So only add users you would trust with sudo, and for automated deployment under a separate deploy user, remember that this user's key effectively grants root. See Docker daemon attack surface in the documentation. If you need Docker without these privileges, see rootless mode below.
Docker Compose and Buildx
Compose and Buildx are already installed as plugins:
docker compose version
docker buildx version
Docker Compose version v5.6.0
github.com/docker/buildx v0.38.0 0dbe87f36ed472fe36b6796c3f70b4210a731fc5
The command is written with a space: docker compose. The old standalone docker-compose program (with a hyphen, version 1) has not been developed for a long time; do not install it. If your scripts use docker-compose, replace it with docker compose: the file format is the same.
Limit log size in /etc/docker/daemon.json
By default, Docker stores container output in JSON files with no size limit, and over a few months they can fill the whole disk. Set up rotation right after installation, while there are no containers yet:
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
EOF
sudo dockerd --validate --config-file /etc/docker/daemon.json
sudo systemctl restart docker
dockerd --validate checks the file before the restart and replies configuration OK. Always do this: with a JSON error, the Docker service will not start. Check that the setting took effect:
docker info --format '{{.LoggingDriver}}' # json-file
docker run -d --name logtest alpine sleep 60
docker inspect -f '{{.HostConfig.LogConfig}}' logtest # {json-file map[max-file:3 max-size:10m]}
docker rm -f logtest
Now each container gets at most three files of 10 MB. The setting only applies to containers created after it. Restarting the Docker service also restarts running containers.
Pin the Docker version
Normally Docker is updated along with the system via apt upgrade. If you need a specific version, for example the same as on your other servers, list the available ones:
apt list --all-versions docker-ce
docker-ce/trixie,now 5:29.9.0-1~debian.13~trixie arm64 [installed]
docker-ce/trixie 5:29.8.2-1~debian.13~trixie arm64
docker-ce/trixie 5:29.8.1-1~debian.13~trixie arm64
Install the one you need and hold it:
VERSION_STRING=5:29.8.2-1~debian.13~trixie
sudo apt install -y --allow-downgrades docker-ce=$VERSION_STRING docker-ce-cli=$VERSION_STRING containerd.io docker-buildx-plugin docker-compose-plugin
sudo apt-mark hold docker-ce docker-ce-cli
Copy the version string from the apt list output on your own server: it includes the OS version (ubuntu.24.04~noble, ubuntu.26.04~resolute, debian.13~trixie). --allow-downgrades is only needed when rolling back to an older version. Now apt upgrade skips these packages:
Not upgrading:
docker-ce docker-ce-cli
To release the hold and update to the latest version:
sudo apt-mark unhold docker-ce docker-ce-cli
sudo apt install -y docker-ce docker-ce-cli
On a single server there is usually no reason to pin a version; pinning helps when several servers must behave identically. Remember to release the hold, or the server will be left without security fixes.
Rootless mode: Docker without root
In rootless mode, the Docker daemon runs as a regular user instead of root. If a process escapes a container, it ends up on the server with that user's privileges. Who needs it: servers where several people use Docker, and anyone who does not want to grant root through the docker group. For a single server of your own, the regular mode is enough.
Limitations (documentation): ports below 1024 (including 80 and 443) cannot be published without extra configuration, overlay networks do not work, data is stored in the user's home directory (~/.local/share/docker), and each user has their own images and containers.
Rootless mode replaces the regular one, so stop the system service first:
sudo systemctl disable --now docker.service docker.socket
sudo rm -f /var/run/docker.sock
Install uidmap (tools for user namespaces) and enable lingering so the user's services run without a login session:
sudo apt install -y uidmap dbus-user-session
sudo loginctl enable-linger admin
Continue as admin. Log in over SSH as that user directly: through su or sudo -i, the user's systemd instance may be unavailable.
dockerd-rootless-setuptool.sh install
The script creates a user service docker.service, enables it and switches the docker command to a new rootless context. The end of the output:
[INFO] Installed docker.service successfully.
[INFO] To control docker.service, run: `systemctl --user (start|stop|restart) docker.service`
…
[INFO] Creating CLI context "rootless"
Current context is now "rootless"
Check:
docker info --format '{{.SecurityOptions}}' # the list includes name=rootless
docker run --rm hello-world
We tested rootless mode on Ubuntu 24.04: hello-world runs, and the --memory and --cpus limits are enforced. Inside the container the process sees itself as root (uid=0), while on the server the same process belongs to admin. After a reboot the service came up on its own, thanks to enable-linger. On Ubuntu 24.04 and 26.04, no extra AppArmor profile is needed for rootless mode: the docker-ce-rootless-extras package installs it.
To remove rootless mode, run dockerd-rootless-setuptool.sh uninstall as the user, then sudo systemctl enable --now docker.service docker.socket.
How to uninstall Docker
sudo apt 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 -rf /etc/docker
sudo rm /etc/apt/sources.list.d/docker.sources
sudo rm /etc/apt/keyrings/docker.asc
apt purge removes the packages (in our case Removing: 6, Freed space: 364 MB) but leaves images, containers and volumes in /var/lib/docker untouched; the second command deletes them, together with all data in volumes. /etc/docker remains after purge if you created daemon.json there. Client settings live in each user's ~/.docker.
Troubleshooting
| Symptom | What to do |
|---|---|
permission denied while trying to connect to the docker API at unix:///var/run/docker.sock |
The user is not in the docker group, or you did not log in again after usermod. Check id |
failed to connect to the docker API at unix:///var/run/docker.sock … no such file or directory |
The service is not running: sudo systemctl start docker; the cause is in journalctl -u docker -n 50 |
Job for docker.service failed after editing daemon.json |
There is an error in the file. sudo dockerd --validate --config-file /etc/docker/daemon.json shows it, for example invalid JSON: invalid character '}' |
E: Conflicting values set for option Signed-By regarding source https://download.docker.com/… |
An old /etc/apt/sources.list.d/docker.list from a previous installation is still there. Delete it; docker.sources is enough |
Signature verification error for download.docker.com: NO_PUBKEY … or Failed to parse keyring "/etc/apt/keyrings/docker.asc" |
The key did not download, or the Signed-By path does not match the key file. Repeat step 2 |
apt cannot find the docker-ce package |
The repository is not added or Suites has the wrong codename: cat /etc/apt/sources.list.d/docker.sources |
Aborting because rootful Docker (/var/run/docker.sock) is running and accessible |
For rootless mode, stop the system Docker service as described above |
| A container port is reachable from the internet while UFW is enabled | Docker opens published ports bypassing UFW. Publish internal services on 127.0.0.1; see Docker Compose in production |
Next steps
- How to run an application with a database and HTTPS through Compose and prepare it for production: Docker Compose in production on a single VPS.
- How to set up automated container deployment: deploying to a VPS from GitHub Actions.
- If the server is new, start with SSH key authentication and initial Ubuntu server setup.
- What takes up disk space in Docker and how to clean it up without losing data: server disk full.