Docker & ContainersIII Β· Installation & DaemonRepositories and pinning
Repository setup and version pinning
What you'll learn
- Add the Docker official apt or dnf repository with a verified signing key
- Pin docker-ce to an exact version and know which pinning mechanism survives a fleet
- Read a Docker package version string and explain every field in it
- Perform and verify a controlled engine upgrade, with a rollback
- Explain what version skew across a fleet costs during an incident
Prerequisites
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
Installing the newest Docker is fine for a laptop. On a fleet it is a decision you make once and then cannot unmake, because a fleet with unpinned versions has no answer to the two questions that matter during an incident: what changed, and is this host the same as that host.
Adding the official apt repository
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/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-get updateSigned-By is the part that matters. It scopes that key to that repository
only, so a compromise of the Docker key cannot be used to sign a package that
apt accepts as coming from Debian. The older pattern of dropping keys into
/etc/apt/trusted.gpg.d/ trusted every key for every repository, and is why
that directory is deprecated.
Verify the key before you trust it
Downloading a key over TLS proves you reached download.docker.com. It does
not prove you reached the real one β that is what the fingerprint is for.
$ gpg --show-keys /etc/apt/keyrings/docker.ascpub rsa4096 2017-02-22 [SCEA]
9DC858229FC7DD38854AE2D88D81803C0EBFCD88
uid Docker Release (CE deb) <docker@docker.com>
sub rsa4096 2017-02-22 [S]Compare 9DC858229FC7DD38854AE2D88D81803C0EBFCD88 against the value Docker
publishes. This check belongs in your provisioning code, not in a humanβs
memory:
EXPECT=9DC858229FC7DD38854AE2D88D81803C0EBFCD88
GOT=$(gpg --show-keys --with-colons /etc/apt/keyrings/docker.asc \
| awk -F: '/^fpr:/ {print $10; exit}')
if [ "$GOT" != "$EXPECT" ]; then
echo "Docker signing key fingerprint mismatch: got $GOT" >&2
exit 1
fi
echo "signing key verified"Reading a Docker version string
Before you can pin, you have to be able to read what you are pinning to.
$ apt-cache madison docker-ce | head -5docker-ce | 5:28.5.2-1~debian.12~bookworm | https://download.docker.com/linux/debian bookworm/stable amd64 Packages
docker-ce | 5:28.5.1-1~debian.12~bookworm | https://download.docker.com/linux/debian bookworm/stable amd64 Packages
docker-ce | 5:28.4.0-1~debian.12~bookworm | https://download.docker.com/linux/debian bookworm/stable amd64 Packages
docker-ce | 5:28.3.3-1~debian.12~bookworm | https://download.docker.com/linux/debian bookworm/stable amd64 Packages
docker-ce | 5:28.3.2-1~debian.12~bookworm | https://download.docker.com/linux/debian bookworm/stable amd64 PackagesIllustrative output
Pinning: three mechanisms, one right answer per situation
apt-mark hold β the interactive brake
sudo apt-mark hold docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
apt-mark showholdHold is a flag in the local dpkg database. It is easy, it is what most people reach for, and it has two properties that matter on a fleet.
It is invisible: nothing in your configuration repository records it. A
rebuilt host has no hold, and drifts. And it is blunt β it prevents any
change, so the host also stops receiving the security update you did want, and
a plain apt upgrade will report packages βkept backβ without explaining why.
Hold is right for a host you are actively debugging, or as a belt-and-braces addition to a declared version. It is not a fleet strategy on its own.
An apt preferences pin β the declarative version
# /etc/apt/preferences.d/docker-ce.pref
Package: docker-ce docker-ce-cli
Pin: version 5:28.5.2-1~debian.12~bookworm
Pin-Priority: 1001A priority above 1000 permits a downgrade as well as blocking an upgrade, which is what makes this a rollback mechanism and not just a brake. The file is a file: it lives in Git, it deploys with the host, and a rebuilt host comes back with the same pin.
Just install the exact version
The simplest form, and the one Dockerβs own documentation uses:
VERSION_STRING=5:28.5.2-1~debian.12~bookworm
sudo apt-get install -y \
docker-ce="$VERSION_STRING" \
docker-ce-cli="$VERSION_STRING" \
containerd.io docker-buildx-plugin docker-compose-plugin$ docker version --format 'client={{.Client.Version}} server={{.Server.Version}} api={{.Client.APIVersion}}/{{.Server.APIVersion}}'client=28.5.2 server=28.1.1 api=1.51/1.49Illustrative output
Hold the whole set, or hold none of it. The five packages are a unit.
RPM systems
sudo dnf install -y python3-dnf-plugin-versionlock
VERSION_STRING=3:28.5.2-1.el9
sudo dnf install -y \
docker-ce-"$VERSION_STRING" \
docker-ce-cli-"$VERSION_STRING" \
containerd.io docker-buildx-plugin docker-compose-plugin
sudo dnf versionlock add docker-ce docker-ce-cli
dnf versionlock listThe controlled upgrade
- Read the release notes for every version between the one you run and the one you want. Docker publishes them per major line; skipped releases are the case live restore explicitly does not cover.
- Confirm the target is a patch bump within the same major line if you intend to keep containers running. A major upgrade is a container-restarting change.
- Confirm live restore is already active:
docker info --format "{{.LiveRestoreEnabled}}". Enable it withsystemctl reload dockerif not β that costs nothing. - Record the current state:
docker version,docker info,dpkg -l | grep -E "docker|containerd", and the PID of a representative container. - Release the hold, install the exact target version, re-apply the hold. Do not run a bare
apt upgrade. - Verify: client and server versions match your intent, the container PID you recorded is unchanged, and the application answers a real request.
- Roll one host. Wait long enough to see a full traffic cycle. Only then roll the rest.
TARGET=5:28.5.2-1~debian.12~bookworm
sudo apt-mark unhold docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
sudo apt-get update
sudo apt-get install -y docker-ce="$TARGET" docker-ce-cli="$TARGET"
sudo apt-mark hold docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
docker version --format 'client={{.Client.Version}} server={{.Server.Version}}'The rollback is the same command with the previous version string, which is why
recording it in step 4 is not busywork. Dockerβs repository retains old
releases, so a downgrade is normally available β but confirm with
apt-cache madison before you rely on it, not during the incident.
Verification that can fail
WANT_VERSION=28.5.2
GOT=$(docker version --format '{{.Server.Version}}')
if [ "$GOT" != "$WANT_VERSION" ]; then
echo "engine drift: running $GOT, expected $WANT_VERSION" >&2
exit 1
fi
if ! apt-mark showhold | grep -qx docker-ce; then
echo "docker-ce is not held; this host will drift" >&2
exit 1
fi
echo "engine $GOT pinned and held"apt-cache policy docker-ce is the companion for a human: it shows the
installed version, the candidate, and which repository each came from, which is
the fastest way to see that a host is silently tracking the wrong suite.
The same principle, one level up: images
Everything above applies to the Docker Engine package. The identical argument applies to the images you run, and the mechanism is a digest rather than a version string.
IMAGE=nginx:1.27
docker pull "$IMAGE"
docker image inspect --format '{{index .RepoDigests 0}}' "$IMAGE"That prints something of the form nginx@sha256: followed by 64 hex characters.
Using that reference in a Compose file or a docker run binds you to exactly
that content, whatever the publisher later does to the tag.
Knowledge check
Knowledge check Β· 6 questions
Q1. Why must a docker-ce version pin include the `5:` prefix?
Q2. A host has `apt-mark hold docker-ce` but not `docker-ce-cli`. Three months later some docker commands fail with API errors. What happened?
Q3. Which of these will cause a fleet to drift onto different Docker Engine versions? Select all that apply.
Q4. Setting `pull_policy: always` in a Compose file will make the deploy fail if the tag now resolves to a different digest.
Q5. What does `Signed-By` in a deb822 .sources file buy you over dropping the key into /etc/apt/trusted.gpg.d/?
Q6. You want to pin containerd.io alongside docker-ce. What should you check first?
Passing score: 75%. Answers are checked in this browser.