Skip to content

Installing Docker Engine on Linux: Setup Done Right

Install Docker Engine from the official repo on Ubuntu, Debian, Fedora and RHEL, then set log rotation, address pools, rootless mode and close the UFW port gap.

●By 11 min read

There are at least four ways to get a docker command onto a Linux server: the distribution's own package, a snap, the get.docker.com convenience script, and Docker's apt or dnf repository. They install different versions, from different maintainers, with different upgrade paths, and the first few months on a server are when the difference starts to bite: a missing Compose plugin, a disk filled by container logs, or a database port that your firewall swore was closed.

This guide installs Docker Engine from the official repository on Debian, Ubuntu, Fedora and RHEL, then covers the post-install settings most tutorials skip. For day-to-day operations once it is running, see Docker for Linux admins.

Pick an Installation Source

SourcePackage namesUse it when
Docker apt/dnf repodocker-ce, docker-ce-cli, containerd.io, pluginsServers that need current releases and the Compose and Buildx plugins: the default choice
Distribution packagedocker.io (Debian/Ubuntu), moby-engine (Fedora)You want distro security support and accept older versions
get.docker.com scriptSame packages as the repoThrowaway VMs and labs; Docker itself says it is not meant for production
Snap (Ubuntu)docker snapAvoid on servers: snap confinement changes paths and can break bind mounts outside home
RHEL defaultpodman, buildahRed Hat does not ship Docker; Podman is the supported engine there

Whichever source you choose, use only one. Packages from different sources conflict, and Docker's docs start by removing them:

# Debian/Ubuntu

$ for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do

sudo apt-get remove -y $pkg

done

# Fedora/RHEL

$ sudo dnf remove docker docker-client docker-client-latest docker-common \

docker-latest docker-latest-logrotate docker-logrotate docker-engine podman runc

Removing packages leaves /var/lib/docker in place, so existing images and volumes survive the switch.

Debian and Ubuntu

The repository is signed with Docker's release key. Store it in /etc/apt/keyrings and reference it with Signed-By, so the key is trusted for this repository only, not for every apt source on the system:

$ sudo apt-get update

$ sudo apt-get 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

# Check the fingerprint before trusting it

$ gpg --show-keys /etc/apt/keyrings/docker.asc

pub rsa4096 2017-02-22 [SCEA]

9DC858229FC7DD38854AE2D88D81803C0EBFCD88

uid Docker Release (CE deb) <[email protected]>

$ 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

Signed-By: /etc/apt/keyrings/docker.asc

EOF

$ sudo apt-get update

$ sudo apt-get install -y docker-ce docker-ce-cli containerd.io \

docker-buildx-plugin docker-compose-plugin

Debian β€” replace linux/ubuntu with linux/debian in both URLs. The Suites line resolves to the Debian codename, such as bookworm or trixie.

Ubuntu derivatives β€” on Linux Mint or Pop!_OS, VERSION_CODENAME is the derivative's name. UBUNTU_CODENAME is used first for that reason; check it with grep CODENAME /etc/os-release.

Starts automatically β€” on Debian and Ubuntu the package enables and starts docker.service during install. Check with systemctl is-enabled docker.

New release, no repo yet β€” for a few weeks after a new Ubuntu or Debian release, Docker may not publish packages for its codename. Wait rather than pointing the repo at the previous codename on a production host.

Fedora and RHEL

Fedora's dnf5 changed the config-manager syntax, so the repository command differs from RHEL's. The packages are the same:

# Fedora 41 and later (dnf5)

$ sudo dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo

# RHEL 8/9/10, Rocky, AlmaLinux (dnf4)

$ sudo dnf -y install dnf-plugins-core

$ sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo

$ sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# dnf shows the GPG key fingerprint on first install: confirm it is 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88

# Unlike Debian, the service is not started for you

$ sudo systemctl enable --now docker

Rocky and AlmaLinux β€” Docker publishes linux/centos and linux/rhel repos; both work on RHEL rebuilds. Use the one matching what your organisation already standardises on.

SELinux stays on β€” docker-ce works with SELinux enforcing. When a bind mount is denied, relabel it with the :Z (private) or :z (shared) volume suffix instead of disabling SELinux.

firewalld β€” Docker registers its own docker zone with firewalld and adds its bridge interfaces to it. If container networking breaks after a firewall reload, restart Docker so it recreates its rules.

sponsored

Verify, Then Pin the Version

$ sudo docker run --rm hello-world

Hello from Docker!

This message shows that your installation appears to be working correctly.

$ sudo docker info --format '{{.ServerVersion}} {{.Driver}} cgroup={{.CgroupDriver}} v{{.CgroupVersion}}'

28.5.1 overlay2 cgroup=systemd v2

$ docker compose version

Docker Compose version v2.39.4

On a fleet, an unattended upgrade that restarts the daemon restarts every container on the host. Hold the packages and upgrade on your own schedule:

# Debian/Ubuntu: list available versions, install one, hold it

$ apt-cache madison docker-ce | awk '{ print $3 }' | head -3

$ sudo apt-get install -y docker-ce=<version> docker-ce-cli=<version>

$ sudo apt-mark hold docker-ce docker-ce-cli containerd.io

# Fedora/RHEL: versionlock plugin

$ sudo dnf list docker-ce --showduplicates | sort -r | head -3

$ sudo dnf install -y python3-dnf-plugin-versionlock # RHEL/dnf4

$ sudo dnf versionlock add docker-ce docker-ce-cli containerd.io

live-restore β€” with "live-restore": true in daemon.json (below), containers keep running while dockerd restarts for an upgrade. It does not apply to Swarm mode.

Package managers compared β€” our package manager reference lists the hold, pin and history commands for apt, dnf, zypper and pacman side by side.

daemon.json: The Settings to Change on Day One

The default json-file log driver has no size limit. A chatty container fills /var/lib/docker and takes the host down with it. The default bridge address range can also collide with corporate or VPN networks in 172.16.0.0/12. Fix both before you run real workloads:

$ sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'

{

"log-driver": "local",

"log-opts": { "max-size": "20m", "max-file": "5" },

"live-restore": true,

"default-address-pools": [

{ "base": "10.200.0.0/16", "size": 24 }

],

"no-new-privileges": true

}

EOF

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

configuration OK

$ sudo systemctl restart docker

KeyWhy
log-driver: localCompressed, rotated logs that docker logs can still read; json-file with max-size also works
default-address-poolsPicks networks for docker network create and Compose projects from a range you control
live-restoreContainers survive daemon restarts and package upgrades
no-new-privilegesApplies --security-opt no-new-privileges to every container, blocking setuid escalation
data-rootMove images and volumes to a dedicated disk, set before the first pull

Log options apply only to containers created after the change; recreate existing ones. To see how much space Docker already uses, run docker system df, and see our disk usage tool for the rest of the filesystem.

The docker Group, Rootless Mode and the Firewall Gap

The install guide tells you to add yourself to the docker group so you can drop sudo. Understand what that grants: access to the Docker socket is root on the host, because any member can start a privileged container with / bind-mounted. Add only people who already have sudo, never service accounts that handle untrusted input.

# Root-equivalent convenience

$ sudo usermod -aG docker "$USER" # log out and back in

# Rootless alternative: the daemon runs as your user

# Needs docker-ce-rootless-extras (normally pulled in with docker-ce) and newuidmap

$ sudo apt-get install -y uidmap # Debian/Ubuntu; shadow-utils on RHEL/Fedora

$ dockerd-rootless-setuptool.sh install

$ systemctl --user enable --now docker

$ sudo loginctl enable-linger "$USER" # keep it running after logout

$ export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock

The surprise that catches most admins: published ports bypass UFW. Docker writes its own iptables rules in the nat table, so traffic to -p 5432:5432 is forwarded to the container before UFW's INPUT rules ever see it.

# Bind to localhost when only the host (or a reverse proxy) needs it

$ docker run -d -p 127.0.0.1:5432:5432 postgres:17

# Or filter forwarded traffic in the DOCKER-USER chain, which Docker never flushes

$ sudo iptables -I DOCKER-USER -i eth0 ! -s 10.0.0.0/8 -j DROP

Rootless limits β€” rootless Docker cannot bind ports below 1024 without extra sysctl changes, and some storage and network features differ. It is a strong default for developer machines and CI runners.

Rules must persist β€” DOCKER-USER rules added by hand vanish on reboot. Save them with your distro's iptables persistence package, or generate a rule set with our firewall rules generator.

Harden the rest of the host β€” Docker is one service on a server. Our server hardening guide covers SSH, updates and auditing; GPU hosts need the extra steps in Docker with NVIDIA GPUs.

Frequently Asked Questions

Should I install docker.io or docker-ce on Ubuntu?

docker-ce from Docker's own repository is the usual choice for servers: it tracks current releases and ships the Compose and Buildx plugins as docker-compose-plugin and docker-buildx-plugin. docker.io is maintained by Ubuntu and gets distro security updates but lags behind upstream. Pick one and remove the other, since they conflict.

Is the get.docker.com convenience script safe for production?

Docker's documentation says it is not recommended for production. It installs the latest version without asking, offers no version pinning, and assumes you trust a script piped from the internet. Use the apt or dnf repository instead, which lets you verify the signing key, pin versions and upgrade on your schedule.

Why do my Docker containers fill the disk with logs?

The default json-file log driver keeps logs forever with no size limit. Set log-driver to local, or keep json-file and add log-opts with max-size and max-file in /etc/docker/daemon.json, restart Docker, and recreate the containers, because log settings only apply to containers created after the change.

Is adding a user to the docker group safe?

It is equivalent to giving that user root. Members can talk to the Docker daemon, which runs as root, and start a container that mounts the host filesystem. Only add users who already have sudo, or use rootless Docker so the daemon runs with the user's own privileges.

Why does UFW not block ports published by Docker?

Docker inserts iptables rules in the nat table that forward published ports to containers before the packets reach UFW's INPUT chain. Bind ports to 127.0.0.1 when they only need local access, or add filtering rules to the DOCKER-USER chain, which Docker evaluates first and never overwrites.

sponsored

Related Tools

See also