gendesign/ops/docker-prune.sh
bot-backend 31c8ac4c5f
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
chore(ops): выставить +x на docker-prune.sh и restore.sh
Оба скрипта закоммичены как 100644, хотя соседние по каталогу backup.sh и
uptime-healthcheck.sh — 100755. На прод-VM файлы лежат с +x, поэтому
`git status` в /opt/gendesign постоянно показывает их как modified:

    M ops/docker-prune.sh
    M ops/restore.sh   (old mode 100644 / new mode 100755, контент идентичен)

Функционально это ничего не ломает: cron зовёт docker-prune.sh через
`bash <путь>`, restore.sh запускают руками так же. Проблема в другом —
постоянный M в прод-репозитории обесценивает единственный дешёвый сигнал,
по которому видно ручную правку файла на проде. Шум надо убирать, а не
привыкать к нему.

Приводим режим к тому, что реально на диске и что уже стоит у соседей.
2026-08-15 17:17:58 +03:00

102 lines
5.8 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Периодическая уборка docker-мусора на прод-VM.
#
# ЗАЧЕМ. 2026-08-15 диск был занят на 76% (110 из 145 ГБ). Разбор показал 201
# том-сироту на 12.6 ГБ: 125 анонимных — каталоги данных PostgreSQL от тестовых
# прогонов CI, 76 — окружения задач Forgejo Actions. Прод-данных среди них не
# было ни одного.
#
# Корневая причина анонимных томов устранена отдельно: ci.yml и ci-tradein.yml
# снимали свой postgres через `docker rm -f` БЕЗ `-v`, поэтому контейнер уходил,
# а его том оставался. Теперь там `docker rm -fv`. Этот скрипт — страховка: он
# подбирает то, что runner не убрал за собой, и то, что накопилось раньше.
#
# ЧТО ИМЕННО УДАЛЯЕТСЯ (осознанно консервативно):
# - остановленные контейнеры старше 24ч;
# - висячие (dangling) образы старше 7 суток;
# - тома-сироты ТОЛЬКО двух известных форм: 64-символьный hex (анонимные) и
# FORGEJO-ACTIONS-TASK-*. Именованные тома со смыслом (gendesign_postgres_data,
# tradein-postgres-data, *_caddy_*, couchdb, redis и любые будущие) не трогаются
# НИКОГДА — даже если в моменте оказались отцеплены. Голый `docker volume prune`
# такой разницы не делает, поэтому здесь он намеренно не используется.
#
# Usage (cron на прод-VM; `bash <путь>`, а не голый путь — тогда снятый +x не ломает).
# Лог в /tmp — как у соседних записей в том же crontab (backup.sh, backfill'ы):
# 0 4 * * 0 bash /opt/gendesign/ops/docker-prune.sh >> /tmp/gendesign-docker-prune.log 2>&1
#
# Воскресенье 04:00 UTC — свободный слот: рядом 03:30 backup.sh, 04:30 backup
# tradein, 05:00+ backfill'ы.
#
# Раз в неделю достаточно: после устранения корневой причины (docker rm -fv в CI)
# копятся только тома runner'а. DRY_RUN=1 — показать, что удалится, не трогая.
set -euo pipefail
DRY_RUN="${DRY_RUN:-0}"
STOPPED_AGE="${STOPPED_AGE:-24h}"
IMAGE_AGE="${IMAGE_AGE:-168h}"
log() { printf '%s %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$*"; }
disk_used_pct() { df --output=pcent / | tail -1 | tr -dc '0-9'; }
before_pct="$(disk_used_pct)"
log "старт: диск занят ${before_pct}%"
if [[ "$DRY_RUN" == "1" ]]; then
log "DRY_RUN=1 — только показываю"
fi
# ── 1. остановленные контейнеры ───────────────────────────────────────────────
if [[ "$DRY_RUN" == "1" ]]; then
# `until` поддерживает только `prune`, у `ls` его нет («invalid filter 'until'»),
# поэтому в dry-run считаем ВСЕ остановленные — это верхняя оценка.
log "остановленных контейнеров всего (удалятся только старше ${STOPPED_AGE}): \
$(docker container ls -aq --filter "status=exited" | wc -l)"
else
log "контейнеры: $(docker container prune -f --filter "until=${STOPPED_AGE}" \
2>&1 | tail -1)"
fi
# ── 2. висячие образы ─────────────────────────────────────────────────────────
if [[ "$DRY_RUN" == "1" ]]; then
log "висячих образов: $(docker image ls -qf dangling=true | wc -l)"
else
log "образы: $(docker image prune -f --filter "until=${IMAGE_AGE}" 2>&1 | tail -1)"
fi
# ── 3. тома-сироты известных форм ─────────────────────────────────────────────
# Отбираем ПОИМЁННО, а не через `docker volume prune`: тот снёс бы любой
# отцепленный именованный том, включая боевой, если контейнер в моменте пересоздаётся.
mapfile -t candidates < <(
docker volume ls -qf dangling=true \
| grep -E '^([0-9a-f]{64}|FORGEJO-ACTIONS-TASK-.*)$' || true
)
skipped="$(docker volume ls -qf dangling=true \
| grep -vE '^([0-9a-f]{64}|FORGEJO-ACTIONS-TASK-.*)$' || true)"
if [[ -n "$skipped" ]]; then
log "ПРОПУЩЕНЫ (именованные, руками): $(echo "$skipped" | tr '\n' ' ')"
fi
if [[ "${#candidates[@]}" -eq 0 ]]; then
log "томов-сирот известных форм нет"
elif [[ "$DRY_RUN" == "1" ]]; then
log "томов к удалению: ${#candidates[@]}"
else
removed=0
for v in "${candidates[@]}"; do
if docker volume rm "$v" >/dev/null 2>&1; then
removed=$((removed + 1))
fi
done
log "томов удалено: ${removed} из ${#candidates[@]}"
fi
after_pct="$(disk_used_pct)"
log "готово: диск занят ${after_pct}% (было ${before_pct}%)"
# Сигнал в лог, если места всё равно мало — повод посмотреть глазами.
if [[ "$after_pct" -ge 85 ]]; then
log "ВНИМАНИЕ: диск занят ${after_pct}% — уборки уже недостаточно"
fi