b0608f9300
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.
247 lines
10 KiB
YAML
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"
|