Skip to main content
RunBook Academy

Docker & ContainersIII · Installation & DaemonInstallation methods

Supported installation methods

Foundation⏱ ~26 mindocker

What you'll learn

  • Choose the right installation method for a given host
  • Understand the trade-offs between the Docker repository, distro packages, binaries and the convenience script
  • Remove the conflicting packages that make an install fail in confusing ways
  • Recognise when to use rootless Docker, and what it costs you
  • Verify an installation with a check that can fail

Prerequisites

None — start here.

Verified against Docker Engine 29.x · Docker Engine 28.x · Docker Compose 2.x · containerd 2.x · runc 1.2.x · BuildKit 0.20+ · Linux kernel 5.15+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-12

Not yet marked complete on this device.

There are five ways to get Docker Engine onto a Linux host. The right one depends less on which is easiest today and more on a question you will be asked during your first CVE response: how do I upgrade this, and how fast?

That is what an installation method really is. Everything else — which directory the binaries land in, whether there is a systemd unit — follows from it.

Clear the ground first

Almost every “the install failed for no reason” report starts here. Distros ship their own container packages, and several of them provide /usr/bin/docker or a conflicting containerd. The Docker documentation names them explicitly.

Service impact possibleconflicts
for pkg in docker.io docker-doc docker-compose docker-compose-v2 \
         docker-buildx podman-docker containerd runc; do
  sudo apt-get remove -y "$pkg"
done

Option 1 — Docker’s official apt/yum repository

The recommended approach for production, and the one the rest of this course assumes.

Configuration changerepository
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

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 "$VERSION_CODENAME")
Components: stable
Architectures: $(dpkg --print-architecture)
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

That installs five packages:

  • docker-ce — the daemon, dockerd.
  • docker-ce-cli — the docker client.
  • containerd.io — the container supervisor.
  • docker-buildx-plugindocker buildx.
  • docker-compose-plugindocker compose.

Updates come from Docker, not your distro, so you get new Engine versions within days of release rather than at the next distro point release. That speed is the whole point, and it is also why the very next lesson is about pinning: an unpinned docker-ce on a fleet means an unattended-upgrade run can change your container runtime on a Tuesday night.

Option 2 — Distribution packages

Debian, Ubuntu, Fedora, RHEL, SUSE and others ship their own Docker or Podman packages. They are older than Docker’s, and they move on the distro’s cadence.

Choose this when:

  • Your compliance regime requires every package to come from the distro vendor, with the distro’s signature and the distro’s support contract.
  • You are on RHEL or a rebuild where the vendor position is Podman, and deviating from it costs you support.
  • The slower cadence is acceptable because you patch on the distro’s schedule anyway.

The cost is real and worth naming: when a container-runtime CVE lands, you wait for your distro to backport it. That is sometimes faster than you fear (distros backport security fixes aggressively) and sometimes much slower.

Option 3 — Static binaries

Download tarballs of dockerd, docker, containerd and runc and place them yourself. Docker documents this and is clear it is the least supported path: no systemd unit, no daemon.json, no user or group, no /var/lib/docker — you create all of it.

Legitimate uses:

  • Air-gapped environments where no repository is reachable, and the artefact is staged through an approved transfer process.
  • Immutable/appliance images built by a pipeline that bakes an exact set of binaries and never runs a package manager at runtime.

The trap is that a binary install has no upgrade mechanism at all. Whatever replaces those files is code somebody has to write and test, and that code is the actual deliverable — not the install.

Option 4 — The convenience script

https://get.docker.com detects your distro and does everything. The Docker documentation states plainly: “The convenience script isn’t recommended for production environments, but it’s useful for creating a provisioning script tailored to your needs.”

The reasons it gives are the reasons to take seriously: it requires root, makes every decision for you, installs dependencies without confirmation, and does not let you customise most parameters. It is genuinely useful for a laptop, a throwaway VM, or as a reference for the provisioning code you write yourself.

Option 5 — Rootless Docker

Runs the whole daemon as an unprivileged user inside a user namespace, so a container escape lands on an ordinary user account rather than on root.

Configuration changerootless
# Prerequisite: newuidmap/newgidmap, from the uidmap package,
# and at least 65536 subordinate IDs in /etc/subuid and /etc/subgid.
grep "^$(id -un):" /etc/subuid /etc/subgid

dockerd-rootless-setuptool.sh install

export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
docker info --format '{{.Name}} rootless={{.SecurityOptions}}'

The rootless daemon listens on a socket under /run/user/<uid>/ and is managed by systemd --user, not by the system instance. That is the source of most rootless confusion: sudo systemctl status docker reports the system daemon, sudo docker ps talks to the system daemon, and neither has anything to do with your rootless one. Configuration lives in ~/.config/docker/daemon.json, not /etc/docker/daemon.json.

It is the right answer for a shared CI runner, where the job you are running was written by somebody who does not have root on the box and should not gain it. It is a poorer answer where you need low-numbered host ports, host networking, or the full set of cgroup resource controls without additional delegation setup — the rootless documentation lists the current limitations, and they change between releases, so check them against the version you are deploying rather than against a blog post.

What about Docker Desktop?

Docker Desktop exists for Linux, and Docker’s own install page offers it. It is a development environment, not a server runtime: it runs a virtual machine, and the docs state it “creates and uses a custom docker context, desktop-linux, on startup.”

Read-only / Safecontexts
$ docker context ls
NAME             DESCRIPTION                               DOCKER ENDPOINT
default *        Current DOCKER_HOST based configuration   unix:///var/run/docker.sock
desktop-linux    Docker Desktop                            unix:///home/ops/.docker/desktop/docker.sock

Illustrative output

Decision matrix

  • Production Linux server — Docker official repo, version pinned, upgrades in a controlled window. Everything else is a deviation you should be able to justify.
  • Air-gapped — Static binaries or a mirrored repository. Whichever you pick, the upgrade tooling is the deliverable, not the install.
  • Shared CI runner — Rootless Docker. The job author does not get root on the runner.
  • Single-tenant dev host — Docker official repo. Same as production, so that "works on my machine" means something.
  • Developer workstation — Docker Desktop is fine, and knowing about docker context is not optional.
  • Hardened baseline (CIS, STIG) — Distro packages plus documented divergences, or the vendor runtime (Podman on RHEL). Fighting the vendor position costs you support.
  • Embedded / resource-constrained — Probably not Docker Engine. containerd alone, or Podman, or no container runtime at all.

Verification that can fail

docker run hello-world proves the whole stack works, which is the point — but only if you actually check the result rather than watching text scroll past.

Read-only / Safeverify
set -e

# 1. The client can reach a daemon, and it is the one you expect.
docker context show
docker info --format 'server={{.ServerVersion}} storage={{.Driver}} cgroup={{.CgroupDriver}} runtime={{.DefaultRuntime}}'

# 2. All three layers are present and agree.
dockerd --version
containerd --version
runc --version

# 3. The full path works: pull, create, run, exit 0, remove.
docker run --rm hello-world >/dev/null
echo "end-to-end OK"

# 4. The daemon will come back after a reboot.
systemctl is-enabled docker.service

Step 4 is the one people skip, and it is the one that produces a host that has been “working fine” for four months and comes back from a kernel patch with no containers.

Knowledge check

Knowledge check · 6 questions

  1. Q1. You install docker-ce from Docker's apt repository on Ubuntu. Do you need `systemctl enable --now docker`?

  2. Q2. Which packages does the Docker documentation tell you to remove before installing docker-ce on Ubuntu? Select all that apply.

  3. Q3. The docker-ce package adds root to the `docker` group so that root can use the socket.

  4. Q4. On a workstation with both Docker Desktop and Docker Engine, an image you built yesterday no longer appears in `docker images`. What is the most likely explanation?

  5. Q5. What does the Docker documentation say about the convenience script at get.docker.com?

  6. Q6. Editing /etc/docker/daemon.json and reloading docker.service has no effect on a rootless Docker daemon.

Passing score: 75%. Answers are checked in this browser.