#!/bin/bash # Docker growth guard — recurring safe prune (kb#184, stability audit 2026-07-24). # # BACKGROUND: root LV (/) holds Docker's data-root. On 2026-07-24 it filled to # 1.5G free / 100% used, risking ENOSPC corruption across every service on Agap. # A one-off safe reclaim (builder prune + dangling images + stopped containers) # took it from 1.5G to 25G free. This script is the recurring guard so it can't # silently refill between now and the next manual audit. # # SAFE SCOPE ONLY (learned from the 2026-07-24 incident — do not expand without # a human decision, see README.md in this directory): # - docker builder prune -f (build cache; always safe, fully rebuildable) # - docker container prune -f (stopped/exited containers only) # - docker image prune -f (DANGLING images only — untagged, no -a) # # EXPLICITLY NOT DONE HERE (human judgment call, see README.md): # - docker image prune -a (removes TAGGED but currently-unused images — # could remove images a human wants kept) # - docker volume prune (destroys live data if a volume is unmounted # but still wanted — never run unattended) # # Pushes freed-space and post-prune build-cache-reclaimable metrics to Zabbix # (trapper items on host AgapHost, hostid 10776). Zabbix lives on lizacer # (192.168.1.4:81), not Agap. # # Run manually to test: bash prune.sh --dry-run # Scheduled via cron (see README.md for the exact line — NOT installed by this # script; installation is a human/handoff step, not something this script does). set -euo pipefail LOG_FILE="/home/alvis/agap_git/docker-maintenance/prune.log" ZABBIX_TOKEN_FILE="/home/alvis/.zabbix_token" ZABBIX_URL="http://192.168.1.4:81/api_jsonrpc.php" ZABBIX_ITEM_ID="70624" # docker.buildcache.reclaimable.bytes on host AgapHost (10776) DRY_RUN=0 if [[ "${1:-}" == "--dry-run" ]]; then DRY_RUN=1 fi log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" } { log "=== docker-maintenance prune.sh start (dry_run=$DRY_RUN) ===" log "--- before ---" df -h / | tail -n +2 docker system df if [[ "$DRY_RUN" -eq 1 ]]; then log "DRY RUN: would run: docker builder prune -f" log "DRY RUN: would run: docker container prune -f" log "DRY RUN: would run: docker image prune -f (dangling only)" else log "Running: docker builder prune -f" docker builder prune -f || true log "Running: docker container prune -f" docker container prune -f || true log "Running: docker image prune -f (dangling only, no -a)" docker image prune -f || true fi log "--- after ---" df -h / | tail -n +2 DF_JSON=$(docker system df --format '{{json .}}') echo "$DF_JSON" # Extract build-cache reclaimable bytes for Zabbix. `docker system df` doesn't # give bytes directly (only human-readable strings like "14.49GB"), so use # `docker system df -v` reclaimable percentage isn't reliable either; instead # compute it from `docker builder du`-equivalent: sum of build cache records # not marked Shared/in-use is nontrivial to script robustly, so fall back to # parsing the "Build Cache" line's RECLAIMABLE column via docker system df. RECLAIM_STR=$(docker system df | awk '/^Build Cache/ {print $NF}') # RECLAIM_STR looks like "14.49GB" or "0B" -- convert to bytes (approx, GB/MB/KB = *1000^n, matching docker's own decimal convention) RECLAIM_BYTES=$(python3 -c " import re, sys s = '$RECLAIM_STR' m = re.match(r'([0-9.]+)\s*([KMGTP]?B)', s) if not m: print(0) else: val, unit = float(m.group(1)), m.group(2) mult = {'B':1,'KB':1000,'MB':1000**2,'GB':1000**3,'TB':1000**4,'PB':1000**5}[unit] print(int(val*mult)) " 2>/dev/null || echo 0) log "Build cache reclaimable: $RECLAIM_STR (~$RECLAIM_BYTES bytes)" if [[ -f "$ZABBIX_TOKEN_FILE" && "$DRY_RUN" -eq 0 ]]; then ZABBIX_TOKEN=$(cat "$ZABBIX_TOKEN_FILE") env -u HTTPS_PROXY -u HTTP_PROXY -u ALL_PROXY -u https_proxy -u http_proxy -u all_proxy \ curl -s -X POST "$ZABBIX_URL" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $ZABBIX_TOKEN" \ -d "{\"jsonrpc\":\"2.0\",\"method\":\"history.push\",\"id\":1,\"params\":{\"itemid\":\"$ZABBIX_ITEM_ID\",\"value\":$RECLAIM_BYTES}}" > /dev/null \ && log "Zabbix notified (docker.buildcache.reclaimable.bytes=$RECLAIM_BYTES)" else log "Skipped Zabbix push (dry-run or token file missing)" fi AVAIL=$(df / | tail -1 | awk '{print $4}') log "Root free: ${AVAIL}KB" log "=== docker-maintenance prune.sh end ===" } | tee -a "$LOG_FILE"