ADR-0027 — Sacar de VM 208 lo que tiene que sobrevivir¶
- Fecha: 2026-10-18.
- Versión: v2, tras la revisión por 5 agentes independientes:
- Alta disponibilidad y fallos.
- Plataforma Docker-en-LXC y NFS.
- Inventario y dependencias.
- Red team de la migración.
- Capacidad y cuelgues.
Todos verificaron en solo lectura contra los sistemas reales.
- Estado: propuesto (v2). Prerrequisito cumplido el 2026-10-18: C1 ya está en pmx-51 (ADR-0026 F4-F6).
- Responsable: Ramon (decisiones «decide Ramon»); ejecución técnica con Claude.
- Solicitud: «rompamos 208 en contenedores más pequeños. Impresióname. Primero un ADR» → «repasa y verifica este plan, usa agentes potentes para revisarlo desde diferentes ángulos, y busca cómo mejorarlo».
- Relacionado: ADR-0026 (roles, capas C0-C3, HA, comprobador). ADR-0007 (Caddy HA, homelab-ctl).
0. Qué cambió de v1 a v2, y por qué¶
La v1 desmontaba VM 208 entera en 11 LXC. La revisión encontró tres motivos para no hacerlo así:
- El NFS en un LXC es peor de lo que suponía. Cada snapshot de un CT (réplica cada 15 min, vzdump, preactualización) lo congela y hace
syncfstambién sobre el NFS (__snapshot_freeze→sync_container_namespace;nfsno está en%nosyncfs). - Con descargas en curso, cada réplica congela el CT entero mientras vacía al NAS.
- Con el NAS colgado, el CT se queda congelado, servicios sin NFS incluidos.
- Eso pasaría en pmx-51, el nodo crítico.
- Además, el bind necesita
shared=1o el HA no puede migrarlo. - Y 10 contenedores que corren como root (Immich, Stash, Audiobookshelf…) perderían el acceso al NAS por el mapeo de UID.
- Partir C2 en 4 CT no aísla nada. Vídeo, descargas, fotos e IA siguen en pmx-50, el mismo dominio de fallo. Concentran el ~70 % del trabajo duro (30 GB de
appdata, NFS con escrituras, AVX2, dependencias por nombre de contenedor) y no ganan HA. - No sirve como experimento de los cuelgues. Las oleadas de C2 dejan la misma carga en el mismo nodo. Además, el análisis de cuelgues reparte las hipótesis así:
- Avería de plataforma: ~60 %.
- Que un invitado como VM 208 provoque el fallo: ~25 %.
- El USB del dongle: refutado. El cuelgue de las 02:25 ocurrió sin USB.
v2: sacar a LXC solo lo que tiene que sobrevivir a pmx-50 (5 CT, sin NFS de escritura). VM 208 se queda, adelgazada, como «VM de cómputo» para C2: su propio kernel para el NFS con escrituras, cpu: host con AVX2 y sin HA, igual que ahora. Pasar C2 a LXC queda como decisión diferida y condicionada (§7).
El 80 % del valor con el 20 % del esfuerzo: lo que sobrevive a pmx-50 pasa de 0 a ~45 contenedores igual que en la v1, con 5 CT en lugar de 11 y 3 oleadas en lugar de 6.
1. Inventario verificado (2026-10-18)¶
1.1 Cifras globales¶
| Recurso | Valor verificado |
|---|---|
| Contenedores | 91 (85 en marcha) en 20 stacks; red compartida stacks_shared con 23 miembros |
| Memoria de los contenedores | En RAM 4,6 GB + 5,3 en swap interna = 9,9 GB de conjunto de trabajo. Los «7,1 GB» de la v1 solo veían la RAM |
| VM 208 desde fuera | 10,9 GB de RSS; balloon mínimo 11,5 GB; 5,9 GB de swap interna |
| CPU media de 7 días, todos los contenedores | 0,43 núcleos del Ryzen. Los «pesados» son picos |
| Imágenes | 88 en total, 56 GB únicos. 21 construidas en local, sin digest de registro, y 7 cuya etiqueta ya apunta a otra versión (compose up en otro anfitrión cambiaría la versión o fallaría) |
| Datos | ~/appdata 41 GB; fuera de appdata: ~/stacks/trip-planner 2,4 G, ~/runwisp 1,3 G, ~/alert-system 0,7 G, ~/audio-library 0,6 G, /opt/stacks 0,4 G, jev-gateway/data, leina-tts/voices, ~/sites, /opt/sonic-galaxy, /opt/pihole-exporter; volúmenes con nombre: openwa 852 MB, trmnl-db 217 MB, Redis de api-proxy y Redis de AudioMuse (anónimo) |
| Snapshots de la VM | 3 (08-08, 09-07 y 10-02): 88 GB |
| NFS | 192.168.0.XXX:/Volume1/Media → /mnt/nfs_media (v3, ya en soft,timeo=150,retrans=3). Carpetas de primer nivel drwxrwx--- 1000:1000; hay miles de ficheros de 0:0 (photos, pics, nopor, music) |
| Fuera de Docker | 22 servicios y 18 timers. Además, 18 tareas de RunWisp, la mayoría atadas a este anfitrión (docker exec, NFS, 127.0.0.1) |
sudo -n |
Funciona |
1.2 Hallazgos que la v1 no tenía¶
| # | Hallazgo | Consecuencia |
|---|---|---|
| H1 | VM 208 es el subnet router de Tailscale de 192.168.0.XXX/24 (PrimaryRoutes) |
Es una vía de acceso remoto que vive en C2. Va a la oleada 0 |
| H2 | HA → leina-tts (C2) sin reserva: el pipeline preferido usa tts.leina_tts y no hay paso automático a Piper |
Viola el cierre de dependencias. La flecha «con reserva» de la v1 no existía |
| H3 | Los jev-* llaman a localhost del servicio que juzgan: jev-paperless → :8000, stash-* → :9990, jev-media → Jellyfin y *arr, jev-fleet/evidencia/logwatch → Loki, argus/dependabot → pushgateway |
No pueden vivir todos en un CT de avisos |
| H4 | Dependencias por nombre de contenedor: plundrio ×3 → ntfy:80; onepacerr → jellyfin:8096; Seerr y Prowlarr → radarr/sonarr/lidarr |
Se rompen sin ruido al separar. No salen buscando .208 |
| H5 | Referencias a 208 repartidas: 9 en packages/jev*.yaml de HA; HA → ntfy, Jellyfin, leina-tts, Prometheus, Loki; la tablet → Navidrome; Vera → :8093, :8098, :8095, :8123 (AudioMuse), :2785, :8002, :8001, CUPS :631, :8073 y ssh media-208; Caddy → Sablier 208:10000 (12 bloques), el waker 208:8477, :9477, :8791, :8089, :3101; nodos y NAS → Loki y Healthchecks; 44 en homelab-maint/tasks.yaml; Ansible caddy_lxc saca el binario de Caddy de VM 208 |
Cada oleada debe cerrar las suyas |
| H6 | homelab-ctl en VM 208 conserva el token de CF, y su watch (RunWisp, cada 5 min) resucita cualquier contenedor etiquetado cuyo compose siga en el glob |
Riesgo de un servicio vivo en dos sitios y de un sync que borre rutas (§4.4) |
| H7 | Errores actuales: trmnl.monxas.casa lleva a Mylar3 (trmnl-app sin red y :8090 ocupado por mylar3); jev-seerr-dup falla porque Sablier duerme Seerr; alert-coordinator corre dos veces (sistema y usuario); en group_vars, pushgateway_url → :9091 (es plundrio; la real está en el :9092); la plantilla de remote-pulse empuja Loki a :3100 (es Jellystat; Loki está en el :3101) |
Arreglar en la oleada 0 |
| H8 | Music Assistant de VM 208 no lo usa HA: HA tiene su propio add-on. Lo usan el DJ de Vera y la tablet | Baja de importancia |
| H9 | f1-radio lleva faster-whisper/ctranslate2; paperless trae torch; AudioMuse usa onnxruntime (no TensorFlow) |
f1-radio → C2; Paperless, a probar en pmx-51 |
| H10 | pmx-51 tiene /dev/dri/renderD128 (QuickSync de Jasper Lake); pmx-50 no expone /dev/dri |
Un Jellyfin en pmx-51 sí podría transcodificar por hardware (§7) |
2. Principios (revisados)¶
- Sacar lo que tiene que sobrevivir; no partir por partir. Un CT nuevo solo si cambia de dominio de fallo o de política de HA.
- Cierre de dependencias: un servicio de C0/C1 no depende duro de C2. Se verifica con H2-H5.
- El observador fuera de lo observado: la monitorización vive en pmx-51.
- El plano de control fuera del plano de datos:
homelab-ctly Tailscale pasan a la consola (CT 212). - Ningún CT de HA-51 monta NFS con escritura. Si un servicio necesita escribir en el NAS, o se queda en la VM de cómputo o escribe en local y se copia al NAS con un job. Un NFS de solo lectura sí se admite:
ro, con lo quesyncfsno tiene nada que vaciar. - Mover sin actualizar. Durante el traslado, la imagen es el mismo digest que en VM 208 (
docker save | docker load). La actualización va al día siguiente, aparte. No es fijar versiones: es no mezclar dos cambios. - Ningún
syncpuede borrar rutas por accidente (§4.4).
3. Arquitectura objetivo¶
3.1 Los 5 CT nuevos, más la consola y la VM de cómputo¶
| CT | IP | Nombre | Contenido | RAM real (RAM+swap) | Datos | NFS | Política |
|---|---|---|---|---|---|---|---|
| 214 | .214 | vaultwarden |
OIDCWarden | 0,05 GB | 5 MB | — | HA-51 con failover, réplica cada 5 min. Separado a propósito del resto: el gestor de contraseñas no comparte CT con automatizaciones |
| 215 | .215 | avisos |
ntfy; jev-gateway con su núcleo (jev-dashboard, jev-f1, forward-grave, argus*, dependabot) y un solo alert-coordinator. Los workers que juzgan una app se quedan con esa app y llaman a avisos.lan:8093 (H3). RunWisp solo con las tareas no atadas a un anfitrión |
~0,4 GB | <1,5 GB | — | HA-51 con failover, 15 min |
| 216 | .216 | pegamento |
wake-on-demand, bazzite-power, homelab-mqtt-bridge, pihole-bridge, cups + avahi, asus-exporter. leina-tts, si funciona sin AVX (prueba en la oleada 0; resuelve H2) | ~0,3 GB | <0,5 GB | — | HA-51 con failover, 15 min |
| 217 | .217 | observa |
Prometheus, Loki, Grafana, Alertmanager, Healthchecks, Uptime Kuma, Blackbox, Pushgateway, pihole-exporter, la web de Scrutiny (los colectores ya están en los nodos) y systemd-journal-remote (§4.5) |
~2,0 GB | ~9,5 GB | — | HA-51 sin failover (estricto en pmx-51; §5.2) |
| 218 | .218 | personal |
Documentos: Paperless (+ Postgres, Redis), Stirling-PDF, Karakeep (+ Meilisearch y Chrome), personal-search. Web: trip-planner, rchat, okrs, docs-home, audio-library, cors-anywhere, api-proxy (+ Redis), scrapegraph-api, justeat ×2, openwa, TRMNL (+ MySQL y Redis, con el puerto arreglado), html2bmp, trmnl-watcher, isponsorblocktv, pairdrop, jobhunter, f1-recorder. Música: Navidrome, Audiobookshelf, Music Assistant, shairport-sync | ~3,0 GB | ~8 GB | solo ro, para música y audiolibros. Paperless con su media en local y copia nocturna al NAS |
HA-51 sin failover (§5.2) |
| 212 (existe) | .212 | maint + plano de control |
Consola + homelab-ctl central + Tailscale como subnet router de 192.168.0.XXX/24 (H1) |
+0,1 GB | — | — | HA-51 con failover (ya) |
| VM 208 | .208 | VM de cómputo (adelgazada) | Vídeo: Jellyfin, Jellystat, jellyfin-share, Tunarr, jellyfin-wake. Descargas: *arr, Seerr, plundrio, slskd, MeTube, Pinchflat, twitch-rec, arr-bridge, arr-salud. Fotos: Immich. IA: AudioMuse, Stash, nopor-ingest, f1-radio, y leina-tts si necesita AVX. Workers jev-media/stash-*/seerr-dup; tareas de RunWisp locales (putio/downloads, asmr, audiomuse_dedup, pg_dumps de sus bases de datos) |
~5,1 GB (picos ~11) | ~30 GB | ✅ con escritura, en su propio kernel | Solo-50 (sin HA), como hoy |
Se retiran: cAdvisor y promtail de la VM (los sustituye §4.5, después de reescribir las 2 reglas que usan cAdvisor: ContainerDown y media_container_missing), periphery, code-server (los dos), deploy-hook (parado desde hace 3 semanas), el segundo alert-coordinator y sonic-galaxy/casa-demo si R1 lo confirma.
3.2 Dependencias (después)¶
flowchart LR
subgraph P51["pmx-51 · HA"]
MT[212 maint · homelab-ctl · Tailscale]
VW[214 vaultwarden]
AV[215 avisos: ntfy, Jev]
PG[216 pegamento: wake, MQTT, cups, leina?]
OB[217 observa]
PE[218 personal: docs, web, música]
HA[Home Assistant]
VERA[Vera]
end
subgraph P50["pmx-50 · cómputo"]
VM[VM 208 cómputo: vídeo, descargas, fotos, IA]
end
HA --> AV
HA --> PG
VERA --> AV
VERA --> PE
VERA -.degradable.-> VM
HA -.solo si leina necesita AVX.-> VM
VM --> AV
PE -.ro.-> NAS[(NAS)]
VM --> NAS
OB -.mide.-> P51 & P50
MT -.rutas.-> CAD[Caddy 270/271]
4. Decisiones técnicas¶
4.1 Docker dentro de LXC sin privilegios (verificado)¶
- Ya funciona aquí: CT 281 y 251 tienen
overlay2sobre ZFS 2.4.4, cgroups v2 y Docker 29.8. - Plantilla: Debian 13,
unprivileged=1,nesting=1,keyctl=1. daemon.json:
journald.conf.d:RateLimitBurst=0,SystemMaxUse=1G.- Riesgo conocido: actualizaciones de runc/containerd o AppArmor ya rompieron Docker en LXC en 2025. Snapshot antes de cada
apt upgradey nunca actualizar Docker en todos los CT a la vez (la regla C3 de ADR-0026 ya escalona).
4.2 NFS: solo de lectura, y solo donde hace falta¶
- En los nodos: montaje normal (sin automontaje, para no dejar binds apuntando al montaje viejo) en
/mnt/nas/mediaconro,soft,timeo=150,retrans=3,noatime,nofail,_netdev,x-systemd.mount-timeout=60. Un hookscriptpre-startcompruebamountpoint -q. - En el CT 218:
mp0: /mnt/nas/media,mp=/mnt/nfs_media,ro=1,shared=1
lxc.idmap: u 0 100000 1000
lxc.idmap: g 0 100000 1000
lxc.idmap: u 1000 1000 1
lxc.idmap: g 1000 1000 1
lxc.idmap: u 1001 101001 64535
lxc.idmap: g 1001 101001 64535
- En
/etc/subuidy/etc/subgidde los dos nodos:root:1000:1. - Propietarios: Navidrome y Audiobookshelf con
user: "1000:1000". Antes,chown -R 1000:1000de sus carpetas en el NAS (hoy hay ficheros de0:0). - Prueba obligatoria en la oleada 0:
pct migratereal del CT con el bind (shared=1) y vzdump con restauración.
4.3 Sablier¶
- Fuera de la VM: solo en
personal, para Karakeep (con Chrome y Meilisearch) y Stirling-PDF. - En la VM: sigue el suyo, solo para C2.
- Rutas:
homelab-ctlapunta cada bloquesablierde Caddy al Sablier del anfitrión del servicio. - Lo demás: siempre encendido.
jev-seerr-dup(que falla porque Seerr duerme): despierta a Seerr antes de consultarlo o deja de depender de él (H7).
4.4 homelab-ctl: un solo controlador, seguro¶
Problemas de hoy:
- Hay copias en VM 208 con el token de CF.
watchresucita contenedores.collect_desired()se queda con el primer contenedor por hostname sin avisar.- Un
synccon un anfitrión que no responde borraría sus rutas.
Diseño:
- Controlador central en 212 (HA):
synclee las etiquetas de cada anfitrión pordocker -H ssh://root@<ip>. Guarda la última lista conocida de cada uno (/var/lib/homelab-ctl/<host>.json): si uno no responde, sus rutas se conservan con la lista vieja. - Barandillas, en las que
syncse niega a desplegar: - Un hostname aparece en dos anfitriones.
- El número de rutas baja más de un 10 %.
- Un anfitrión desconocido aparece o desaparece.
Se puede forzar con --force-shrink, nunca desde Vera.
3. watch y update, en local en cada anfitrión (timer del rol), solo sobre sus stacks. Así no se descifran los secretos de todos en un único sitio.
4. En la oleada 0:
- Quitar CF_API_TOKEN de ~/.env de VM 208.
- Sustituir su homelab-ctl por un stub que solo informa (como tunnel-sync.sh en ADR-0026).
- Apuntar el homelab-ctl de RunWisp de la VM al watch local.
4.5 Logs que siguen al CT¶
- Por qué no
promtail_lxc_jobspor nodo: cuando el HA mueve un CT, sus logs se pierden. Y si se pone en los dos nodos, el pasivo leería la réplica montada. - Envío: cada CT envía su journal con
systemd-journal-uploada unsystemd-journal-remoteenobserva. Un único promtail enobservalo lee y lo manda a Loki. - Sin agentes extra: nada de cAdvisor ni promtail por CT.
- Métricas por CT:
prometheus-pve-exporterenobserva.
4.6 Nombres y referencias¶
- DNS
.lanen los dos Pi-hole (Ansible):avisos.lan,pegamento.lan,observa.lan,personal.lan,vault.lan,computo.lan(= VM 208). - Antes de separar cualquier pareja, sustituir los nombres de contenedor que cruzan grupos (H4) por nombres
.lan: plundrio →avisos.lan:8082, etc. - Al cerrar cada oleada: se reapuntan sus referencias de H5 y un
grepde192\.168\.0\.208y de nombres de contenedor en HA, Vera, Caddy, Ansible ytasks.yamldebe salir vacío para los servicios movidos.
4.7 Declarativo y reproducible¶
ansible/lxc_services.yml+ roldocker_lxc, como en la v1: crear CT, Docker, NFSroe idmap, DNS, alta en HA, réplica y backup,startup.- Repo
homelab-stacks(SOPS) con un compose por CT. Los stacks mixtos (tools,media) se parten en ficheros por grupo antes de mover nada. - Imágenes:
- las 21 locales y las 7 desfasadas se exportan en la oleada 0 (
docker saveal NAS); - su
Dockerfileo su origen va al repo; - un
registry:2con caché de Docker Hub enobservaevita los límites de descarga. - Backups por CT con base de datos: un timer de
pg_dump/mysqldump(sustituye la tareapg_dumpsde RunWisp). El simulacro de restauración demaintpasa a cada CT. - Consola
maint: los modos L1-L4 dejan de llevar listas fijas de VM 208. Usan la etiqueta Dockerhomelab.writer=trueen todos los anfitriones.
5. Capacidad (números corregidos: RAM + swap)¶
5.1 Uso normal¶
| pmx-51 (31,3 GB) | pmx-50 (28,5 GB; 31,5 con la iGPU a 512 MB) | |
|---|---|---|
| Hoy (tras ADR-0026 F4-F6) | C0+C1+C3 ≈ 13,1 GB (pico 14,7) | ~16,4 GB de invitados + ~3,5 de sistema (slab, zram, ARC) |
| ADR-0027 v2 | +214-218 ≈ 5,8 GB | VM 208 adelgazada: 8 GB asignados, balloon mínimo 6 (hoy 12/11,5) → −4 a −5 GB |
| Libre | ~11 GB | ~12 GB (~15 con la iGPU) |
| CPU | ~1,9 de media y ~3 de p95 sobre 4 núcleos | Baja |
Disco zfs-ha |
138 GB libres hoy (las réplicas de C1 ya están dentro) − ~25 GB de CT nuevos → ~110 GB libres. No hace falta SSD nuevo | Libera los 88 GB de snapshots de la VM tras la oleada 3 |
5.2 N-1: el conjunto de failover es explícito¶
El HA de Proxmox no tiene control de admisión: si lo que se recupera no cabe, el nodo receptor entra en swap, que es lo que pasó hoy en pmx-50. Por eso se separan dos subconjuntos:
| Conjunto | Contenido | RAM real | Si cae pmx-51 |
|---|---|---|---|
| Con failover | C0 (túnel, WireGuard, PocketID, maint, vaultwarden) + HA + Vera + ha-ml + tv-gw + avisos + pegamento | ~13,6 GB | Se recupera en pmx-50 |
Estricto en pmx-51 (node-affinity estricta solo a proxmox2) |
observa, personal |
~5 GB | Espera a que vuelva pmx-51 |
¿Cabe el conjunto con failover en pmx-50?
| Situación | Libre en pmx-50 | Resultado |
|---|---|---|
| Hoy | 7,8 GB | ❌ (normalidad ya lo marca) |
| Con la VM adelgazada | ~12 GB | ⚠️ |
| ~~Con la VM adelgazada y la iGPU a 512 MB~~ | — | Descartado (2026-10-18): con UMA_SPECIFIED + 512M el UM773 Lite (BIOS 1.15, la última) no arranca y la BIOS vuelve sola a los valores anteriores. La iGPU se queda en ~2,5 GiB |
| Con 64 GB en pmx-50 (opción R6) | — | ✅ holgado |
Regla permanente: normalidad mide el N-1 con RAM + swap (ya implementado el 2026-10-18) y avisa si el conjunto con failover no cabe con 2 GB de margen.
Si cae pmx-50, no se mueve nada: C2 espera.
6. Plan por oleadas¶
6.1 Procedimiento por servicio (idempotente y reanudable)¶
Se lanza desde 212 en tmux. Un fichero de estado /var/lib/migra/<svc>.estado permite retomar en cualquier paso si pmx-50 se cuelga a mitad.
- PREPARAR (reversible).
- Ansible crea o actualiza el CT.
docker save | docker loaddel mismo digest de VM 208.- Copia en caliente:
rsync -aH --numeric-ids(sin-X) con el receptor dentro del CT (ssh root@CT), nunca escribiendo desde el nodo en el subvolumen. Incluyeappdata, los directorios fuera deappdatay los volúmenes con nombre. - PROTEGER (reversible, antes del
down). - En VM 208, quitar el servicio del compose activo: si el stack es de un solo servicio, renombrar su
compose.yaml; si es mixto, moverlo acompose.migrado.yaml, que queda fuera del glob. - Después,
up -d --remove-orphansdel stack de origen. - Así
watchno lo resucita y un cuelgue deja el servicio apagado, no duplicado. - CORTE.
stop -t 60.rsyncen frío de la diferencia.pg_dump/mysqldumpde su base de datos y restauración de prueba en un contenedor temporal, contando filas.- Sumas de control y
COPIA-OK. - ARRANCAR en el CT, solo si existe
COPIA-OK. Esperar ahealthy. - RUTA.
homelab-ctl sync, con detección de duplicados. Pruebas de humo de solo lectura. ⚠ Punto de no retorno blando: a partir de la primera escritura de un usuario, volver atrás exige una copia inversa CT → VM (Paperless mueve ficheros deconsume/; Vaultwarden y TRMNL guardan entradas nuevas). - CERRAR.
- Alta en HA, réplica y backup.
- Prueba de
pct migratesi el CT tiene bind. - Referencias de H5.
normalidad.- Estado
HECHO. - Al día siguiente:
updatede imágenes en el CT, aparte del traslado.
Vuelta atrás:
- Antes del paso 5: quitar la protección en VM 208,
up -dallí ysync. - Después del paso 5: parar el CT, copia en frío inversa CT → VM con el digest viejo, quitar la protección y
sync.
6.2 Oleadas¶
| Oleada | Contenido | Corte |
|---|---|---|
| 0 · Plataforma y seguridad | (a) Tailscale subnet router en 212, aprobado en la consola de Tailscale antes de tocar el de VM 208 (H1). (b) homelab-ctl central con barandillas; stub en la VM; token de CF fuera (§4.4). (c) Segundo conector cloudflared del mismo túnel en pmx-50 (CT pequeño, sin HA, activo-activo): el acceso externo deja de tener los 2-3 min de recuperación del CT 123. (d) DNS .lan; nombres de contenedor entre grupos → .lan (H4). (e) Exportar las 28 imágenes (§4.7). (f) Rol docker_lxc, journal-remote, montaje ro + idmap + shared=1, probado con un CT de ensayo y un pct migrate real. (g) Arreglos de H7 (TRMNL, alert-coordinator doble, URLs de group_vars, jev-seerr-dup). (h) Binario de Caddy al repo. (i) leina-tts en un CT de prueba en pmx-51: decide si va a 216 o se queda en la VM; si se queda, crear en HA el pipeline de reserva con Piper y la automatización que lo activa (H2). (j) tasks.yaml con la etiqueta homelab.writer |
0 |
| 1 · Lo que no puede caer | 214 vaultwarden, 215 avisos, 216 pegamento; reapuntar HA (ntfy, jev*.yaml), Vera (buzón, alert-bridge, CUPS) y Caddy (waker) |
Segundos-minutos por servicio |
| 2 · Observabilidad | 217 observa: copia en caliente y luego diferencia de 9,5 GB, para que el corte sea de segundos | Segundos |
| 3 · Personal | 218 personal (documentos, web y música en ro); reapuntar la tablet y el DJ de Vera; adelgazar VM 208 (8 GB, balloon mínimo 6) |
Minutos por servicio |
| 4 · Cierre | Backup de la VM en PBS → borrar los 3 snapshots (88 GB) → simulacros (§8). Con pmx-50 sin cuelgues: valorar §7 | 0 |
7. Diferido y condicionado: C2 a LXC y HA-50¶
Se reabre solo si pmx-50 lleva ≥14 días sin cuelgues o su causa está identificada y corregida, y en ese momento:
- Con la VM ya delgada, pasar C2 a LXC solo aporta HA. Haría falta resolver el freeze con
syncfs(§0.1), elshared=1y el UID 0 (Immich, Stash). - Alternativa atractiva: Jellyfin en un CT de pmx-51 con QuickSync (
/dev/dri/renderD128, H10) transcodifica por hardware mejor que hoy en pmx-50, donde se hace por software.
La caza de los cuelgues va aparte. No es el objetivo de este ADR; propuesta de ADR-0028 con los experimentos del análisis:
- Esperar ≥48 h a ver si la racha actual se mantiene.
- Si vuelven: una noche de ≥12 h con VM 208 parada.
- Después, de uno en uno y 24 h cada uno:
- Wi-Fi MT7921 deshabilitado;
- APST del NVMe desactivado (
nvme_core.default_ps_max_latency_us=0); - BIOS 1.15 → la última, con «Typical Current Idle»;
- otra fuente de alimentación;
- limpiar el equipo y recolocar la RAM (las DIMM llegaron a 89 °C).
8. Riesgos (revisados)¶
| Riesgo | Mitigación |
|---|---|
| Un servicio en dos sitios o rutas borradas | Paso 2 PROTEGER antes del down; barandillas de sync; stub en la VM sin token (§4.4) |
| Escrituras perdidas al volver atrás | Copia inversa obligatoria después del paso 5; anotado en cada servicio |
syncfs en NFS al hacer snapshot |
Ningún CT de HA-51 monta NFS con escritura (principio 5) |
Dependencias ocultas por nombre o por localhost |
H3, H4 y H5 resueltos en las oleadas 0-1; grep al cierre de cada una |
| Imágenes imposibles de volver a descargar | Exportación y repo en la oleada 0 |
| Docker en LXC tras actualizar | Snapshot previo y actualizaciones escalonadas |
| N-1 que no cabe | Conjunto de failover explícito, VM adelgazada, iGPU de pmx-50 y vigilancia continua en normalidad |
| CPU de pmx-51 en un pico | cpuunits (ADR-0026 §A5.3): observa/personal 100, por debajo de C0/C1 |
| Vera tocando lo nuevo | Sin SSH root a los CT nuevos: tareas con nombre vía maint o jev-gateway |
Simulacros de cierre:
node-maintenancede pmx-51: el conjunto con failover pasa a pmx-50,observa/personalesperan, y medir los cortes.- Cuelgue natural de pmx-50: el acceso externo no se corta, gracias al segundo
cloudflared. - NAS parado 10 min:
personalsigue (solo pierde música y audiolibros) y no se congela ningún CT.
9. Decisiones de Ramon¶
| # | Decisión | Recomendación |
|---|---|---|
| R1 | ¿Sobra algo de la parte web de personal? (sonic-galaxy, casa-demo, okrs) |
Retirar lo que no se use |
| R2 | Sablier solo para Karakeep y Stirling fuera de la VM | Sí |
| R3 | Repo privado homelab-stacks (SOPS) |
Sí |
| R4 | Borrar los 3 snapshots de VM 208 (88 GB) en la oleada 4 | Sí, tras el backup |
| R5 | Segundo cloudflared en pmx-50 |
Sí (barato, quita el corte de acceso en cada cuelgue de pmx-51) |
| R6 | pmx-50 a 64 GB (2×32 DDR5 SO-DIMM, ~150-200 €) | Muy recomendado: con la iGPU descartada, la VM adelgazada sola deja el N-1 justo (~12 GB libres para ~13,6 GB). Con 64 GB sobra |
10. Alternativas descartadas¶
| Alternativa | Por qué no |
|---|---|
| v1: 11 CT, C2 incluido | §0: NFS en el kernel del nodo con syncfs en cada réplica, UID 0, sin ganar HA, y no sirve como experimento |
| VM 208 entera en HA | CPU host + AVX2, 93-181 GB, y la migración en vivo AMD↔Intel reinicia el invitado (verificado con HA el 2026-10-18) |
| Un CT por contenedor o contenedores OCI de PVE | Demasiadas piezas; se pierde compose |
| Kubernetes, k3s o Nomad | Dos nodos, uno de ellos un Celeron: no compensa |
11. Ejecución (2026-10-18/19)¶
Ejecutado con 4 agentes en paralelo, con cerrojos para lo compartido: rutas en VM 208 (~/.adr0027-rutas.lock), stacks (~/.adr0027-stacks.lock) y HA/réplicas en pmx-51 (/run/adr0027-pve.lock). DNS con lan-dns NOMBRE IP en pmx-51, que cambia los dos Pi-hole en caliente por dns.hosts. Cada corte fue un único script en el host con vuelta atrás automática (10-15 min), para que la caída de un agente (pasó 4 veces por el límite de uso) no dejase un servicio apagado. La vuelta atrás se activó sola 5 veces y en todas funcionó.
| CT | Movido | HA | Corte |
|---|---|---|---|
| 214 vaultwarden | Vaultwarden (757 entradas verificadas) | failover, réplica cada 5 min | 35 s |
| 215 avisos | ntfy, jev-gateway + córtex, jev-f1, forward-grave, argus, dependabot | failover, 15 min | segundos |
| 216 pegamento | waker :8477, bazzite-power, homelab-mqtt-bridge, asus-exporter, CUPS + avahi | failover, 15 min | segundos |
| 217 observa | Prometheus, Loki, Grafana, Alertmanager, Pushgateway, Healthchecks, Kuma, Blackbox, pihole-exporter, Scrutiny | ha-solo-51 (estricto) |
110 s |
| 218 personal | Música (Navidrome, Audiobookshelf, Music Assistant, shairport) y web (audio-library, docs-home, okrs, cors-anywhere, pairdrop, api-proxy, rchat, justeat ×2, scrapegraph) | ha-solo-51 (estricto) |
segundos |
Se quedan en VM 208, y por qué:
- Paperless: consume del NAS en uso real, necesita NFS con escritura (principio 5).
- Stirling y Karakeep: necesitan Sablier por anfitrión en homelab-ctl (hoy SABLIER_URL es fijo).
- trip-planner (despliegue overlay), openwa, isponsorblocktv, f1-recorder, TRMNL (roto), job-hunter (systemd), sonic-galaxy/casa-demo (R1).
- leina-tts, pihole-bridge y el RunWisp de avisos: pospuestos.
- La oleada 0 completa (Tailscale en 212, homelab-ctl central con barandillas, segundo cloudflared) no se hizo: las rutas se movieron con tunnel-static.yaml + sync en la VM, como PocketID.
Correcciones al diseño:
- §4.2: el root del CT no puede atravesar la raíz del NAS (770, 1000:1000). Hace falta un bind ro por carpeta (music, podcasts, audiobooks), con shared=1,replicate=0.
- §4.6: el DNS interno de Docker en VM 208 no resuelve .lan. Los contenedores que lo necesitan llevan dns: de los Pi-hole.
- Los nodos y Vera preguntaban primero al router (no conoce .lan). Ahora usan Pi-hole primero (pvesh set /nodes/*/dns, pct set 100 --nameserver).
Capacidad tras la ejecución: VM 208 bajó de 84 a 54 contenedores y de ~11 a ~6,2 GB usados. Su balloon mínimo es 7 GB (sigue con 12 asignados; reducir memory exige reiniciar la VM). normalidad ahora calcula el N-1 sin lo estricto y sumando lo cedible por balloon: OK (12,3 GB que se moverían frente a 11,6 libres + 5 cedibles).
Pendiente:
- Retirar los reenviadores de transición en VM 208 (adr0027-reenvio@*, observa-reenvio, tailnet-reenvio) cuando ss no muestre clientes. Quedan: la integración ntfy de HA (.storage, se reconfigura desde la UI), el workflow n8n beszel-alert-forwarder (208:8082), git-autocommit.sh de Vera (208:8073), tv-gw (promtail), API_PROXY_URL de trip-planner (208:3102).
- update de imágenes en los CT nuevos, aparte (§6.1 paso 7).
- Codificar en Ansible: rol docker_lxc, lan-dns, tcp-reenvio.py, /opt/observa.
- Bajar memory de VM 208 a 8 GB en su próximo reinicio planificado.
11.1 Oleada 0 (2026-10-19)¶
- (b)
homelab-ctlcentral en CT 212:/usr/local/bin/homelab-ctl, token desde SOPS a/etc/homelab-ctl/env(600), rutas estáticas en/etc/homelab-ctl/tunnel-static.yaml, última lista conocida por anfitrión en/var/lib/homelab-ctl/. Lee etiquetas de VM 208 y CT 214-218 con una clave dedicada de comando forzado (homelab-ctl-remote). Barandillas probadas: duplicados, caída >10 % (--force-shrink), anfitriones nuevos/perdidos (--accept-hosts), nunca forzables desde Vera. Modo sombra: render idéntico (317 líneas) antes del corte; smoke 49/49. En VM 208 queda solowatch/updatelocal y un stub que remite al 212; el token de CF se retiró de 6 ficheros. Código enservices/homelab-ctl/; botones enmaint(pack «7 · Rutas»). - (c) Segundo
cloudflared: CT 219cloudflared2(.219) en pmx-50, sin HA a propósito, mismo token de túnel. Túnel con 2 conectores (8 conexiones); probado parando el de CT 123: sin corte. - (a) Tailscale en CT 212: instalado (TUN en el CT, reenvío IP), pendiente de que Ramon autorice el equipo y apruebe
192.168.0.XXX/24. Se mantiene también el subnet router de VM 208: Tailscale hace relevo automático entre los dos. - H2 (voz de HA): pipeline «openAI (voz local)» con Piper
es_ES-leina-mediumypackages/voz_reserva.yaml: si leina-tts falla 90 s, los 12 satélites cambian; vuelven tras 5 min sanos. Probado. Commit1fa559eenhome-assistant-config.
11.2 Cierre y repaso (2026-10-19 tarde)¶
- Tailscale:
maint-212autorizado; el 212 es el subnet router principal y la VM 208 queda de reserva. Incidente: la VM 208 tenía--accept-routesy, al pasar a secundaria, enrutó192.168.0.XXX/24portailscale0y quedó inalcanzable desde la LAN unos 75 min. Arreglo:tailscale set --accept-routes=falseen la VM. Regla: ningún equipo de la LAN conaccept-routes, salvo portátiles que salen de casa. - Imágenes actualizadas (§6.1 paso 7): 214, 215 (
pull), 217 (10 imágenes de vuelta a su origen) y 218 (9). Las construidas en local no tienen registro y se quedan. Audiobookshelf pasa de2.36.0@sha256alatest; Uptime Kuma:2y Redis:7-alpinerespetan el mayor declarado. Snapshots previos:pre_update_20261019(214/215/217 porpct, 218 por ZFS). Borrarlos tras unos días. - Loki 3.7.8 pedía
common.compactor_grpc_address: 127.0.0.1:9096, añadido. - Trampa:
sed -isobre un fichero montado suelto en Docker crea otro inodo y el contenedor sigue viendo el viejo (Prometheus no aplicó un cambio). Editar en el mismo inodo o reiniciar el contenedor. - Duplicados resucitados: Vera «actualizó» el ntfy viejo de VM 208. Un
profiles:no protege (compose up <svc>lo activa): hay que quitar el bloque del compose. Vera tiene la tabla de mudanzas endocs/service-catalog.mdy una línea enAGENTS.md. service_routesde RunWisp en VM 208 retirada; sustituta:rutas-smoke-diario.timeren el 212 → pushgateway →RutasSmokeFallos/RutasSmokeMudo.contenedores_caidos.shya no avisa de los lazy (tunnel.lazy=trueen el propio contenedor) recreados sin arrancar.jev-seerr-dupse salta la pasada si Seerr duerme.- Reenviadores de transición: registran
USO <puerto> <cliente>(una vez por hora);~/scripts/reenvios-usoen VM 208 lista los que nadie usa. Retirada prevista hacia el 2026-10-26. Pendiente conocido: los sensoresplatform: restdepihole_metrics.yamlen HA ya apuntan aobserva.lan, pero no cambian hasta el próximo reinicio del núcleo de HA.
11.3 Jellyfin y cierre de capacidad (2026-10-19 noche)¶
Jellyfin trasladado a CT220 (.220), con HA ha-group-ha-network, preferencia por pmx-51, GPU opcional y réplica de 15 min. Se probó primero el traslado manual y después la migración mediante HA a pmx-50 y el retorno automático a pmx-51 (failback=1 restaurado). HLS real en ambos: CPU en pmx-50 y VAAPI para decodificar en pmx-51. Codificación por hardware pendiente de HuC. Identidad y biblioteca conservadas, SQLite quick_check=ok, acceso público/VIP 200 y acceso privado sin credenciales 401.
El corte incluye la retirada del bloque de VM208, contenedor antiguo parado con restart=no, reenviador de transición 8096 y ruta desde CT212. Seerr/Jellystat/Share/onepacerr apuntan a jellyfin.lan:8096; se corrigió también la URL de Jellystat guardada en PostgreSQL. El catálogo de Vera impide resucitar la copia antigua.
VM208 ya tiene 8.192 MiB aplicados, balloon 6.144 y onboot=1. CT110 y CT172 se añaden a ha-solo-51 junto a CT217/218. ARC de pmx-50 pasa a 1,5 GiB, persistido en modprobe/initramfs. N-1 pasa con Jellyfin cargado. CT220 está incluido en el backup diario PBS. Sus primeras copias y las de CT219 terminaron correctamente esta noche (CT220: 2 min 25 s); no se ensayó restauración.
Se corrigieron el retorno 2 válido del dry-run, la comprobación de errores rsync, la parada explícita antes de Docker y los reintentos de arranque. Una corrupción bbolt de containerd detectada al volver obligó a reconstruir el runtime, sin sustituir datos de Jellyfin. La segunda validación completa con HA pasó. Su causa no está demostrada; pmx-50 reinició dos veces al principio y sigue pendiente el diagnóstico. Código, pruebas y recuperación.
11.4 Auditoría posterior: N-1 condicional y protección de originales¶
El OK de §11.3 depende de recuperar 2 GiB teóricos de VM208 y deja unos594MiB; sin balloon faltan1.454MiB. VM208 tenía1.795MiB disponibles y3.433MiB de swap; el consumo actual no cubre los máximos/arranque de los invitados. CT110/172/217/218 siguen excluidos y no recuperan en50 si cae51. Debe mostrarse como capacidad condicional, no garantía holgada de recuperación.
La caché NAS ya era writethrough antes de la corrupción. No está acreditada su causalidad; conservar y medir. El scrub del traspaso queda descartado por los bloqueos documentados. Las copias de invitados no incluyen Media NFS: no se acreditó copia independiente actual de photos/pics. El rescate de documentos/proyectos es manual, sin actualización periódica demostrada. Intel no codifica por GPU todavía, y la Radeon de50 está desactivada por blacklist. Informe con evidencia y propuestas. No se cambiaron configuraciones en esta auditoría.