Merge pull request 'chore(ops): зависшие job-контейнеры, лимит журнала, мёртвый блок status в Caddy' (#2912) from chore/ops-prune-logs-caddy into main
Some checks failed
Deploy / deploy-status (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 13s
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
Some checks failed
Deploy / deploy-status (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 13s
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
This commit is contained in:
commit
1fe0e90f66
3 changed files with 116 additions and 22 deletions
22
Caddyfile
22
Caddyfile
|
|
@ -222,8 +222,7 @@ www.gendsgn.ru {
|
||||||
# резолвится на этот сервер → HTTP-01/TLS-ALPN challenge недостижим) и продолжит
|
# резолвится на этот сервер → HTTP-01/TLS-ALPN challenge недостижим) и продолжит
|
||||||
# ретраить с backoff, ПОКА запись не появится. Остальные site-блоки в этом же
|
# ретраить с backoff, ПОКА запись не появится. Остальные site-блоки в этом же
|
||||||
# Caddyfile (gendsgn.ru, obsidian.gendsgn.ru и т.д.) не затрагиваются —
|
# Caddyfile (gendsgn.ru, obsidian.gendsgn.ru и т.д.) не затрагиваются —
|
||||||
# автоматический HTTPS в Caddy изолирован per-hostname (тот же принцип, что
|
# автоматический HTTPS в Caddy изолирован per-hostname. Повторные неудачные попытки ДО
|
||||||
# уже описан для status.gendsgn.ru ниже). Повторные неудачные попытки ДО
|
|
||||||
# появления DNS могут исчерпать rate-limit Let's Encrypt (5 failed
|
# появления DNS могут исчерпать rate-limit Let's Encrypt (5 failed
|
||||||
# validations/hostname/hour) — не критично, просто подождать; `docker volume
|
# validations/hostname/hour) — не критично, просто подождать; `docker volume
|
||||||
# rm gendesign_caddy_data` для этого НЕ нужен (и вообще требует user-approval).
|
# rm gendesign_caddy_data` для этого НЕ нужен (и вообще требует user-approval).
|
||||||
|
|
@ -370,25 +369,6 @@ errors.gendsgn.ru {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Uptime Kuma — self-hosted uptime monitoring + public status page (#75 B6-1).
|
|
||||||
# DNS: A-record status.gendsgn.ru → IP VPS (добавить перед деплоем стека).
|
|
||||||
# Контейнер из docker-compose.uptime.yml (project gendesign-uptime) на shared
|
|
||||||
# gendesign_shared network. Если стек не запущен — Caddy отдаёт 502 ТОЛЬКО на
|
|
||||||
# этом домене, main-сайт не страдает (как obsidian.gendsgn.ru).
|
|
||||||
#
|
|
||||||
# ВНИМАНИЕ: status-page НАМЕРЕННО публичен (trust-building для пилотов, issue #75).
|
|
||||||
# Admin-панель Kuma (/dashboard, /manage-*) защищена собственным логином Kuma —
|
|
||||||
# НЕ кладём её за caddy/users.caddy.snippet, иначе double-auth сломает setup.
|
|
||||||
status.gendsgn.ru {
|
|
||||||
encode zstd gzip
|
|
||||||
|
|
||||||
reverse_proxy uptime-kuma:3001
|
|
||||||
|
|
||||||
log {
|
|
||||||
output file /var/log/caddy/status.gendsgn.ru.log
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Forgejo — self-hosted git (migration 2026-05-16).
|
# Forgejo — self-hosted git (migration 2026-05-16).
|
||||||
# DNS: A-record git.gendsgn.ru → IP VPS.
|
# DNS: A-record git.gendsgn.ru → IP VPS.
|
||||||
# Forgejo container из forgejo-migration/docker-compose.yml на shared
|
# Forgejo container из forgejo-migration/docker-compose.yml на shared
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,15 @@
|
||||||
# FORGEJO-ACTIONS-TASK-*. Именованные тома со смыслом (gendesign_postgres_data,
|
# FORGEJO-ACTIONS-TASK-*. Именованные тома со смыслом (gendesign_postgres_data,
|
||||||
# tradein-postgres-data, *_caddy_*, couchdb, redis и любые будущие) не трогаются
|
# tradein-postgres-data, *_caddy_*, couchdb, redis и любые будущие) не трогаются
|
||||||
# НИКОГДА — даже если в моменте оказались отцеплены. Голый `docker volume prune`
|
# НИКОГДА — даже если в моменте оказались отцеплены. Голый `docker volume prune`
|
||||||
# такой разницы не делает, поэтому здесь он намеренно не используется.
|
# такой разницы не делает, поэтому здесь он намеренно не используется;
|
||||||
|
# - зависшие (running, но фактически брошенные) job-контейнеры раннера Forgejo
|
||||||
|
# Actions старше JOB_CONTAINER_MAX_AGE_HOURS. 2026-08-15: живьём на проде
|
||||||
|
# обнаружены три штуки в статусе Up 4-8 недель (раннер не убрал контейнер
|
||||||
|
# после прерванного/упавшего workflow — task killed, рестарт раннера в
|
||||||
|
# процессе job'а и т.п.). CI job физически не идёт сутками, поэтому что
|
||||||
|
# угодно с этим именем старше порога — гарантированный мусор, а не активная
|
||||||
|
# задача. `docker container prune` их не видит: тот фильтрует только
|
||||||
|
# status=exited, а эти контейнеры формально Up.
|
||||||
#
|
#
|
||||||
# Usage (cron на прод-VM; `bash <путь>`, а не голый путь — тогда снятый +x не ломает).
|
# Usage (cron на прод-VM; `bash <путь>`, а не голый путь — тогда снятый +x не ломает).
|
||||||
# Лог в /tmp — как у соседних записей в том же crontab (backup.sh, backfill'ы):
|
# Лог в /tmp — как у соседних записей в том же crontab (backup.sh, backfill'ы):
|
||||||
|
|
@ -35,6 +43,9 @@ set -euo pipefail
|
||||||
DRY_RUN="${DRY_RUN:-0}"
|
DRY_RUN="${DRY_RUN:-0}"
|
||||||
STOPPED_AGE="${STOPPED_AGE:-24h}"
|
STOPPED_AGE="${STOPPED_AGE:-24h}"
|
||||||
IMAGE_AGE="${IMAGE_AGE:-168h}"
|
IMAGE_AGE="${IMAGE_AGE:-168h}"
|
||||||
|
# Job CI никогда не идёт сутками — что угодно с именем job-контейнера раннера
|
||||||
|
# старше этого порога снимается безусловно (см. секцию 4 ниже).
|
||||||
|
JOB_CONTAINER_MAX_AGE_HOURS="${JOB_CONTAINER_MAX_AGE_HOURS:-24}"
|
||||||
|
|
||||||
log() { printf '%s %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$*"; }
|
log() { printf '%s %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$*"; }
|
||||||
|
|
||||||
|
|
@ -93,6 +104,73 @@ else
|
||||||
log "томов удалено: ${removed} из ${#candidates[@]}"
|
log "томов удалено: ${removed} из ${#candidates[@]}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── 4. зависшие job-контейнеры раннера Forgejo Actions ───────────────────────
|
||||||
|
# Фильтр по имени — ЯКОРЬ на начало (`^FORGEJO-ACTIONS-TASK-`), не "содержит
|
||||||
|
# подстроку": `docker ps --filter name=` матчит как regex, поэтому `^...`
|
||||||
|
# гарантирует точный префикс, а не случайное совпадение где-то в середине
|
||||||
|
# имени сервисного контейнера. Долгоживущие сервисные контейнеры (forgejo,
|
||||||
|
# forgejo-runner*, gendesign-*, tradein-*, couchdb) под этот префикс не
|
||||||
|
# подпадают вообще — но ниже всё равно есть explicit-skip как страховка на
|
||||||
|
# случай будущего переименования, а не молчаливая надежда на то, что фильтр
|
||||||
|
# никогда не ошибётся.
|
||||||
|
#
|
||||||
|
# Возраст — из `docker inspect .State.StartedAt` (RFC3339), НЕ из текстового
|
||||||
|
# "Up 4 weeks" в выводе `docker ps`: тот округляет к ближайшей крупной единице
|
||||||
|
# и не пригоден для сравнения с порогом в часах.
|
||||||
|
mapfile -t job_ids < <(docker ps -aq --filter "name=^FORGEJO-ACTIONS-TASK-" || true)
|
||||||
|
|
||||||
|
job_removed=0
|
||||||
|
job_candidates=0
|
||||||
|
if [[ "${#job_ids[@]}" -eq 0 ]]; then
|
||||||
|
log "зависших job-контейнеров нет"
|
||||||
|
else
|
||||||
|
for id in "${job_ids[@]}"; do
|
||||||
|
name="$(docker inspect --format '{{.Name}}' "$id" 2>/dev/null | sed 's#^/##' || true)"
|
||||||
|
[[ -z "$name" ]] && continue
|
||||||
|
|
||||||
|
case "$name" in
|
||||||
|
forgejo | forgejo-runner* | gendesign-* | tradein-* | couchdb)
|
||||||
|
log "job-контейнеры: ПРОПУЩЕН сервисный '${name}' (не должен был пройти фильтр имени)"
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
started_at="$(docker inspect --format '{{.State.StartedAt}}' "$id" 2>/dev/null || true)"
|
||||||
|
[[ -z "$started_at" || "$started_at" == "0001-01-01T00:00:00Z" ]] && continue
|
||||||
|
|
||||||
|
started_epoch="$(date -u -d "$started_at" +%s 2>/dev/null || echo 0)"
|
||||||
|
[[ "$started_epoch" -eq 0 ]] && continue
|
||||||
|
|
||||||
|
now_epoch="$(date -u +%s)"
|
||||||
|
age_hours=$(((now_epoch - started_epoch) / 3600))
|
||||||
|
[[ "$age_hours" -lt "$JOB_CONTAINER_MAX_AGE_HOURS" ]] && continue
|
||||||
|
|
||||||
|
job_candidates=$((job_candidates + 1))
|
||||||
|
size="$(docker ps -a --filter "id=${id}" --size --format '{{.Size}}' 2>/dev/null \
|
||||||
|
| awk '{print $1}' || true)"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == "1" ]]; then
|
||||||
|
log "job-контейнеры: [dry-run] снял бы '${name}' (возраст ${age_hours}ч, writable-слой ${size:-?})"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if docker rm -f "$id" >/dev/null 2>&1; then
|
||||||
|
job_removed=$((job_removed + 1))
|
||||||
|
log "job-контейнеры: снят '${name}' (возраст ${age_hours}ч, writable-слой ${size:-?} освобождён)"
|
||||||
|
else
|
||||||
|
log "job-контейнеры: НЕ удалось снять '${name}' (id ${id:0:12})"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$job_candidates" -eq 0 ]]; then
|
||||||
|
log "job-контейнеры: ${#job_ids[@]} шт., ни один не старше порога ${JOB_CONTAINER_MAX_AGE_HOURS}ч"
|
||||||
|
elif [[ "$DRY_RUN" == "1" ]]; then
|
||||||
|
log "job-контейнеры: к снятию ${job_candidates} из ${#job_ids[@]}"
|
||||||
|
else
|
||||||
|
log "job-контейнеры: снято ${job_removed} из ${job_candidates} кандидатов (порог ${JOB_CONTAINER_MAX_AGE_HOURS}ч)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
after_pct="$(disk_used_pct)"
|
after_pct="$(disk_used_pct)"
|
||||||
log "готово: диск занят ${after_pct}% (было ${before_pct}%)"
|
log "готово: диск занят ${after_pct}% (было ${before_pct}%)"
|
||||||
|
|
||||||
|
|
|
||||||
36
ops/journald-gendesign.conf.example
Normal file
36
ops/journald-gendesign.conf.example
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# systemd-journald drop-in — cap persistent journal disk usage on prod VPS.
|
||||||
|
#
|
||||||
|
# ЗАМЕР 2026-08-15 (ssh gendesign, read-only): `/var/log` занимал 3.1G. Наивная
|
||||||
|
# первая проверка `journalctl --disk-usage` показала только 174M и навела на
|
||||||
|
# ложный след «основной объём — не journald». На деле `journalctl --disk-usage`,
|
||||||
|
# запущенный НЕ из группы systemd-journal/adm, недосчитывает — он не может
|
||||||
|
# полноценно перечислить архивные *.journal файлы без прав на чтение. Прямой
|
||||||
|
# `du -sh /var/log/journal` дал 2.5G — это ~80% всего `/var/log`, ровно 100
|
||||||
|
# файлов по ~48M в /var/log/journal/<machine-id>/. Второй по размеру вклад —
|
||||||
|
# традиционный rsyslog (syslog/syslog.1/auth.log/kern.log/ufw.log/dmesg/btmp,
|
||||||
|
# ~0.6G) — те уже ротируются через logrotate (видны .1/.4.gz копии), отдельного
|
||||||
|
# вмешательства не требуют и вне scope этого файла.
|
||||||
|
#
|
||||||
|
# В /etc/systemd/journald.conf на проде НЕТ SystemMaxUse (все ключи закомменчены
|
||||||
|
# дефолтами) — без явного лимита journald довольствуется default-правилом
|
||||||
|
# «до 10% файловой системы», на VPS с диском ~145G это фактически безлимит.
|
||||||
|
#
|
||||||
|
# УСТАНОВКА НА СЕРВЕРЕ (руками, deploy.yml этот файл НЕ подхватывает —
|
||||||
|
# systemd-конфиги вне /opt/gendesign, деплой синкает только сам репозиторий):
|
||||||
|
# sudo mkdir -p /etc/systemd/journald.conf.d
|
||||||
|
# sudo cp /opt/gendesign/ops/journald-gendesign.conf.example \
|
||||||
|
# /etc/systemd/journald.conf.d/gendesign-max-use.conf
|
||||||
|
# sudo systemctl restart systemd-journald
|
||||||
|
#
|
||||||
|
# `restart systemd-journald` применяет лимит немедленно — journald сам
|
||||||
|
# провакуумит существующие архивные файлы вниз до SystemMaxUse (ожидаемый
|
||||||
|
# эффект: /var/log/journal схлопнется примерно с 2.5G до ~500M). Это НЕ
|
||||||
|
# `docker volume rm` / `caddy reload` — под общий deploy-guard не подпадает,
|
||||||
|
# но всё равно на живом проде: делает user сам после ревью PR.
|
||||||
|
#
|
||||||
|
# Значение 500M — консервативный запас на 4 vCPU/4-16G VPS с активным CI
|
||||||
|
# (docker/forgejo-runner логи в journald тоже льются). При необходимости
|
||||||
|
# больше retention для дебага — поднять SystemMaxUse, не удалять файл.
|
||||||
|
|
||||||
|
[Journal]
|
||||||
|
SystemMaxUse=500M
|
||||||
Loading…
Add table
Reference in a new issue