Docker 📖 13 min read

Docker Installation Guide

Ubuntu and Debian ship their own Docker packages, but those are maintained by the distribution rather than by Docker, they usually trail Docker's releases, and Docker's documentation tells you to remove them before installing the official ones. This guide installs Docker Engine 29 and the Compose plugin from Docker's repositories on Ubuntu, Debian, Fedora, RHEL and CentOS Stream. It then covers the settings that matter on a server you plan to keep: who can use the Docker socket, log rotation, the firewall gap, holding a version, and removing Docker cleanly.

Jump to your distribution: Ubuntu and Debian or Fedora, RHEL and CentOS Stream. Everything after that applies to all of them.

Before you start

Docker publishes packages for these releases. The list comes from Docker's install pages as published on October 9, 2026; Docker drops releases when the distribution ends support, so check the page for your distribution if yours is older.

DistributionSupported releasesRepository
Ubuntu (64-bit)26.04 LTS, 24.04 LTS, 22.04 LTSdownload.docker.com/linux/ubuntu
Debian13 "trixie", 12 "bookworm"download.docker.com/linux/debian
Fedora44, 43download.docker.com/linux/fedora
RHEL8, 9, 10download.docker.com/linux/rhel
CentOS Stream9, 10download.docker.com/linux/centos

Docker doesn't officially support Ubuntu derivatives such as Linux Mint, although the Ubuntu steps may work there. You also need an account with sudo and outbound HTTPS access to download.docker.com. To confirm what you're running:

cat /etc/os-release
uname -m          # x86_64 or aarch64 are the common answers

If this machine already ran Docker from another source, its images, containers and volumes in /var/lib/docker are kept when you remove the old packages. Switching package sources is still a good moment for a copy of any volume data you care about. With the old daemon stopped:

sudo systemctl stop docker docker.socket
sudo tar -czf ~/docker-volumes-backup.tar.gz -C /var/lib/docker volumes

The firewall gap

Read this before you publish any container port. Docker's install pages warn that ports you publish with -p or ports: bypass ufw and firewalld rules, because Docker writes its own iptables rules. A container published as 8080:80 is reachable from the network even though ufw status doesn't mention 8080. Docker also only works with iptables-nft and iptables-legacy; rules you create directly with nft aren't supported on a Docker host.

Two habits avoid surprises. Publish ports that only a local reverse proxy needs on the loopback address (127.0.0.1:8080:80), and put any filtering you need for published ports in the DOCKER-USER chain described on Docker's packet filtering and firewalls page.

Ubuntu and Debian

These steps follow Docker's install pages for Ubuntu and Debian. The two differ in the list of conflicting packages, the repository URL and one line of the sources file, so each step shows both.

Terminal window with the commands from this section
Docker's signing key and repository added on Ubuntu, before the packages are installed.

1. Remove conflicting packages

The command only removes packages that are actually installed, so it's safe to run on a fresh system; apt may report that there's nothing to remove.

Ubuntu
sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc | cut -f1)
Debian
sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-doc docker-buildx podman-docker containerd runc | cut -f1)

2. Add Docker's signing key and repository

Docker's instructions store the key as an ASCII-armored .asc file and describe the repository in a deb822 .sources file. The shell fills in your release codename and architecture when the file is written.

Ubuntu
sudo apt update
sudo apt install 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 <<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
sudo apt update
sudo apt install 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 <<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

Check it: cat /etc/apt/sources.list.d/docker.sources should show a real codename such as noble or trixie on the Suites: line, and apt-cache policy docker-ce should list a candidate version from download.docker.com. On Debian testing or a derivative such as Kali, Docker's page says to replace the codename with the matching Debian release, for example trixie.

Older guides created /etc/apt/sources.list.d/docker.list and /etc/apt/keyrings/docker.gpg. If either exists from a previous attempt, delete it, or apt will see the same repository twice with different keys.

3. Install the packages

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

docker-ce is the daemon, docker-ce-cli the docker command, containerd.io bundles containerd and runc, and the two plugins add docker buildx and docker compose.

4. Confirm it runs

On Debian and Ubuntu the service starts straight away and is enabled at boot.

sudo systemctl status docker
sudo docker run hello-world
sudo docker version
docker compose version

hello-world pulls a tiny image and prints a message that starts with "Hello from Docker!". docker version should show both a Client and a Server section; if the Server part is an error, the daemon isn't running (see troubleshooting). Continue with using Docker without sudo.

Fedora, RHEL and CentOS Stream

The RPM-based steps follow Docker's pages for Fedora, RHEL and CentOS. Unlike on Debian and Ubuntu, the packages don't start Docker for you.

Terminal window with the commands from the section Fedora, RHEL and CentOS Stream
Fedora and RHEL: install the packages, start Docker, run hello-world and check Compose.

1. Remove conflicting packages

Fedora
sudo dnf remove docker docker-client docker-client-latest docker-common docker-latest \
  docker-latest-logrotate docker-logrotate docker-selinux docker-engine-selinux docker-engine
RHEL
sudo dnf remove docker docker-client docker-client-latest docker-common docker-latest \
  docker-latest-logrotate docker-logrotate docker-engine podman runc
CentOS Stream
sudo dnf remove docker docker-client docker-client-latest docker-common docker-latest \
  docker-latest-logrotate docker-logrotate docker-engine

On RHEL the list includes podman and runc. If you use Podman for other workloads on the same machine, decide before running this whether you want both container engines on one host. dnf may report that none of the packages are installed, which is fine.

2. Add Docker's repository

Fedora ships DNF5, whose config-manager syntax differs from the DNF 4 used on RHEL and CentOS Stream.

Fedora
sudo dnf config-manager addrepo --from-repofile https://download.docker.com/linux/fedora/docker-ce.repo
RHEL
sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
CentOS Stream
sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

Check it: dnf repolist should now include docker-ce-stable. On CentOS Stream, Docker's page also requires the centos-extras repository, which is enabled by default.

3. Install, start and test

sudo dnf install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo docker run hello-world
docker compose version

When dnf asks you to accept Docker's GPG key, compare the fingerprint with the one on Docker's install page, 060A 61C5 1B55 8A7F 742B 77AA C52F EB6B 621E 9F35, and only accept it if they match. systemctl enable --now starts Docker and enables it at boot. The install also creates a docker group with no members.

On Fedora, if the service fails and journalctl -u docker shows failed to find iptables, Docker's page gives this fix:

sudo alternatives --set iptables /usr/bin/iptables-nft
sudo systemctl restart docker

Using Docker without sudo

The daemon always runs as root, and by default only root can talk to its socket. Docker's post-installation steps offer two ways around typing sudo, and its docs are blunt about the first one: membership in the docker group grants root-level privileges, because anyone in it can start a container that mounts the host's filesystem.

  • Add your user to the docker group. Reasonable on a single-user machine where your account already has sudo.
  • Use rootless mode. The daemon runs as your user instead of root. It's the better fit for shared machines, with some limits that page lists.

For the group route:

sudo groupadd docker          # usually exists already; an error saying so is harmless
sudo usermod -aG docker $USER

Group membership is read at login, so log out of the whole session (or end the SSH connection) and log back in. Opening a new terminal tab isn't enough; in a VM you may need a reboot. newgrp docker applies it to the current shell only. Then:

id -nG                        # should now include docker
docker run hello-world

If you ran sudo docker commands before joining the group, you may see WARNING: Error loading config file: /home/you/.docker/config.json ... permission denied. The ~/.docker directory was created by root. Docker's fix:

sudo chown "$USER":"$USER" /home/"$USER"/.docker -R
sudo chmod g+rwx "$HOME/.docker" -R

The same page also describes a password-protected newgrp setup for single-user workstations. The Linux permissions guide explains how groups and sockets fit together.

Stop container logs from filling the disk

This is the post-install step most often skipped on servers. The default json-file logging driver keeps every line a container prints, with no size limit, so a chatty container slowly fills /var/lib/docker. Docker's json-file driver docs show how to cap it in /etc/docker/daemon.json:

/etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

That keeps at most three 10 MB files per container. The values must be quoted strings, even the number. If daemon.json already exists, add these keys to it rather than replacing the file. Check the syntax, then restart:

sudo dockerd --validate --config-file /etc/docker/daemon.json
sudo systemctl restart docker

The new limits apply to containers created after the restart. Existing containers keep their old settings until you recreate them (for a Compose project, docker compose up -d --force-recreate). docker inspect --format '{{.HostConfig.LogConfig}}' CONTAINER shows what a container is actually using.

Installing or holding a specific version

Installing from the repository gives you the newest release, and later apt upgrade or dnf upgrade runs bring in new ones, including major versions. To choose the version yourself on Debian or Ubuntu, list what the repository has and install an exact string from that list:

Terminal window with the commands from the section Installing or holding a specific version
List the versions on offer, then install one exactly as printed.
apt list --all-versions docker-ce

# Copy one version string exactly as printed, for example:
VERSION_STRING=5:29.8.2-1~ubuntu.24.04~noble
sudo apt install docker-ce=$VERSION_STRING docker-ce-cli=$VERSION_STRING containerd.io docker-buildx-plugin docker-compose-plugin

# Optional: stop routine upgrades from moving it
sudo apt-mark hold docker-ce docker-ce-cli

On Fedora, RHEL and CentOS Stream, dnf list docker-ce --showduplicates | sort -r lists versions; install one with sudo dnf install docker-ce-VERSION docker-ce-cli-VERSION containerd.io docker-buildx-plugin docker-compose-plugin, using the full version string from that list. Remember to release a hold (sudo apt-mark unhold docker-ce docker-ce-cli) when you want security fixes. The Linux package management guide covers holds and version locks on both package managers. Read the Engine release notes before moving to a new major version.

What about get.docker.com?

Docker's convenience script installs everything in one command, but Docker recommends it only for testing and development. It installs the latest release without asking, may pull in many dependencies, and isn't designed to upgrade an existing installation. If you use it on a throwaway VM, look first: curl -fsSL https://get.docker.com -o get-docker.sh followed by sudo sh ./get-docker.sh --dry-run prints the steps without running them. Upgrade later through the package manager, not by running the script again.

Test Compose end to end

hello-world proves the engine works. This short test also exercises the Compose plugin, a published port and cleanup. In an empty directory, create compose.yaml:

Terminal window with the commands from the section Test Compose end to end
The end-to-end check: web should be running, and curl should get 200 OK from nginx.
compose.yaml
services:
  web:
    image: nginx:alpine
    ports:
      - "127.0.0.1:8080:80"

There is no top-level version: line. Current Compose treats it as obsolete and prints a warning if it's there.

docker compose up -d
docker compose ps                  # web should be "running"
curl -I http://127.0.0.1:8080      # expect HTTP/1.1 200 OK and Server: nginx
docker compose logs web

Because the port is bound to 127.0.0.1, other machines can't reach it, whatever your firewall says. Clean up afterwards:

docker compose down                # stops and removes the container and network
docker container prune             # removes stopped containers, including the hello-world runs; asks first
docker image rm nginx:alpine hello-world

Run the prune before removing the images: Docker refuses to delete an image that a stopped container still references. On a host that already runs other workloads, remove the hello-world containers by name from docker container ls -a instead of pruning.

Troubleshooting

permission denied while trying to connect to the Docker daemon socket

Your session isn't in the docker group yet. id -nG shows the groups the current session actually has. If docker is missing even though you ran usermod, log out completely and back in.

Cannot connect to the Docker daemon ... Is the docker daemon running?

Start it with sudo systemctl enable --now docker. If it won't stay up, sudo journalctl -u docker -b shows why. A syntax error in /etc/docker/daemon.json is a common cause; sudo dockerd --validate --config-file /etc/docker/daemon.json points at it.

apt errors after adding the repository

Conflicting values set for option Signed-By means the repository is configured twice with different keys, usually an old docker.list next to the new docker.sources. Delete the old file. NO_PUBKEY or "is not signed" means the Signed-By: path doesn't match where the key was saved, or the key file isn't readable (sudo chmod a+r /etc/apt/keyrings/docker.asc).

A published port is reachable even though the firewall blocks it

That's the firewall gap described above. Bind the port to 127.0.0.1 if only local services need it, or filter it in the DOCKER-USER chain.

Containers can't resolve hostnames, but the host can

Some networks block DNS servers other than their own. Set the resolvers Docker hands to containers with a "dns" key in daemon.json, for example "dns": ["192.168.1.1"] for your router, then restart Docker and recreate the containers.

Disk space keeps shrinking

docker system df shows how much space images, containers, volumes and build cache use. docker system prune removes stopped containers, unused networks, dangling images and build cache; adding -a also removes every image no container uses. Volumes are only removed with --volumes, and that deletes their data. Read the confirmation prompt before answering yes on a server you care about. If the space goes to logs, set up log rotation.

Removing Docker completely

Removing the packages leaves images, containers and volumes in place. Deleting /var/lib/docker and /var/lib/containerd erases them permanently, so copy any volume data you need first (see Before you start).

Ubuntu and Debian
sudo apt purge 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 /etc/apt/sources.list.d/docker.sources /etc/apt/keyrings/docker.asc
Fedora, RHEL and CentOS Stream
sudo dnf remove 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

Configuration you edited yourself, such as /etc/docker/daemon.json, has to be deleted by hand.

Next steps