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
| Source | Package names | Use it when |
|---|---|---|
| Docker apt/dnf repo | docker-ce, docker-ce-cli, containerd.io, plugins | Servers that need current releases and the Compose and Buildx plugins: the default choice |
| Distribution package | docker.io (Debian/Ubuntu), moby-engine (Fedora) | You want distro security support and accept older versions |
| get.docker.com script | Same packages as the repo | Throwaway VMs and labs; Docker itself says it is not meant for production |
| Snap (Ubuntu) | docker snap | Avoid on servers: snap confinement changes paths and can break bind mounts outside home |
| RHEL default | podman, buildah | Red 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.
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
| Key | Why |
|---|---|
log-driver: local | Compressed, rotated logs that docker logs can still read; json-file with max-size also works |
default-address-pools | Picks networks for docker network create and Compose projects from a range you control |
live-restore | Containers survive daemon restarts and package upgrades |
no-new-privileges | Applies --security-opt no-new-privileges to every container, blocking setuid escalation |
data-root | Move 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.