Files
esh-pfi-infrastructure/playbooks/upgrade-docker-ce.yaml
T
vh a3ab1a7b6c playbooks/upgrade-docker-ce: detect + move aside stale unit override
nh3-docker's daemon kept failing post-package-swap with status=203
even after daemon-reload. Root cause: a stale
/etc/systemd/system/docker.service.d/override.conf from the docker.io
era hardcoding ExecStart=/usr/sbin/dockerd. The override (a) points
at the no-longer-existing path, AND (b) typically also adds
-H tcp://... which now duplicates the hosts: setting in
/etc/docker/daemon.json — dockerd refuses to start when both define
hosts ('conflicting host options').

Daemon.json is the modern way to expose the TCP socket. The
override is redundant and wrong. Move it aside (preserve a
.pre-upgrade copy for forensics), then daemon-reload, then start.

Should let esh-docker-vm and ana-docker upgrades go through cleanly
without the manual debug loop nh3-docker required.
2026-04-26 14:03:06 -07:00

206 lines
8.4 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: Move aside any stale docker.service drop-in override
# The docker.io era often left an /etc/systemd/system/docker.service.d/
# override.conf adding `-H tcp://...` to ExecStart for remote docker
# discovery. After the swap, the override (a) hard-codes
# /usr/sbin/dockerd which no longer exists, and (b) duplicates the
# `hosts:` setting in /etc/docker/daemon.json — dockerd refuses to
# start when both are set ("conflicting host options"). daemon.json
# is the modern way; the override is redundant.
shell: |
f=/etc/systemd/system/docker.service.d/override.conf
if [ -f "$f" ]; then
echo " moving aside $f"
sudo mv "$f" "${f}.pre-upgrade"
fi
sudo: true
- name: systemctl daemon-reload (CRITICAL — package swap put dockerd at
/usr/bin/dockerd; without reload, systemd keeps the old unit's
ExecStart pointing at /usr/sbin/dockerd from docker.io and fails
with status=203/EXEC "No such file or directory")
shell: systemctl daemon-reload
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
shell: docker compose version | grep -qE 'v2\.[0-9]+\.[0-9]+'
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"