Files
cc-ci/nix/modules/docker-prune.nix
T
2026-11-02 17:56:11 +00:00

83 lines
3.8 KiB
Nix

# Conservative, surgical Docker prune (Phase 2pc / PC1).
#
# REPLACES `virtualisation.docker.autoPrune` (which ran `docker system prune --force --all
# --filter until=24h` daily). The `--all` removed every image NOT used by a *running* container —
# between CI runs no test apps run, so it evicted the cached recipe base images and forced a cold
# re-pull on the next run → the prune->re-pull->Docker-Hub-rate-limit churn documented in JOURNAL-2.
#
# On this single host, Docker's local image store is the CI cache. Keep tagged images during
# routine pressure, but evict unused old images at 90% usage. This recovers from a warm service
# crash loop: an unmet replica must not prevent the emergency cleanup that lets it restart.
# Never prune volumes, and skip while a run-app stack is live.
{ pkgs, ... }:
let
# `/` usage % at/above which a surgical prune is permitted. Below this: keep the cache, no-op.
threshold = 80;
emergencyThreshold = 90;
prune = pkgs.writeShellApplication {
name = "cc-ci-docker-prune";
runtimeInputs = with pkgs; [ docker coreutils gnugrep gawk ];
text = ''
THRESH=${toString threshold}
EMERGENCY_THRESH=${toString emergencyThreshold}
used="$(df --output=pcent / | tail -1 | tr -dc '0-9')"
: "''${used:=0}"
if [ "$used" -lt "$THRESH" ]; then
echo "docker-prune: / at ''${used}% (< ''${THRESH}%) — keeping local image cache, nothing to do"
exit 0
fi
# NEVER prune mid-run: a live run-app stack means a deploy/test is in flight (mid-pull layers
# can look prunable). Run-app services: <=4char>-<6hex>_<zone-stem>_* (lifecycle.py).
# Zone stem matches BOTH ci_autonomic_zone (current) and ci_commoninternet_net
# (legacy stacks from before the 2026-09 domain cutover) during the bake window.
if docker service ls --format '{{.Name}}' \
| grep -qE '^[a-z0-9]{1,4}-[0-9a-f]{6}_ci_(autonomic_zone|commoninternet_net)_'; then
echo "docker-prune: a run-app stack is live — skipping (never prune mid-run)"
exit 0
fi
if [ "$used" -ge "$EMERGENCY_THRESH" ]; then
echo "docker-prune: / at ''${used}% (>= ''${EMERGENCY_THRESH}%) — emergency prune of unused images older than 7d"
docker container prune -f --filter until=24h || true
docker image prune -af --filter until=168h || true
docker builder prune -f --filter until=24h || true
df -h /
exit 0
fi
# NEVER prune while ANY swarm service is converging (unmet replicas => a pull/deploy in flight,
# including infra warm redeploys). Replicas field is "running/desired" e.g. 1/1.
converging="$(docker service ls --format '{{.Replicas}}' \
| awk -F/ '{ if (($1+0) != ($2+0)) c++ } END { print c+0 }')"
if [ "$converging" -gt 0 ]; then
echo "docker-prune: $converging service(s) converging (deploy/pull in flight) — skipping"
exit 0
fi
echo "docker-prune: / at ''${used}% (>= ''${THRESH}%) — surgical prune (dangling + until=24h; NEVER --all/--volumes)"
docker container prune -f --filter until=24h || true
docker image prune -f --filter until=24h || true
docker builder prune -f --filter until=24h || true
df -h /
'';
};
in
{
systemd.services.ci-docker-prune = {
description = "Disk-pressure-gated Docker prune (old unused images at 90%; never volumes or mid-run)";
after = [ "docker.service" ];
requires = [ "docker.service" ];
path = [ pkgs.docker ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${prune}/bin/cc-ci-docker-prune";
};
};
systemd.timers.ci-docker-prune = {
description = "Daily timer for the surgical Docker prune";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "daily";
Persistent = true;
};
};
}