Skip to content

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í:

  1. 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 syncfs también sobre el NFS (__snapshot_freeze → sync_container_namespace; nfs no está en %nosyncfs).
  2. Con descargas en curso, cada réplica congela el CT entero mientras vacía al NAS.
  3. Con el NAS colgado, el CT se queda congelado, servicios sin NFS incluidos.
  4. Eso pasaría en pmx-51, el nodo crítico.
  5. Además, el bind necesita shared=1 o el HA no puede migrarlo.
  6. Y 10 contenedores que corren como root (Immich, Stash, Audiobookshelf…) perderían el acceso al NAS por el mapeo de UID.
  7. 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.
  8. 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í:
  9. Avería de plataforma: ~60 %.
  10. Que un invitado como VM 208 provoque el fallo: ~25 %.
  11. 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)

  1. 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.
  2. Cierre de dependencias: un servicio de C0/C1 no depende duro de C2. Se verifica con H2-H5.
  3. El observador fuera de lo observado: la monitorización vive en pmx-51.
  4. El plano de control fuera del plano de datos: homelab-ctl y Tailscale pasan a la consola (CT 212).
  5. 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 que syncfs no tiene nada que vaciar.
  6. 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.
  7. Ningún sync puede 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 overlay2 sobre ZFS 2.4.4, cgroups v2 y Docker 29.8.
  • Plantilla: Debian 13, unprivileged=1, nesting=1,keyctl=1.
  • daemon.json:
{"live-restore": true, "log-driver": "journald", "log-opts": {"tag": "{{.Name}}"}}
  • 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 upgrade y 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/media con ro,soft,timeo=150,retrans=3,noatime,nofail,_netdev,x-systemd.mount-timeout=60. Un hookscript pre-start comprueba mountpoint -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/subuid y /etc/subgid de los dos nodos: root:1000:1.
  • Propietarios: Navidrome y Audiobookshelf con user: "1000:1000". Antes, chown -R 1000:1000 de sus carpetas en el NAS (hoy hay ficheros de 0:0).
  • Prueba obligatoria en la oleada 0: pct migrate real 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-ctl apunta cada bloque sablier de 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.
  • watch resucita contenedores.
  • collect_desired() se queda con el primer contenedor por hostname sin avisar.
  • Un sync con un anfitrión que no responde borraría sus rutas.

Diseño:

  1. Controlador central en 212 (HA): sync lee las etiquetas de cada anfitrión por docker -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.
  2. Barandillas, en las que sync se niega a desplegar:
  3. Un hostname aparece en dos anfitriones.
  4. El número de rutas baja más de un 10 %.
  5. 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_jobs por 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-upload a un systemd-journal-remote en observa. Un único promtail en observa lo lee y lo manda a Loki.
  • Sin agentes extra: nada de cAdvisor ni promtail por CT.
  • Métricas por CT: prometheus-pve-exporter en observa.

4.6 Nombres y referencias

  • DNS .lan en 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 grep de 192\.168\.0\.208 y de nombres de contenedor en HA, Vera, Caddy, Ansible y tasks.yaml debe salir vacío para los servicios movidos.

4.7 Declarativo y reproducible

  • ansible/lxc_services.yml + rol docker_lxc, como en la v1: crear CT, Docker, NFS ro e 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 save al NAS);
  • su Dockerfile o su origen va al repo;
  • un registry:2 con caché de Docker Hub en observa evita los límites de descarga.
  • Backups por CT con base de datos: un timer de pg_dump/mysqldump (sustituye la tarea pg_dumps de RunWisp). El simulacro de restauración de maint pasa a cada CT.
  • Consola maint: los modos L1-L4 dejan de llevar listas fijas de VM 208. Usan la etiqueta Docker homelab.writer=true en 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.

  1. PREPARAR (reversible).
  2. Ansible crea o actualiza el CT.
  3. docker save | docker load del mismo digest de VM 208.
  4. 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. Incluye appdata, los directorios fuera de appdata y los volúmenes con nombre.
  5. PROTEGER (reversible, antes del down).
  6. 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 a compose.migrado.yaml, que queda fuera del glob.
  7. Después, up -d --remove-orphans del stack de origen.
  8. Así watch no lo resucita y un cuelgue deja el servicio apagado, no duplicado.
  9. CORTE.
  10. stop -t 60.
  11. rsync en frío de la diferencia.
  12. pg_dump/mysqldump de su base de datos y restauración de prueba en un contenedor temporal, contando filas.
  13. Sumas de control y COPIA-OK.
  14. ARRANCAR en el CT, solo si existe COPIA-OK. Esperar a healthy.
  15. 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 de consume/; Vaultwarden y TRMNL guardan entradas nuevas).
  16. CERRAR.
  17. Alta en HA, réplica y backup.
  18. Prueba de pct migrate si el CT tiene bind.
  19. Referencias de H5.
  20. normalidad.
  21. Estado HECHO.
  22. Al día siguiente: update de imágenes en el CT, aparte del traslado.

Vuelta atrás:

  • Antes del paso 5: quitar la protección en VM 208, up -d allí y sync.
  • 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), el shared=1 y 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:

  1. Esperar ≥48 h a ver si la racha actual se mantiene.
  2. Si vuelven: una noche de ≥12 h con VM 208 parada.
  3. Después, de uno en uno y 24 h cada uno:
  4. Wi-Fi MT7921 deshabilitado;
  5. APST del NVMe desactivado (nvme_core.default_ps_max_latency_us=0);
  6. BIOS 1.15 → la última, con «Typical Current Idle»;
  7. otra fuente de alimentación;
  8. 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:

  1. node-maintenance de pmx-51: el conjunto con failover pasa a pmx-50, observa/personal esperan, y medir los cortes.
  2. Cuelgue natural de pmx-50: el acceso externo no se corta, gracias al segundo cloudflared.
  3. NAS parado 10 min: personal sigue (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-ctl central 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 solo watch/update local y un stub que remite al 212; el token de CF se retiró de 6 ficheros. Código en services/homelab-ctl/; botones en maint (pack «7 · Rutas»).
  • (c) Segundo cloudflared: CT 219 cloudflared2 (.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-medium y packages/voz_reserva.yaml: si leina-tts falla 90 s, los 12 satélites cambian; vuelven tras 5 min sanos. Probado. Commit 1fa559e en home-assistant-config.

11.2 Cierre y repaso (2026-10-19 tarde)

  • Tailscale: maint-212 autorizado; el 212 es el subnet router principal y la VM 208 queda de reserva. Incidente: la VM 208 tenía --accept-routes y, al pasar a secundaria, enrutó 192.168.0.XXX/24 por tailscale0 y quedó inalcanzable desde la LAN unos 75 min. Arreglo: tailscale set --accept-routes=false en la VM. Regla: ningún equipo de la LAN con accept-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 de 2.36.0@sha256 a latest; Uptime Kuma :2 y Redis :7-alpine respetan el mayor declarado. Snapshots previos: pre_update_20261019 (214/215/217 por pct, 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 -i sobre 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 en docs/service-catalog.md y una línea en AGENTS.md.
  • service_routes de RunWisp en VM 208 retirada; sustituta: rutas-smoke-diario.timer en el 212 → pushgateway → RutasSmokeFallos/RutasSmokeMudo.
  • contenedores_caidos.sh ya no avisa de los lazy (tunnel.lazy=true en el propio contenedor) recreados sin arrancar.
  • jev-seerr-dup se salta la pasada si Seerr duerme.
  • Reenviadores de transición: registran USO <puerto> <cliente> (una vez por hora); ~/scripts/reenvios-uso en VM 208 lista los que nadie usa. Retirada prevista hacia el 2026-10-26. Pendiente conocido: los sensores platform: rest de pihole_metrics.yaml en HA ya apuntan a observa.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.