Migrando el homelab de un BMAX a un GMKtec G11 (II): 23 servicios, uno a uno

Terminal mostrando la restauración en curso de varios contenedores LXC en Proxmox

Con la red del GMKtec ya montada y el storage de PBS visible desde el nuevo nodo (ver la primera parte), tocaba lo que de verdad importaba: restaurar los 23 servicios que corrían en el BMAX, sin cortar nada que no hiciera falta cortar.

El orden que decidí, y por qué cambió sobre la marcha

La primera versión del plan tenía a Home Assistant migrando bastante pronto en la lista. Pero cayendo en la cuenta a tiempo, me di cuenta de que Home Assistant, mqtt, zigbee2mqtt, el RFXtrx y la monitorización del SAI dependen todos de hardware USB físico — el coordinador Zigbee, el propio RFXtrx, el cable del SAI — y esos dispositivos no pueden estar enchufados a dos máquinas a la vez. Así que ese bloque entero pasó a ser el último paso de todos, fusionado con el momento del traslado físico definitivo.

El resto quedó así:

Orden VMID Servicio Por qué en ese punto
1 138 Pi-hole DHCP de toda la casa — sin dependencia de USB
2 110 AdGuard DNS de toda la casa
3 102 WireGuard El único camino de entrada remota — migrado pronto por si algo se torcía después
4 140 Vaultwarden Contraseñas, sin dependencia de USB
5+ el resto sin USB Jellyfin, Navidrome, Piwigo, Homepage, Grafana/Loki, Vikunja, NetAlertX, WatchYourLAN, Compra-app, Cuotix, Piper-TTS, ct-homebot, Uptime Kuma, Webserver-local Sin dependencias fuertes entre sí, orden libre
Casi al final 145 (pve-exporter), 103 (PBS) El exporter necesita un token nuevo creado en el propio GMKtec; PBS se deja para el final del bloque porque mientras lo migras sigues necesitándolo funcionando para restaurar el resto
Último, con el traslado físico 101, 106, 109 + RFXtrx + SAI Home Assistant, mqtt, zigbee2mqtt, RFXtrx, SAI — todo lo que depende de hardware USB que solo puede estar en un sitio a la vez

Transmission (128) se quedó fuera de este orden a propósito: nunca tuvo backup en PBS (decisión tomada de antemano, sin datos que mereciera la pena preservar en el propio contenedor), así que su migración necesitaría un tratamiento aparte si algún día se decidía hacer.

El patrón que repetí 22 veces

Para cada servicio, el procedimiento fue siempre el mismo: restaurar desde PBS sin arrancar todavía, revisar la red restaurada, apagar el original en el BMAX (con onboot 0 para que no se levantara solo si el BMAX se reiniciaba por cualquier motivo durante el resto de la migración), arrancar el nuevo, y verificar de verdad —no solo que el proceso estuviera arriba, sino que respondiera con normalidad.

pct restore <VMID> pbs:backup/ct/<VMID>/<timestamp> --storage local-lvm

o qmrestore para las dos VMs (Home Assistant y PBS).

Un detalle que aprendí por las malas con la propia VM de PBS: la regla de "apaga primero el original" aplica igual a VMs que a CTs, pero con una VM la IP no aparece en qm config —vive dentro del propio sistema operativo invitado—, así que es fácil olvidarse de pararla antes y acabar con dos máquinas respondiendo a la misma IP sin darte cuenta hasta que algo empieza a fallar de forma rara.

Y en cada verificación, aparte de comprobar que el servicio cargaba, revisaba la memoria real, no solo que el proceso siguiera vivo:

pct exec <VMID> -- free -h

Los límites que traían los contenedores estaban calculados para un BMAX de 7.6GB con overcommit constante. En el GMKtec varios se quedaron cortos —sobre todo los que corrían Docker dentro del LXC, como NetAlertX, donde nginx, PHP-FPM y los procesos internos competían por el mismo límite de 512MB y el contenedor Docker interno acababa muriendo con SIGKILL poco después de arrancar. Le subí la memoria hasta 2GB y el problema desapareció; no había motivo para mantener los límites ajustados que arrastraba por necesidad en el equipo viejo.

El caso Navidrome: cuando el backup no trae lo que crees

Este fue el hallazgo más importante de toda la migración, porque se repitió más de una vez. Al restaurar Navidrome (CT135), la biblioteca de música apareció completamente vacía —el pct.conf restaurado sí mencionaba el mp0 de datos, pero el volumen en sí nunca había viajado en el backup.

La causa quedó clara al mirar el log de la tarea original de vzdump, no el de la restauración:

excluding volume mount point mp0 ('/data/music') from backup (disabled)

Proxmox excluye por defecto los mountpoints adicionales de un backup, salvo que se marquen explícitamente con backup=1 —nadie lo había hecho nunca para ese volumen, así que ningún backup de los últimos meses lo había incluido jamás.

La solución fue un rsync directo entre el BMAX y el GMKtec, pero con dos matices que costó descubrir:

  • Hay que lanzarlo desde dentro del propio contenedor de destino (pct exec <VMID> -- rsync ...), no desde el host —los mountpoints de un CT en marcha no son visibles tal cual desde /var/lib/lxc/<VMID>/rootfs/ en el host, solo con el CT parado y montado explícitamente.
  • Hace falta --no-owner --no-group si el contenedor es unprivileged, o falla al final de cada fichero con chown ... Invalid argument —el UID/GID original queda fuera del rango de mapeo permitido dentro del contenedor.

Y como la carpeta de música estaba gestionada por Syncthing, apareció un efecto colateral: Syncthing había llegado a escanear la carpeta mientras seguía vacía (antes de que el rsync terminara), y se quedó con el error folder marker missing. Un simple reinicio del servicio, una vez el marcador .stfolder ya estaba presente de verdad, lo resolvió:

pct exec <VMID> -- systemctl restart syncthing@root.service

Quedaron además tres elementos "no sincronizados" pendientes de confirmar manualmente en la UI de Syncthing —resultaron ser solo metadatos internos (.stfolder.removed-*), seguros de confirmar sin pérdida real.

Con Piwigo pasó algo parecido, aunque el mensaje del log fue más explícito esta vez (from backup (not a volume), porque en ese caso era un bind-mount NFS hacia la NAS, no un volumen local): el bind-mount a /mnt/piwigo-nas tampoco viajaba nunca en ningún backup, y hubo que replicar el mismo mount NFS en el host del GMKtec —ampliando antes el ACL del export en la NAS para permitir la nueva IP del cliente.

El incidente de PBS en mitad de la migración

A medio camino, PBS empezó a dar errores de "Connection timed out" al intentar hacer backup. El diagnóstico llevó por varios callejones: primero sospeché de la VM colgada por presión de memoria, luego de un lock de backup atascado —y la causa real resultó ser mucho más tonta y a la vez más reveladora: el disco raíz de la VM de PBS estaba al 99%, con solo 401MB libres de 29GB.

Bajando capa a capa (/var → /var/log → /var/log/proxmox-backup/tasks/) encontré cinco ficheros de log de 24 gigabytes cada uno, generados por un verificationjob que llevaba meses reventando en un bucle sin backoff contra un datastore vacío llamado local-backups —un resto de configuración de cuando, mucho antes, se había intentado montar el Storage Box de Hetzner directamente por SSHFS y se abandonó la idea por problemas de permisos UID. Ese datastore fantasma nunca tuvo contenido real, así que cada intento de verify se topaba con un EIO y entraba en un bucle que generaba gigabytes de la misma línea repetida.

La limpieza fue sencilla una vez localizada: truncar los logs para recuperar espacio, borrar el datastore fantasma (confirmando antes, revisando los 4 jobs de vzdump, que ninguno lo usaba), y fijar una retención de logs de tareas para que esto no volviera a pasar sin darse cuenta:

proxmox-backup-manager node update --task-log-max-days 30

De paso, este incidente sacó a la luz algo más gordo: el sync job de backup offsite de PBS (hacia un servicio de terceros contratado en su día) llevaba también meses sin funcionar de verdad —apuntaba a un periodo de prueba caducado, y encima al datastore fantasma en vez del real. Es decir, durante todo ese tiempo no había existido ninguna copia de los backups fuera de casa, sin que nadie lo supiera hasta que un problema no relacionado obligó a mirar dentro.

Rediseñando WireGuard: de rutas sueltas a un /24 limpio

Con varios servicios ya migrados y accesibles por VPN, llegó el momento de montar la segunda red interna que había planeado desde el principio. La idea: los servicios que se acceden desde fuera por WireGuard —Pi-hole, AdGuard, la gestión del propio Proxmox, Home Assistant— se conectan además a vmbr1 (10.50.99.0/24), con una segunda interfaz de red, sin tocar la que ya tienen en 192.168.1.0/24.

net1: name=eth1,bridge=vmbr1,ip=10.50.99.4/24,type=veth

Sin gateway declarado en esa interfaz: el contenedor sigue saliendo por su ruta habitual, y eth1 solo responde al tráfico que ya sabe llegar directamente a esa subred —que es justo lo que hace WireGuard.

Para que ese segmento no se convirtiera en una puerta trasera hacia el resto de la LAN, me apoyé en tres capas: el host GMKtec sin reenvío de paquetes activado (ya lo teníamos desde la instalación), los contenedores dual-homeados sin forwarding tampoco (el valor por defecto, no había que tocar nada), y una única excepción intencionada —el propio CT de WireGuard, al que sí se le activó el forwarding porque su función es precisamente esa, rutear tráfico del túnel hacia la LAN.

Con CT102 dual-homeado, el peer correspondiente en el servidor WireGuard del VPS pasó a declarar las dos subredes:

AllowedIPs = 192.168.1.0/24, 10.50.99.0/24

Y en el perfil que hasta entonces acumulaba rutas /32 sueltas cada vez que hacía falta acceder a un servicio nuevo, esas rutas se sustituyeron por una sola línea:

AllowedIPs = 10.100.0.0/24, 10.50.99.0/24

Cualquier cosa que se moviera a 10.50.99.0/24 a partir de entonces quedaría cubierta automáticamente, sin volver a tocar el perfil.

El traslado físico final

Con todo lo demás migrado y confirmado, llegó el momento de mover el hardware USB —el coordinador Zigbee, el RFXtrx, el cable del SAI— del BMAX al GMKtec. Antes de desenchufar nada, anoté las rutas persistentes de cada dispositivo (/dev/serial/by-id/, nunca /dev/ttyUSBx, que puede reordenarse al cambiar de puerto o de equipo), restauré ya en el GMKtec los CT/VM de Home Assistant, mqtt y zigbee2mqtt sin arrancarlos, y apagué el BMAX de forma limpia.

Con los USB ya movidos y sus mismos identificadores by-id confirmados en el nuevo equipo, arranqué en orden —mqtt primero, luego zigbee2mqtt, luego Home Assistant, porque cada uno depende del anterior— y verifiqué que las entidades Zigbee reportaban estado sin necesidad de volver a emparejar nada, que el RFXtrx respondía, y que las automatizaciones seguían disparándose con normalidad.

La reubicación del SAI mereció su propio hallazgo, porque no era un simple passthrough USB a un contenedor: descubrí que NUT corría directamente en el sistema operativo del host, no dentro de ningún CT, por una razón de diseño que se me había olvidado hasta ese momento —para que el propio Proxmox pudiera apagarse de forma ordenada ante un corte de luz largo, algo que un contenedor no puede hacer sobre la máquina que lo aloja. Ese hallazgo mereció su propio post: NUT vive en el host, no en un CT: la sorpresa al migrar el SAI al GMKtec.

Con todo verificado, llegó el momento de reasignar la IP definitiva del GMKtec —de la 192.168.1.15 provisional a la 192.168.1.10 que había tenido el BMAX siempre—, actualizando de paso la reserva DHCP, cualquier rewrite de DNS que quedara apuntando a la IP vieja, el certificado HTTPS de Proxmox, y un script propio (ct-homebot, un bot de Telegram que administra los discos de la casa por SSH al propio host) que tenía la IP del nodo hardcodeada en su configuración. El BMAX quedó apagado definitivamente, sin desmontar todavía por si acaso.

De cómo se completó realmente el cierre —el mirror ZFS, un susto de arranque con causa raíz sorprendente, y la monitorización que faltaba— trata la tercera y última parte.

Jaume Ferré

Jaume Ferré

Tengo un trabajo que no tiene nada que ver con esto. Pero me gusta cacharrear con webs, Linux y el homelab. De vez en cuando alguien me paga por ello, lo cual siempre sorprende. Canon R7 en mano y Arch Linux de fondo.

¿Te ha sido útil?

Ayúdame a mejorar con tu puntuación.

0.0 (0 votos)
Comentarios