Files
esh-pfi-infrastructure/playbooks/upgrade-docker-ce.yaml
T
vh b0608f9300 playbooks/upgrade-docker-ce: accept compose plugin major ≥ 2 (was hardcoded v2.X.Y)
docker-ce 29 ships docker-compose-plugin renumbered to v5.x (was v2.x
with docker-ce 26-28). Same Compose v2 codebase under the hood —
Docker just realigned the major number. The verify regex was hardcoded
to `v2\.[0-9]+\.[0-9]+`, so a successful migration on esh-docker-vm
(29.4.1, 16/16 stacks back up clean) reported FAILED on the verify
phase. Switched to `docker compose version --short` parsed for major,
gated `>= 2` — works across future plugin renumbers too.

STATUS.md: mark esh-docker-vm done. ana-docker is the last host.
2026-04-26 14:22:26 -07:00

247 lines
10 KiB
YAML

# upgrade-docker-ce — migrate a host from Debian's docker.io (20.10.x,
# bookworm-packaged) to Docker's official docker-ce repo (28+).
#
# Bookworm's docker.io stays pinned at 20.10.24, which:
# * uses an old client/daemon API (1.41) that newer compose clients
# (1.52+) refuse to talk to → "client version 1.52 is too new"
# during builds
# * is EOL upstream — docker.io upstream doesn't ship to it anymore
# * is missing modern buildx driver versions our newer client expects
#
# This playbook handles the migration on one host at a time. Stops
# every running stack via `docker compose stop`, removes the old
# packages (preserving /var/lib/docker/), adds Docker's signed APT
# repo, installs docker-ce + docker-compose-plugin + containerd.io,
# starts the new daemon, and brings each stack back up via
# `docker compose up -d` (compose's restart=unless-stopped also
# auto-restarts containers when the daemon comes back, but doing
# them explicitly per-stack lets us see failures cleanly).
#
# Usage:
# scripts/elway <host> --playbook playbooks/upgrade-docker-ce.yaml
#
# Recommended host order (least → most blast radius):
# 1. nh3-docker
# 2. esh-docker-vm
# 3. ana-docker
#
# Verify after each before moving to the next:
# ssh <host> 'docker version --format "{{.Server.Version}}"'
# ssh <host> 'docker ps --format "{{.Names}}\t{{.Status}}" | head'
#
# Rollback (if a daemon won't start, or a container errors out):
# ssh <host> 'sudo apt install --allow-downgrades docker.io'
# then re-add the docker.io packages from /tmp/docker-pre-upgrade.txt
#
# /var/lib/docker/ is preserved throughout (apt remove, not purge),
# so volumes / images / containers survive the package swap. The
# overlay2 storage driver is the default on both packages, so no
# data migration needed.
vars:
pkgs_to_remove: docker.io docker-compose docker-compose-plugin docker-buildx-plugin docker-doc docker
pkgs_to_install: docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
steps:
# ── snapshot + stop ─────────────────────────────────────────────────
- name: Snapshot current docker package versions (rollback reference)
shell: |
dpkg -l | awk '/^ii\s+(docker|containerd)/ {print $2 "=" $3}' \
| sudo tee /tmp/docker-pre-upgrade.txt >/dev/null
cat /tmp/docker-pre-upgrade.txt
sudo: true
changed_when: "false"
- name: List currently-running compose stacks (snapshot for restart)
shell: |
docker ps --format '{{.Label "com.docker.compose.project"}}' \
| sort -u | grep -v '^$' \
| tee /tmp/docker-pre-upgrade-stacks.txt
changed_when: "false"
- name: Stop every running stack via docker compose
# Walk each /opt/docker/compose/<stack>/ dir that has a running
# container and `compose stop` it. Skips dirs without a running
# stack so reruns don't error.
shell: |
set +e
for dir in /opt/docker/compose/*/; do
stack=$(basename "$dir")
if docker ps --format '{{.Label "com.docker.compose.project"}}' | grep -q "^${stack}$"; then
echo " stopping $stack"
(cd "$dir" && docker compose stop) || echo " (no-op or failed)"
fi
done
true
# ── add Docker's official APT repo ──────────────────────────────────
- name: Install prereqs for the new APT repo
shell: apt-get install -y ca-certificates curl gnupg
sudo: true
- name: Ensure /etc/apt/keyrings exists (mode 0755)
shell: install -d -m 0755 /etc/apt/keyrings
sudo: true
creates: /etc/apt/keyrings
- name: Fetch + install Docker's signing key (only if absent)
shell: |
curl -fsSL https://download.docker.com/linux/debian/gpg \
| sudo gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
sudo: true
when: '[ ! -f /etc/apt/keyrings/docker.gpg ]'
- name: Add Docker's APT source
# codename is whatever lsb_release says — bookworm on this fleet.
shell: |
codename=$(lsb_release -cs)
arch=$(dpkg --print-architecture)
url="https://download.docker.com/linux/debian"
line="deb [arch=${arch} signed-by=/etc/apt/keyrings/docker.gpg] ${url} ${codename} stable"
echo "${line}" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo: true
when: '[ ! -f /etc/apt/sources.list.d/docker.list ]'
- name: apt-get update (now sees Docker's repo)
shell: apt-get update -qq
sudo: true
- name: Show candidate version of docker-ce
shell: apt-cache policy docker-ce | head -5
sudo: true
changed_when: "false"
# ── swap packages ───────────────────────────────────────────────────
- name: Remove Debian's docker.io packages (PRESERVE /var/lib/docker)
# apt remove (not purge) keeps /var/lib/docker/* in place — images,
# volumes, container metadata all survive the swap.
shell: DEBIAN_FRONTEND=noninteractive apt-get remove -y {{ pkgs_to_remove }} || true
sudo: true
- name: Install docker-ce + plugins from Docker's repo
shell: DEBIAN_FRONTEND=noninteractive apt-get install -y {{ pkgs_to_install }}
sudo: true
- name: Rewrite docker.service drop-in to match the new package layout
# Two related fixes for the systemd unit, learned the hard way on
# nh3-docker. The docker.io era left an
# /etc/systemd/system/docker.service.d/override.conf hardcoding
# ExecStart=/usr/sbin/dockerd (Debian package path) and usually
# adding `-H tcp://...` for remote discovery.
#
# After the swap to docker-ce two things are wrong:
# 1. /usr/sbin/dockerd doesn't exist — docker-ce installs at
# /usr/bin/dockerd. Daemon fails status=203/EXEC.
# 2. If daemon.json defines `hosts:` (typical here for the
# 0.0.0.0:2375 homepage-discovery binding), then ANY `-H`
# flag in ExecStart conflicts: dockerd refuses to start with
# "conflicting host options".
#
# The shipped docker-ce unit's ExecStart is `dockerd -H fd://
# --containerd=/run/containerd/containerd.sock` — that `-H fd://`
# ALSO conflicts with daemon.json hosts:. So we always need an
# override that strips `-H` entirely when daemon.json defines
# hosts.
#
# Procedure:
# * Back up any existing override (preserves it as .pre-upgrade)
# * Probe /etc/docker/daemon.json for a `hosts:` setting
# * If hosts: is defined → install an override that clears the
# base unit's ExecStart and replaces it WITHOUT -H
# * Otherwise no override is needed (base unit's `-H fd://` is fine)
shell: |
d=/etc/systemd/system/docker.service.d
f="$d/override.conf"
sudo install -d -m 0755 "$d"
if [ -f "$f" ]; then
echo " backing up existing $f → ${f}.pre-upgrade"
sudo mv "$f" "${f}.pre-upgrade"
fi
if [ -f /etc/docker/daemon.json ] && grep -q '"hosts"' /etc/docker/daemon.json; then
echo " daemon.json has hosts: — installing override that strips -H from ExecStart"
sudo tee "$f" >/dev/null <<'OVR'
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd --containerd=/run/containerd/containerd.sock
OVR
else
echo " daemon.json has no hosts: — base unit's ExecStart is fine, no override needed"
fi
sudo: true
- name: systemctl daemon-reload (so the new unit + override are loaded)
shell: systemctl daemon-reload
sudo: true
- name: Reset the docker.service failed state (3 failed starts during
the install often leaves it in start-rate-limited state — reset
so the next start isn't immediately denied)
shell: systemctl reset-failed docker.service
sudo: true
- name: Enable + start the new daemon (idempotent)
shell: systemctl enable --now docker
sudo: true
- name: Confirm new daemon is responsive + report its version
shell: docker version --format 'client={{.Client.Version}} server={{.Server.Version}} api={{.Server.APIVersion}}'
changed_when: "false"
# ── bring stacks back up ────────────────────────────────────────────
- name: Start every previously-running compose stack
# Use the snapshot we took before the swap. Stacks whose containers
# have restart=unless-stopped will already be coming back via the
# daemon — `compose up -d` is idempotent and surfaces failures we
# might otherwise miss.
shell: |
set +e
while read stack; do
dir="/opt/docker/compose/$stack"
if [ -d "$dir" ]; then
echo " starting $stack"
(cd "$dir" && docker compose up -d) || echo " FAILED — investigate $stack"
fi
done < /tmp/docker-pre-upgrade-stacks.txt
true
- name: Brief settle pause then list container states
shell: sleep 8 && docker ps --format 'table {{.Names}}\t{{.Status}}'
changed_when: "false"
verify:
- name: docker daemon responding
shell: docker info >/dev/null
changed_when: "false"
- name: docker server version is 24+ (was 20.10)
shell: |
v=$(docker version --format '{{.Server.Version}}' | cut -d. -f1)
[ "$v" -ge 24 ]
changed_when: "false"
- name: docker compose plugin v2+ installed
# docker-ce 29 ships docker-compose-plugin renumbered to v5.x (was
# v2.x with docker-ce 26-28). Both are the same Compose v2 codebase
# under the hood — Docker just realigned the plugin's major number.
# Accept anything ≥ 2 so this verify keeps working across future
# plugin renumbers.
shell: |
major=$(docker compose version --short | sed 's/^v//' | cut -d. -f1)
[ "$major" -ge 2 ]
changed_when: "false"
- name: at least one previously-running stack is back up
# If the host had no running stacks pre-upgrade (e.g. nh3-docker may
# be empty), this verify passes trivially.
shell: |
[ ! -s /tmp/docker-pre-upgrade-stacks.txt ] && exit 0
head -1 /tmp/docker-pre-upgrade-stacks.txt | xargs -I {} docker ps \
--filter 'label=com.docker.compose.project={}' --format '{{.Names}}' \
| grep -q .
changed_when: "false"