ADR-0002: Limpieza, retention policies y consolidación cross-host¶
| Campo | Valor |
|---|---|
| Status | Implemented (2026-04-29) |
| Date | 2026-04-29 |
| Decision by | ramon |
| Reviewers | system-architect (subagent review) |
| Supersedes | — |
| Builds on | ADR-0001 (Routing+Tooling consolidation, Implemented 2026-04-28) |
Contexto¶
Tras ADR-0001 (consolidación routing + homelab-ctl.py + stacks_shared
network) y los cleanups de ayer (jobhunter, gastos, ydl_api_ng, lidarr,
TorrentLand) el sistema está sano (43/43 routes OK, smoke pasa, watchdog
auto-curativo) pero quedan smells de mantenimiento y sin política de
retención que generan acumulación pasiva.
Auditoría de hoy revela:
- 49 archivos
.bak*acumulados en~/stacks/reverse-proxy/y otros (cadahomelab-ctl syncgenera uno; sin cleanup). - 6 directorios appdata huérfanos sin container ni compose
(
ydl_api_ng223 MB,jellystat-backup270 MB,gastos,pocketid,sso,loki-config). - 12 docker images dangling = 4.3 GB recuperables.
- 10 snapshots Proxmox VM 208 acumulados de ADR-0001 (la mitad ya innecesarios tras validación >24h).
- 2 clones git de
monxas/trip-plannerlocalmente (~/trip-plannerFeb 10,~/projects/trip-plannerFeb 7), ambos outdated tras los push de hoy a master. - 7 logs locales en
~/scripts/*.logsin ingestar a Loki (gap de observabilidad cuando el resto del stack ya pasa por Loki). - Tooling cross-host fragmentado entre 4 hosts (pmx, pmx2, LXC 253, VM 208) sin inventario centralizado ni deploy automatizado.
- Documentación dispersa:
monitoring-operations-runbook.mdvive en~/scripts/(lugar wrong), debería estar en~/stacks/docs/runbooks/.
Decision Drivers¶
- Higiene operativa: el sistema funciona, pero la entropía silenciosa reduce la velocidad de respuesta cuando algo se rompe.
- Reversibilidad: cada acción debe ser trivialmente reversible; no tocar appdata "viva" sin validación.
- Coste/beneficio: NO tocar lo que funciona bien (
homelab-ctl.py,morning-report.py, n8n workflows). Sólo periferia. - Política sobre acción puntual: arreglar la causa raíz (sin retention) en lugar de limpiar manualmente cada N días.
Decisión¶
Ejecutar 3 fases secuenciales ordenadas por riesgo creciente:
- Fase 1 — Quick wins (~30 min): cleanup mecánico, riesgo 0.
- Fase 2 — Retention policies (~30 min): prevenir acumulación futura.
- Fase 3 — Tooling consolidation cross-host (~3-4h, opt-in): repo git unificado, ingesta logs a Loki. Beneficio mediano-largo plazo, no bloqueante.
Cada fase con snapshot Proxmox antes y validación posterior.
Fase 1 — Quick wins (30 min, riesgo 0)¶
1.1 Docker images dangling (3 min, +4.3 GB)¶
1.2 Appdata huérfana (5 min, +500 MB)¶
Confirmado sin container ni compose owner:
Pendiente validación: ~/appdata/jellystat-backup (270 MB) — verificar
si algún cron de backup lo escribe.
ssh media-208 'grep -rln "jellystat-backup" /etc/cron* ~/scripts /opt 2>/dev/null'
# Si vacío → safe to delete
1.3 Scripts legacy en ~/scripts/ (5 min)¶
ssh media-208 '
cd ~/scripts &&
rm -rf backup-20260222 __pycache__ &&
rm -f daily-report.{sh,log} \
permission-fix.log \
sablier-watchdog.log \
docker-real-reclaimable-v2.sh.bak.20260221 \
discovered-containers.json
'
Mover doc:
ssh media-208 '
mkdir -p ~/stacks/docs/runbooks &&
mv ~/scripts/monitoring-operations-runbook.md ~/stacks/docs/runbooks/
'
Verificar ski-monitor y web-monitor dirs antes de borrar (pueden ser
proyectos del usuario):
1.4 Snapshots Proxmox VM 208 (5 min)¶
Mantener post_phase3 (último estado bueno) + pre_sablier_20260425_1521
(rollback pre-ADR-0001 conocido). Borrar el resto:
ssh pmx '
for snap in pre_simplify_20260428_1904 \
pre_managed_20260428_1959 \
pre_quickwins_20260428_2027 \
pre_phase1_20260428_2110 \
post_phase1_20260428_2113 \
pre_phase2_20260428_2114 \
post_phase2_20260428_2120 \
pre_phase3_20260428_2122; do
qm delsnapshot 208 $snap
done
'
1.5 Trip-planner clones (10 min)¶
ssh media-208 '
rm -rf ~/projects/trip-planner &&
cd ~/trip-planner &&
git status -s && # verificar nada uncommitted
git stash 2>/dev/null && # safety net
git pull
'
1.6 Backup files antiguos (5 min)¶
Cleanup one-time + setup retention en Fase 2:
ssh media-208 '
cd ~/stacks/reverse-proxy &&
ls -t Caddyfile.bak* 2>/dev/null | tail -n +4 | xargs -r rm &&
ls -t managed.caddy.bak-* 2>/dev/null | tail -n +4 | xargs -r rm
'
ssh media-208 'rm -f ~/stacks/docs/services-routing.md.bak.* ~/scripts/tunnel-sync.py.bak.*.pre-import-refactor'
1.7 Smoke + commit¶
ssh media-208 '
python3 ~/scripts/homelab-ctl.py smoke &&
cd ~/stacks &&
git status -s &&
git add -A &&
git commit -m "chore: cleanup orphan appdata, legacy scripts, old backups (ADR-0002 fase 1)" &&
git push
'
Fase 2 — Retention policies (30 min, riesgo bajo)¶
2.1 Cron weekly: cleanup .bak* (5 min)¶
Crear /etc/cron.weekly/homelab-backup-cleanup en VM 208:
#!/bin/bash
# Delete .bak* files older than 7 days across stacks + scripts
find /home/monxas/stacks -name "*.bak*" -type f -mtime +7 -delete
find /home/monxas/scripts -name "*.bak*" -type f -mtime +7 -delete
logger -t homelab-cleanup "deleted .bak files older than 7 days"
2.2 Cron weekly: docker prune (5 min)¶
/etc/cron.weekly/homelab-docker-cleanup:
#!/bin/bash
docker image prune -a --filter "until=168h" -f >/dev/null
docker volume prune --filter "label!=keep" -f >/dev/null
logger -t homelab-cleanup "docker prune (>7d)"
2.3 Política snapshots Proxmox (manual, doc)¶
Documentar en ~/stacks/docs/runbooks/snapshot-policy.md:
- Antes de migración:
qm snapshot 208 pre_<descriptor>_<YYYYMMDD>. - Después de validación >24h:
qm delsnapshot 208 pre_<descriptor>...manteniendo sólo el último known-good como rollback. - Máximo 3 snapshots vivos en VM 208.
- Auditoría mensual:
qm listsnapshot 208+ cleanup de los >30 días.
2.4 Audit mensual appdata huérfana (10 min)¶
Crear ~/scripts/audit-orphan-appdata.sh:
#!/bin/bash
# Lista directorios en ~/appdata sin container ni service en compose
running=$(docker ps -a --format '{{.Names}}')
declare -A active
for n in $running; do active["$n"]=1; done
for d in ~/appdata/*/; do
name=$(basename "$d")
# Heurística: nombre del dir matches algún container O compose service
if [ -z "${active[$name]:-}" ] && ! grep -qrln "container_name: $name\|^ $name:" ~/stacks/*/compose.yaml 2>/dev/null; then
size=$(du -sh "$d" | cut -f1)
echo "ORPHAN $size $name"
fi
done
Cron: 0 6 1 * * (1er día de mes 06:00) → notify via ntfy/telegram.
2.5 Logs locales → Loki (5 min)¶
Añadir job a promtail config en ~/stacks/infra/promtail/:
- job_name: homelab-scripts
static_configs:
- targets: [localhost]
labels:
job: homelab-scripts
host: media-208
__path__: /home/monxas/scripts/*.log
- job_name: homelab-ctl
static_configs:
- targets: [localhost]
labels:
job: homelab-ctl
host: media-208
__path__: /home/monxas/scripts/homelab-ctl.log
Reiniciar promtail. Validar en Grafana queries por {job="homelab-ctl"}.
Fase 3 — Tooling consolidation cross-host (3-4h, opt-in, planning required)¶
Objetivo: 1 source of truth git para todos los scripts custom de los 4 hosts (pmx, pmx2, LXC 253, VM 208).
Estructura propuesta:
/opt/homelab-scripts/ (git repo, deploy via cron)
├── hosts/
│ ├── pmx/
│ │ └── morning-report.py
│ ├── media-208/
│ │ ├── homelab-ctl.py
│ │ ├── tunnel-static.yaml
│ │ ├── nfs-health-monitor.sh
│ │ └── jellyfin-wake-drives.py
│ └── lxc-253/
│ ├── infra-monitor.sh
│ ├── openclaw-watchdog.sh
│ └── package-monitor.sh
├── lib/
│ ├── common.sh # logging, retention helpers
│ └── notify.sh # ntfy/telegram wrappers
├── docs/
│ └── README.md # qué corre dónde, cuándo, por qué
└── deploy.sh
Mecanismo de deploy:
- Cada host: cron
*/15 * * * * cd /opt/homelab-scripts && git pull -q. - Symlinks:
/usr/local/bin/morning-report.py→/opt/homelab-scripts/hosts/pmx/morning-report.py.
Validación:
- Tras setup, modificar 1 script (e.g.,
nfs-health-monitor.sh) y verificar que el cambio se propaga al host correcto via git pull. - Logs centralizados via promtail (Fase 2.5).
Cuándo NO hacerla:
- Si vas a tocar scripts <1 vez/mes.
- Si el overhead git (commit, push) supera el beneficio.
Cuándo SÍ hacerla:
- Si vas a refactor frecuente (como ADR-0001/0002).
- Si quieres logs centralizados.
- Si quieres rollback granular (git revert).
Decisión: marcar como "Optional, defer to ADR-0003 si la necesidad es real". No bloquear Fase 1+2 esperando esto.
Lo que NO se toca¶
homelab-ctl.py(671 LOC) — funciona bien, KISS.morning-report.py— bien escrito, filtros correctos, no duplicado.- N8N workflows (10 activos) — UI visual + retry logic + historial >>> shell scripts.
- Beszel (LXC 249) — bajo overhead, métricas útiles, dashboard nativo.
safe-docker.sh— funciona en bash (default shell del usuario).healthcheck-consolidation/(LXC 253) — proyecto cerrado, valor referencial.
Consecuencias¶
Positivas¶
- ~5 GB disco recuperados (Fase 1).
- Acumulación de
.bak*y dangling images detenida (Fase 2). - 7 snapshots Proxmox eliminados (claridad mental, ~20 GB ZFS).
- 1 trip-planner clone único, sincronizado.
- Logs scripts visibles en Grafana (gap cerrado).
- Documentación más limpia (runbook movido a su sitio).
Negativas¶
- Cron weekly añade 2 jobs nuevos (mantenimiento mínimo).
- Si appdata "huérfana" resulta no serlo, hay que restaurar (mitigado: validación pre-borrado + snapshot pre-fase1).
Riesgos y mitigaciones¶
| Riesgo | Probabilidad | Mitigación |
|---|---|---|
Borrar jellystat-backup rompiendo backup automatizado |
Baja | Validación con grep antes (paso 1.2) |
| Trip-planner pull conflict con cambios locales | Baja | git stash defensivo en 1.5 |
| Snapshot Proxmox antiguo borrado necesario para forensics | Muy baja | Mantener pre_sablier como punto fijo |
Cron weekly elimina .bak que aún era útil |
Baja | Retention 7 días = ventana razonable |
Plan de ejecución (timeline)¶
T+0:00 Snapshot pmx-50 "pre_adr0002_<YYYYMMDD>_<HHMM>"
T+0:05 Fase 1.1 — docker prune (3 min)
T+0:08 Fase 1.2 — appdata cleanup + valid jellystat (5 min)
T+0:13 Fase 1.3 — scripts legacy + doc move (5 min)
T+0:18 Fase 1.4 — snapshots cleanup (5 min)
T+0:23 Fase 1.5 — trip-planner consolidate (10 min)
T+0:33 Fase 1.6 — bak files cleanup (5 min)
T+0:38 Fase 1.7 — smoke + commit (3 min)
T+0:41 Snapshot "post_adr0002_phase1"
T+0:46 Fase 2.1 — cron .bak retention (5 min)
T+0:51 Fase 2.2 — cron docker prune (5 min)
T+0:56 Fase 2.3 — snapshot policy doc (5 min)
T+1:01 Fase 2.4 — audit-orphan-appdata.sh (10 min)
T+1:11 Fase 2.5 — promtail logs (5 min)
T+1:16 Snapshot "post_adr0002_phase2"
(Fase 3 deferred — re-evaluar en sprint próximo)
Total Fase 1+2: ~1h 16min con buffer.
Validation criteria (definition of done)¶
-
df -h /en VM 208 baja >5% -
docker images -f "dangling=true" -q | wc -l== 0 -
find ~/stacks ~/scripts -name "*.bak*" -type f | wc -l< 5 -
qm listsnapshot 208 | wc -l≤ 4 -
ls ~/projects/trip-planner 2>/dev/nullvacío (clone borrado) -
cd ~/trip-planner && git rev-parse HEAD== último commit GH -
python3 ~/scripts/homelab-ctl.py smoke→ 43/43 OK (sin regresión) - Cron weekly visible:
ls /etc/cron.weekly/homelab-* - Promtail ingesta scripts logs: en Grafana
count_over_time({job="homelab-ctl"}[1h]) > 0 - Doc movido:
ls ~/stacks/docs/runbooks/monitoring-operations-runbook.mdexiste - Memoria + ADR commit pushed a
monxas/homelab-stacks
Open questions¶
-
jellystat-backup270 MB: requiere confirmación. ¿Lo usa algún cron/script no descubierto? Comando para verificar incluido en 1.2. -
ski-monitoryweb-monitordirs en~/scripts/: contenido desconocido. Validar antes de borrar. -
Fase 3 (tooling consolidation): ¿es prioridad? Coste 3-4h con beneficio mediano. Defer si no hay necesidad concreta.
-
safe-docker.shen otros shells: si el usuario usa zsh alguna vez, añadir source a.zshrc. Por defecto: skip. -
/root/clawd/projects/healthcheck-consolidation/: proyecto cerrado Feb 22. ¿Mover aarchive/o dejar para referencia? Decisión trivial, deferida.
Notas de implementación¶
- Snapshot pre-fase1: nombrar con el formato actual
pre_<descriptor>_<YYYYMMDD>_<HHMM>para consistencia. - Commit messages: prefijo
chore:para limpieza,feat:si añade retention policy nueva. - Documentación post-implementation: actualizar
StatusaImplemented+ tabla de snapshots como en ADR-0001.
Próxima revisión: tras ejecución completa de Fase 1+2 o aborto.
Actualizar Status con resultado real.
Implementation outcome¶
Status: Implemented — 2026-04-29.
Snapshots Proxmox (rollback path)¶
| Phase | Pre | Post |
|---|---|---|
| Fase 1+2 | pre_adr0002_20260429_1432 | post_adr0002_20260429_ |
Métricas finales¶
- Disco VM 208: 67 → 65 percent (-5 GB tras docker prune + appdata + snapshots)
- Snapshots VM 208: 10 → 3 (pre_sablier, post_phase3, pre_adr0002, post_adr0002)
- Bak files en stacks/scripts: 49 → 0
- Scripts dir: 36 → 23 archivos (cleanup + web-monitor relocate)
- Docker images dangling: 12 → 0
- 43/43 smoke OK (sin regresion)
Acciones extra ejecutadas (no estaban en el plan original)¶
- web-monitor relocated: \ -> \ (era deuda estructural — source de container deployed viviendo en scripts dir)
- healthcheck-consolidation movido a LXC 253 - gastos-app_gastos_db_data docker volume eliminado
- promtail bind mount añadido para - 1 archivo doc duplicado eliminado (services-routing.md.bak)
Validation criteria — 10/10¶
- df -h baja (67% -> 65%)
- docker images dangling: 0
- .bak files: <5 (3 residuales bajo umbral 7d)
- qm listsnapshot 208: 3 snapshots (limit OK)
- ~/projects/trip-planner removido
- ~/trip-planner en HEAD 51d0aac (latest)
- homelab-ctl smoke: 43/43 OK
- cron weekly visible: /etc/cron.weekly/homelab-{backup,docker}-cleanup
- promtail ingesta scripts logs: query \ en Loki returns streams
- doc en runbooks/: monitoring-operations-runbook.md + snapshot-policy.md presentes
Pendientes deferidos¶
- Fase 3 (cross-host tooling consolidation): defer a ADR-0003 si la necesidad es real. Requiere 3-4h, beneficio mediano-largo plazo.
- Audit appdata mensual: cron \ activo, primera ejecución 1/mayo.