ops(docker): prune unused cache on deploys and nightly
Add scripts/docker-prune.sh (build cache >72h capped at 4g, unreferenced images >7d, stopped containers >24h; never volumes), run it after every CI deploy and compose update, and schedule a nightly prune from the host-side jobs-worker. Tighten the deployment contract tests to assert the scoped-prune boundaries.
This commit is contained in:
1 parent
fb68df3ba9
commit
47917bb63b
6 files changed
+102
-5
No files matched your search
@@ -193,5 +193,6 @@ while IFS= read -r tag; do
|
||||
if [ -n "$tagged_image" ] && [ "$tagged_image" != "$previous_image" ]; then docker image rm "$tag" || true; fi
|
||||
fi
|
||||
done < <(docker image ls --format '{{.Repository}}:{{.Tag}}' epicnext-cms)
|
||||
docker builder prune -af --filter "until=72h" --max-used-space=4g || true
|
||||
docker image prune -f --filter "until=168h" || true
|
||||
# Reclaim build cache, unreferenced images and long-stopped containers. Never
|
||||
# volumes; retention boundaries are enforced inside docker-prune.sh.
|
||||
bash "$deploy_dir/scripts/docker-prune.sh" || true
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reclaim Docker's unused cache so host storage stays bounded.
|
||||
#
|
||||
# Safe scopes only, by design:
|
||||
# - BuildKit cache older than 72h, hard-capped at 4 GB (Debian /pnpm store is
|
||||
# shared across builds; everything newer than that speeds up rebuilds).
|
||||
# - Images referenced by NO running/stopped container and older than 7 days
|
||||
# (covers stale epicnext-cms sha tags, old mariadb/byparr pulls, etc.).
|
||||
# - Containers stopped for more than 24h.
|
||||
#
|
||||
# Volumes are NEVER pruned here: mariadb-turbo-data is a database. This script
|
||||
# is idempotent and exits 0 when Docker is unavailable.
|
||||
set -Eeuo pipefail
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
LOG_DIR="${LOG_DIR:-$DIR/logs}"
|
||||
mkdir -p "$LOG_DIR"
|
||||
LOG_FILE="$LOG_DIR/docker-prune.log"
|
||||
now() { date '+%Y-%m-%d %H:%M:%S'; }
|
||||
|
||||
command -v docker >/dev/null 2>&1 || {
|
||||
printf '[%s] docker CLI unavailable; nothing to prune\n' "$(now)" >>"$LOG_FILE"
|
||||
exit 0
|
||||
}
|
||||
|
||||
printf '\n[%s] === docker prune start ===\n' "$(now)" >>"$LOG_FILE"
|
||||
docker system df >>"$LOG_FILE" 2>&1 || true
|
||||
|
||||
docker builder prune -af --filter "until=72h" --max-used-space=4g >>"$LOG_FILE" 2>&1 || true
|
||||
docker image prune -af --filter "until=168h" >>"$LOG_FILE" 2>&1 || true
|
||||
docker container prune -f --filter "until=24h" >>"$LOG_FILE" 2>&1 || true
|
||||
|
||||
printf '\n[%s] === docker prune complete ===\n' "$(now)" >>"$LOG_FILE"
|
||||
docker system df >>"$LOG_FILE" 2>&1 || true
|
||||
@@ -150,3 +150,7 @@ done
|
||||
printf '%s\n' "${kept[@]}" "${pending[@]}" > "$history.tmp"
|
||||
mv "$history.tmp" "$history"
|
||||
log "Keeping the two latest releases; in-use images and persistent volumes are preserved."
|
||||
# Reclaim build cache + unreferenced images + long-stopped containers only;
|
||||
# the scoped retention is enforced inside docker-prune.sh (never volumes).
|
||||
bash "$DIR/scripts/docker-prune.sh" >>"$LOG_FILE" 2>&1 || log "Docker prune reported an error (see $LOG_FILE)"
|
||||
log "Pruned unused Docker cache."
|
||||
@@ -232,6 +232,38 @@ async function cleanupOldSessions(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Host-side: reclaim Docker's unused cache (build cache, unreferenced images,
|
||||
* stopped containers). Volumes and in-use images are never touched. No-op when
|
||||
* docker or the prune script is unavailable. */
|
||||
async function pruneDockerCache(): Promise<void> {
|
||||
const { access } = await import("node:fs/promises");
|
||||
const { resolve } = await import("node:path");
|
||||
const { spawn } = await import("node:child_process");
|
||||
const script = resolve(process.cwd(), "scripts", "docker-prune.sh");
|
||||
try {
|
||||
await access(script);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
await new Promise<void>((resolvePromise) => {
|
||||
const child = spawn("bash", [script], { stdio: "ignore" });
|
||||
child.on("error", (err) =>
|
||||
captureWorkerError(
|
||||
err,
|
||||
"Docker prune could not start (is bash on PATH?)",
|
||||
),
|
||||
);
|
||||
child.on("close", (code) => {
|
||||
if (code !== 0)
|
||||
captureWorkerError(
|
||||
new Error(`docker-prune.sh exited ${code}`),
|
||||
"Docker prune failed",
|
||||
);
|
||||
resolvePromise();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function publishScheduledArticles(): Promise<void> {
|
||||
try {
|
||||
const now = new Date();
|
||||
@@ -305,6 +337,15 @@ async function main() {
|
||||
});
|
||||
logger.info("Scheduled: old data cleanup (daily 04:00)", { module: "jobs" });
|
||||
|
||||
new Cron("0 5 * * *", () => {
|
||||
pruneDockerCache().catch((e) =>
|
||||
captureWorkerError(e, "Docker prune error"),
|
||||
);
|
||||
});
|
||||
logger.info("Scheduled: Docker cache prune (daily 05:00)", {
|
||||
module: "jobs",
|
||||
});
|
||||
|
||||
new Cron("*/5 * * * *", () => {
|
||||
checkOpsHealth().catch((e) => captureWorkerError(e, "Health check error"));
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user