mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
The small GitHub runner shape (AWS, 72G root) ran out of disk in swarm and compose rows (#1329: WordPress#2 of run 41261531965, Nextcloud#6 and #7 of run 41262286962) while an empty instance-store disk nvme1n1 sat unused. mount_spare_disk.sh formats and mounts it at /mnt, but only on a GitHub-hosted runner outside act, and only a whole unpartitioned disk without any signature, mount or swap use of at least 75G; every decline leaves a notice. relocate_docker_dataroot.sh then bind-mounts /var/lib/containerd from the spare disk and moves the Docker data-root to /mnt/docker-host (not /mnt/docker, which is the nested daemon's volume that dedicated.sh wipes). It refuses before stopping anything when images, volumes, build cache, foreign containerd namespaces or shims exist or daemon.json is unreadable, and rolls back the bind mount and daemon.json on any later failure or cancel. Swap goes on / once containerd lives on /mnt, dump_state.sh logs disk size, model and free space per storage root, and the swarm watchdog samples / and /var/lib/containerd every 10 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
174 lines
6.6 KiB
Bash
Executable File
174 lines
6.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Usage: relocate_docker_dataroot.sh MIN_FREE_GB
|
|
# MIN_FREE_GB smallest free space on /mnt that makes the move worth it
|
|
# SKIP_DOCKER_RELOCATE=1 leave Docker and containerd on /
|
|
set -euo pipefail
|
|
|
|
MIN_FREE_GB="${1:?MIN_FREE_GB is required - state the smallest free GB on /mnt worth relocating to at the call site}"
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
DUMP="${HERE}/dump_state.sh"
|
|
|
|
DATA_ROOT=/mnt/docker-host
|
|
CONTAINERD_ROOT=/var/lib/containerd
|
|
CONTAINERD_TARGET=/mnt/containerd
|
|
DAEMON_JSON=/etc/docker/daemon.json
|
|
|
|
# Param: $1 why Docker and containerd stay on /
|
|
decline() {
|
|
echo "::notice::relocate_docker_dataroot.sh left Docker and containerd on /: $1"
|
|
"$DUMP" "docker-relocate skipped ($1)"
|
|
exit 0
|
|
}
|
|
|
|
# Param: $1 what was found that the relocation would lose or orphan
|
|
refuse() {
|
|
echo "::error::relocate_docker_dataroot.sh refuses to move Docker and containerd: $1" >&2
|
|
exit 1
|
|
}
|
|
|
|
if [ "${SKIP_DOCKER_RELOCATE:-0}" = "1" ]; then
|
|
decline "SKIP_DOCKER_RELOCATE=1"
|
|
fi
|
|
|
|
if [ -n "${ACT:-}" ]; then
|
|
echo "::error::relocate_docker_dataroot.sh refuses to run under act (ACT=${ACT}): it restarts Docker and containerd and must never touch the operator's machine" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! [[ "${MIN_FREE_GB}" =~ ^[1-9][0-9]*$ ]]; then
|
|
echo "::error::MIN_FREE_GB must be a positive integer, got '${MIN_FREE_GB}'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ "${RUNNER_ENVIRONMENT:-}" != github-hosted ]; then
|
|
echo "::notice::relocate_docker_dataroot.sh left Docker and containerd where they are: not a GitHub-hosted runner (RUNNER_ENVIRONMENT='${RUNNER_ENVIRONMENT:-}')"
|
|
exit 0
|
|
fi
|
|
|
|
if ! mountpoint -q /mnt; then
|
|
decline "/mnt is not a separate mount"
|
|
fi
|
|
|
|
mnt_free=$(df --output=avail -B1G /mnt | tail -n1 | tr -d ' ')
|
|
root_free=$(df --output=avail -B1G / | tail -n1 | tr -d ' ')
|
|
echo "Free: / ${root_free}G, /mnt ${mnt_free}G; need >= ${MIN_FREE_GB}G on /mnt and roomier than /"
|
|
|
|
if [ "$mnt_free" -lt "$MIN_FREE_GB" ]; then
|
|
decline "/mnt has ${mnt_free}G free, below ${MIN_FREE_GB}G"
|
|
fi
|
|
if [ "$mnt_free" -le "$root_free" ]; then
|
|
decline "/mnt has ${mnt_free}G free, not more than the ${root_free}G on /"
|
|
fi
|
|
|
|
if ! systemctl cat containerd.service >/dev/null; then
|
|
refuse "containerd.service is missing"
|
|
fi
|
|
|
|
configured_root="$(sudo containerd config dump | sed -nE "s/^root = ['\"](.*)['\"]\$/\1/p")"
|
|
if [ "${configured_root}" != "${CONTAINERD_ROOT}" ]; then
|
|
refuse "containerd root is '${configured_root}', expected ${CONTAINERD_ROOT}"
|
|
fi
|
|
|
|
containers="$(docker ps -aq)"
|
|
[ -z "${containers}" ] || refuse "Docker containers exist: ${containers//$'\n'/ }"
|
|
images="$(docker images -q)"
|
|
[ -z "${images}" ] || refuse "Docker images exist: ${images//$'\n'/ }"
|
|
volumes="$(docker volume ls -q)"
|
|
[ -z "${volumes}" ] || refuse "Docker volumes exist: ${volumes//$'\n'/ }"
|
|
build_cache="$(docker system df --format '{{.Type}}={{.TotalCount}}' | sed -n 's/^Build Cache=//p')"
|
|
[ "${build_cache}" = 0 ] || refuse "the build cache holds '${build_cache}' records"
|
|
|
|
namespaces="$(sudo ctr namespaces ls -q)"
|
|
for namespace in ${namespaces}; do
|
|
[ "${namespace}" != moby ] || continue
|
|
foreign="$(sudo ctr -n "${namespace}" containers ls -q)"
|
|
[ -z "${foreign}" ] || refuse "containerd namespace ${namespace} holds containers: ${foreign//$'\n'/ }"
|
|
tasks="$(sudo ctr -n "${namespace}" tasks ls -q)"
|
|
[ -z "${tasks}" ] || refuse "containerd namespace ${namespace} runs tasks: ${tasks//$'\n'/ }"
|
|
done
|
|
|
|
shim_rc=0
|
|
shims="$(pgrep -a containerd-shim)" || shim_rc=$?
|
|
case "${shim_rc}" in
|
|
0) refuse "containerd shims are running: ${shims//$'\n'/; }" ;;
|
|
1) ;;
|
|
*) refuse "pgrep could not list containerd shims (rc=${shim_rc})" ;;
|
|
esac
|
|
|
|
sudo true
|
|
daemon_backup="$(mktemp)"
|
|
daemon_new="$(mktemp)"
|
|
if sudo test -e "${DAEMON_JSON}"; then
|
|
daemon_existed=yes
|
|
# shellcheck disable=SC2024
|
|
sudo cat "${DAEMON_JSON}" >"${daemon_backup}"
|
|
if [ -s "${daemon_backup}" ] && ! jq empty "${daemon_backup}"; then
|
|
refuse "${DAEMON_JSON} is not valid JSON"
|
|
fi
|
|
else
|
|
daemon_existed=no
|
|
fi
|
|
if [ -s "${daemon_backup}" ]; then
|
|
jq --arg dr "${DATA_ROOT}" '. + {"data-root": $dr}' "${daemon_backup}" >"${daemon_new}"
|
|
else
|
|
jq -n --arg dr "${DATA_ROOT}" '{"data-root": $dr}' >"${daemon_new}"
|
|
fi
|
|
|
|
left_behind="$(sudo du -sxh "${CONTAINERD_ROOT}")"
|
|
echo "Left behind on /: ${left_behind%%[[:space:]]*} in ${CONTAINERD_ROOT}"
|
|
echo "Relocating the Docker data-root to ${DATA_ROOT} and the containerd root to ${CONTAINERD_TARGET}"
|
|
|
|
# Param: $1 what failed
|
|
rollback() {
|
|
trap - ERR INT TERM
|
|
set +e
|
|
echo "::error::relocation failed ($1); rolling Docker and containerd back onto /" >&2
|
|
if ! sudo systemctl stop docker.socket docker.service containerd.service; then
|
|
echo "::error::could not stop Docker and containerd for the rollback" >&2
|
|
fi
|
|
if mountpoint -q "${CONTAINERD_ROOT}" && ! sudo umount "${CONTAINERD_ROOT}"; then
|
|
echo "::error::could not unmount the bind over ${CONTAINERD_ROOT}" >&2
|
|
fi
|
|
if [ "${daemon_existed}" = yes ]; then
|
|
if ! sudo install -m 0644 "${daemon_backup}" "${DAEMON_JSON}"; then
|
|
echo "::error::could not restore ${DAEMON_JSON}" >&2
|
|
fi
|
|
elif ! sudo rm -f "${DAEMON_JSON}"; then
|
|
echo "::error::could not remove the ${DAEMON_JSON} this script wrote" >&2
|
|
fi
|
|
if ! sudo systemctl start containerd.service docker.socket docker.service; then
|
|
echo "::error::Docker or containerd did not come back up" >&2
|
|
fi
|
|
sudo systemctl status --no-pager containerd.service docker.service >&2
|
|
sudo journalctl --no-pager -n 80 -u containerd.service -u docker.service >&2
|
|
exit 1
|
|
}
|
|
|
|
trap 'rollback "command failed: ${BASH_COMMAND}"' ERR
|
|
trap 'rollback "interrupted"' INT TERM
|
|
sudo systemctl stop docker.socket docker.service containerd.service
|
|
sudo mkdir -p "${DATA_ROOT}" "${CONTAINERD_TARGET}" "$(dirname "${DAEMON_JSON}")"
|
|
sudo mount --bind "${CONTAINERD_TARGET}" "${CONTAINERD_ROOT}"
|
|
sudo install -m 0644 "${daemon_new}" "${DAEMON_JSON}"
|
|
sudo systemctl start containerd.service
|
|
sudo systemctl start docker.socket docker.service
|
|
sudo ctr version >/dev/null
|
|
|
|
if ! sudo test -f "${CONTAINERD_TARGET}/io.containerd.metadata.v1.bolt/meta.db"; then
|
|
rollback "containerd came up without its metadata in ${CONTAINERD_TARGET}"
|
|
fi
|
|
root_dir="$(docker info --format '{{.DockerRootDir}}')"
|
|
if [ "${root_dir}" != "${DATA_ROOT}" ]; then
|
|
rollback "Docker came up with root '${root_dir}', expected '${DATA_ROOT}'"
|
|
fi
|
|
mnt_dev="$(stat -c %d /mnt)"
|
|
if [ "$(stat -c %d "${root_dir}")" != "${mnt_dev}" ]; then
|
|
rollback "Docker root ${root_dir} is not on the /mnt filesystem"
|
|
fi
|
|
|
|
trap - ERR INT TERM
|
|
rm -f "${daemon_backup}" "${daemon_new}"
|
|
echo "Docker Root Dir: ${root_dir}; containerd root ${CONTAINERD_ROOT} bound from ${CONTAINERD_TARGET}"
|
|
"$DUMP" "after docker-relocate -> ${DATA_ROOT}, containerd -> ${CONTAINERD_TARGET}"
|