Cómo añadir un servicio al homelab¶
Procedimiento actualizado el 11 de septiembre de 2026, contrastado con VM208 y el código desplegado. La configuración del servicio vive en su Compose; las rutas Docker, en sus labels. Un pequeño contrato JSON registra qué comprobar y dónde está la evidencia. No contiene secretos ni sustituye Compose.
Entrada rápida para humanos y agentes¶
- Leer esta guía y la ficha del producto; identificar dueño, datos y uso real.
- Crear candidato con
service_check.py init; rellenar Compose y contrato. - Revisar estado vivo y diferencias antes de recrear cualquier contenedor.
- Preparar recuperación y autenticación antes de publicar el hostname.
- Desplegar únicamente el servicio elegido y sus dependencias nuevas conocidas.
- Comprobar configuración, acceso y una operación real; registrar los límites.
- Registrar el contrato en el comprobador periódico existente y guardar fuentes/evidencia.
Herramientas en VM208: /home/monxas/scripts/service-onboarding/.
Fuentes: scripts/service-onboarding/ del repositorio homelab-infra.
# Estado guardado: no vuelve a inspeccionar Docker ni solicita nada a las apps.
python3 /home/monxas/scripts/service-onboarding/service_check.py status
# En VM208. Crea archivos candidatos; no instala ni expone nada.
python3 /home/monxas/scripts/service-onboarding/service_check.py init mi-servicio --directory /home/monxas/service-candidates/mi-servicio
# Tras adaptar el contrato a la instalación real:
python3 /home/monxas/scripts/service-onboarding/service_check.py check /home/monxas/scripts/service-onboarding/contracts/mi-servicio.json --http --output /home/monxas/appdata/service-onboarding/mi-servicio.json
# Comprobación de los servicios registrados, sin solicitudes a Sablier:
python3 /home/monxas/scripts/service-onboarding/service_check.py fleet /home/monxas/scripts/service-onboarding/contracts --http --output /home/monxas/appdata/service-onboarding/latest.json
La salida breve contiene fecha, fallos y comprobaciones pendientes; el detalle permanece en JSON. Código 0 significa que las condiciones ejecutadas pasan, no que toda la aplicación esté validada. unverified y recorded nunca equivalen a una prueba funcional actual. Código 1: al menos un fallo; código 2: invocación inválida. No pegar secretos, logs completos ni bases de datos en una conversación.
Qué automatiza el alta y qué requiere adaptación¶
Esta guía es el procedimiento que debe seguir una petición como «añade un servicio». Una pregunta hipotética o un encargo de diseñar el plan no implica instalarlo. No hay un instalador integral: las herramientas existentes generan candidatos, validan y programan; el trabajo propio del producto sigue siendo parte del alta.
| Etapa | Automatismo existente | Trabajo específico y evidencia necesaria |
|---|---|---|
| Preparar | service_check.py init NOMBRE --directory DIRECTORIO_NUEVO |
Elegir host, imagen fijada, recursos, puertos, paths, dependencias y ciclo de vida; completar plantilla y contrato |
| Contrastar instalación | service_check.py check CONTRATO |
Reconciliar con el estado vivo; revisar opciones Docker no cubiertas; añadir probes reales, sin inventar endpoints |
| Acceso y rutas | route_check.py HOSTNAME |
Configurar identidad y políticas, publicar solo el hostname, probar login y acceso LAN/WAN; el inspector no publica ni configura OIDC |
| Recuperación y tareas | register_runwisp_task.py FRAGMENTO valida; --apply registra |
Implementar backup coherente y retención, probar restauración y primera ejecución; el helper no genera backups ni ejecuta tareas |
| Seguimiento | Contrato desplegado en contracts/ |
Completar route_hostnames cuando corresponda; entra en chequeo horario y rutas diario existentes, sin otra tarea por producto |
| Próxima consulta | service_check.py status |
Leer fecha, fallos y pendientes del JSON guardado; investigar solo lo necesario, revalidando antes de mutar |
Las tres fuentes tienen responsabilidades distintas: Compose declara, el contrato selecciona comprobaciones y referencia evidencia, la ficha explica acceso y operación. Los scripts no reemplazan la prueba funcional del producto ni la restauración. Un servicio solo queda cerrado cuando cada prueba necesaria tiene resultado o una limitación explícita.
El procedimiento sirve para cualquier nuevo servicio del homelab; la automatización de runtime actual cubre Compose/Docker en VM208. Un LXC, unidad systemd o despliegue en otro host requiere adaptar la comprobación y documentar esa cobertura, sin fingir que el contrato Docker lo valida. No imponer SQLite, OIDC o Sablier cuando el producto o su uso no los admitan.
Skill disponible para futuras sesiones¶
La skill homelab-add-service se puede invocar con $homelab-add-service y permite selección automática cuando la petición trata de incorporar un servicio al homelab. La copia instalada está en ~/.codex/skills/homelab-add-service/; la fuente versionada, en skills/homelab-add-service/SKILL.md. La skill dirige al procedimiento y a los scripts existentes, sin mantener otro instalador ni duplicar la guía.
Para mantenerla, editar la fuente versionada, validar con ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py, actualizar los archivos correspondientes de la copia instalada y comprobar que coinciden. La entrada estable local es ~/homelab-docs/como-anadir-servicio.md; si cambia el checkout, actualizar allí su enlace. Actualizar también la copia operativa de esta guía en VM208, comprobando cambios concurrentes. La carga del catálogo depende de la sesión: si no aparece todavía, abrir una nueva sesión o indicar la ruta exacta de SKILL.md.
1. Ficha mínima antes de instalar¶
| Decisión | Registrar |
|---|---|
| Propósito | Qué hará el usuario; si una aplicación existente ya lo resuelve |
| Dueño | Host, VM/LXC, cuenta, repositorio, rama, Compose o unidad activa |
| Recursos | Arquitectura/CPU compatibles, RAM medida/presupuestada, CPU/GPU, espacio e I/O |
| Ciclo de vida | Siempre activo o Sablier; tareas que impiden dormir y dependencias que deben acompañarlo |
| Persistencia | Cada directorio/volumen, UID/GID, permisos, NAS frente a disco local, caché recreable |
| Acceso | URL, puerto publicado e interno, cliente web/móvil/RSS, LAN y WAN, autenticación |
| Dependencias | Base de datos, Redis, buscador, navegador, OAuth; versiones y disponibilidad necesarias |
| Recuperación | Método coherente de copia, ubicación, retención, restauración, RPO/RTO razonables |
| Operación | Healthcheck, prueba funcional, dueño del calendario, límite/lock, señal de fallo/frescura |
| Actualización | Versión/digest, compatibilidad de DB, política de actualización y reversión |
No generar contraseñas de ejemplo que acaben en producción. Usar SOPS/age y los almacenes existentes; .env vive solo en el host con permisos limitados. Comparar valores en memoria y mostrar únicamente nombres de claves distintas. No poner tokens en argumentos de curl, historial, contratos ni repositorio. Un nuevo cliente OIDC necesita su configuración real: no hay un label que lo invente.
2. Identificar fuente y detectar diferencias¶
Acceso habitual: ssh -o StrictHostKeyChecking=yes media-208. Los stacks vivos están bajo /home/monxas/stacks/*/compose.yaml; no desplegar el checkout del Mac encima sin reconciliarlo. Revisar las instrucciones AGENTS.md aplicables y preservar checkouts sucios mediante worktree.
El comprobador ejecuta docker compose config --format json, Docker inspect e image inspect, pero guarda solo comparaciones filtradas. Contrasta imagen resuelta, entorno con defaults de imagen, memoria, mounts incluidos volúmenes inesperados, nombres de red, puertos, política de reinicio y labels de rutas. Comprueba estado, healthcheck cuando existe, dependencias y persistencia esperada. No compara todas las opciones posibles de Docker: revisar también comandos, capabilities, devices, CPU, usuario, secrets/configs y redes especiales si se utilizan.
La salida JSON de Compose vuelve a escapar los dólares del entorno ($ pasa a $$). El comprobador decodifica esa capa una sola vez antes de comparar con Docker inspect; no transforma defaults de imagen. Si aparece una diferencia en una credencial, verificar primero esta representación con un dato sintético y el comportamiento real, sin imprimirla ni cambiarla para satisfacer el checker.
No aceptar diferencias conocidas de forma automática. Una recreación con Compose antiguo puede perder OAuth, un volumen anónimo, un límite de RAM o una red aunque el contenedor arranque. Immich y Karakeep requirieron esa reconciliación el 11 de septiembre. El contrato de Immich conserva expresamente su volumen /data existente.
Antes de editar, crear una copia privada de los archivos afectados y una copia coherente de sus datos. Registrar hashes y diff filtrado. Volver a verificar el hash justo antes de aplicar para detectar cambios concurrentes. Un backup antiguo del Compose puede ser obsoleto: no es una reversión segura sin comparar con el estado conservado.
3. Preparar Compose y almacenamiento¶
El generador deja un fragmento incompleto a propósito: imagen, paths, puertos y salud deben proceder del producto real. La plantilla no publica hostname. Elegir versión/digest y comprobar la imagen instalada antes de usar --pull never; una instalación nueva necesita descargar la imagen exacta de forma explícita.
- Persistir DB, configuración y metadatos, no solo medios. Declarar volúmenes anónimos existentes antes de recrear.
- Verificar NAS montado, ruta correcta, permisos y capacidad. Evitar que Docker cree un directorio local vacío cuando falta NFS. No montar/desmontar ni reiniciar el NAS como parte del alta.
- Bases de datos: usar su exportación nativa o copia en frío coherente. No copiar una SQLite activa omitiendo WAL. Usar SQLite compatible con la aplicación.
- Marcar cachés recreables y exclusiones de backup. Los medios NAS y la DB local tienen coberturas diferentes.
- Publicar solo los puertos necesarios y probarlos desde los nodos consumidores. Caddy vive fuera de Docker; un puerto atado a
127.0.0.1en VM208 no es accesible desde CT270/271. - Evitar
privileged, socket Docker o redes amplias salvo necesidad documentada. Mantener UID/GID y volúmenes de solo lectura donde corresponda. - Definir healthcheck con la ruta/binario que realmente soporte la imagen; no inventar
/health.
4. Rutas, DNS y autenticación¶
El dueño habitual es /home/monxas/scripts/homelab-ctl.py, con labels Docker y /home/monxas/scripts/tunnel-static.yaml para servicios fuera de VM208. La copia estática del checkout y la desplegada difieren al revisar: leer la del host y reconciliar antes de sincronizar.
| Label reconocido | Uso |
|---|---|
tunnel.hostname |
FQDN o lista separada por comas; activa inclusión en rutas públicas |
tunnel.port |
Puerto publicado; evitar depender del primer binding |
tunnel.target_port |
Puerto interno explícito al seleccionar el binding en stacks_shared |
tunnel.lazy |
true para Sablier, false por defecto |
tunnel.lazy.names |
Nombres exactos de todos los contenedores que despiertan juntos |
tunnel.lazy.session_duration |
Duración de sesión; defecto 30m |
tunnel.lazy.theme |
Pantalla de espera; defecto shuffle |
tunnel.lazy.display_name |
Nombre visible |
No existen tunnel.auth, tunnel.sablier, tunnel.upstream, tunnel.timeout ni tunnel.lan_only. El generador incorpora los hostnames al Tunnel: no usar estos labels para un servicio que deba ser solo LAN. Ese caso requiere un diseño explícito distinto antes de publicar nada.
Caddy: CT270 en pmx, .40; CT271 en pmx2, .41; VIP .250. Cloudflare Tunnel apunta a https://192.168.0.250:443 con SNI del hostname. Caddy usa 192.168.0.208:puerto-publicado para Docker, no nombres DNS de una red Docker remota. La copia /home/monxas/stacks/reverse-proxy/managed.caddy no es el servidor: comprobar configuración activa de ambos nodos.
Para otro host, el formato realmente soportado es:
No duplicar un hostname en labels, estáticas y Caddy manual. En el código actual las labels ganan a estáticas: una colisión puede ocultar errores.
DNS: verificar registro exacto y wildcard efectivo en Cloudflare, proxy y destino; un wildcard no corrige un registro exacto equivocado. Comprobar ambos DNS de LAN y resolución WAN por separado. Pi-hole interno dirige el dominio a VIP; eso evita Cloudflare Access. Autenticación nativa/OIDC y protección en el borde son capas distintas. RSS y clientes móviles pueden requerir acceso nativo, no un redirect SSO indiscriminado. Un 302 de Access no prueba login ni funcionamiento de la app. Un enlace RSS privado no es una credencial que deba aparecer en docs.
Crear/verificar Access y política restrictiva adecuada antes de exponer una UI nueva; no copiar la política de otra aplicación por comodidad. Probar usuario permitido y rechazo sin sesión en LAN/WAN, callback OIDC, cierre de sesión y cliente nativo aplicable. Marcar pruebas que requieran participación del usuario como pendientes.
route_check.py inspecciona las rutas sin visitar la aplicación: usar su --help para los argumentos vigentes. Contrasta configuración deseada y activa y no despierta servicios. Es una inspección, no un publicador.
5. Aplicar el cambio acotado¶
Preparar candidato concreto, backup y validaciones antes de cualquier autorización final que realmente sea necesaria. Si el usuario ya pidió instalar/configurar ese servicio, continuar dentro del alcance autorizado sin volver a pedir permiso por rutina.
# En el directorio del stack correcto, con imagen exacta ya disponible:
docker compose config --quiet
docker compose up -d --no-deps --pull never mi-servicio
--no-deps exige que las dependencias necesarias estén preparadas. Desplegar nuevas dependencias por nombre y en orden, no levantar todos los stacks. No usar down, prune, watch ni update como test. Comprobar que no han cambiado IDs/estado de servicios ajenos.
homelab-ctl sync no tiene dry-run seguro. Escribe y despliega Caddy incluso antes de preguntar por Cloudflare. --yes no limita el alcance: recoge todas las rutas, puede reiniciar Caddy y visita todos los hostnames al terminar. No incluirlo como paso automático del alta. status lee; smoke puede despertar Sablier. watch, update y remove mutan; remove deja labels/estáticas que pueden recrear una ruta después.
Revisar diff completo de rutas deseadas frente a ambas configuraciones activas. Aplicar solo el hostname acordado, preservando estructura y rutas ajenas. Validar el Caddyfile completo con su entorno, después recargar secundario y luego primario, y comparar comportamiento/configuración activa. Si un nuevo bloque Sablier exige restart por caché del plugin, hacerlo expresamente y verificar ambos nodos. Conservar reversión local/remota y la configuración de Tunnel previa; Caddy, DNS y Access no forman una transacción única.
El antiguo scripts/add-service.sh modifica Caddy manualmente y lleva una política de identidad concreta. No mezclarlo con homelab-ctl para el mismo hostname ni tratarlo como instalador universal. Los nuevos scripts generan candidatos y comprueban; la publicación requiere un diff dirigido al servicio real.
6. Automatización y observabilidad sin otra plataforma¶
RunWisp ya es el dueño de tareas en VM208. No añadir además cron/systemd para lo mismo. Su fichero es /home/monxas/runwisp/runwisp.toml, datos /home/monxas/runwisp/data. Formato actual:
[tasks.mi_servicio]
cron = "17 * * * *"
run = "/ruta/absoluta/script --limite-explicito"
env_base = "clean"
working_dir = "~"
catch_up = "skip"
on_overlap = "skip"
Consultar runwisp schema y runwisp agent-guide para obtener el contrato del binario instalado, evitando reconstruirlo de memoria. No usar command, [[tasks]] ni sintaxis de otro scheduler. El helper register_runwisp_task.py FRAGMENTO.toml valida con ese binario y muestra un plan; --apply añade una única tarea nueva con backup privado, comprobación de cambios concurrentes y reload. Rechaza nombres existentes y otras secciones; no ejecuta la tarea. Usar un fragmento por servicio, sin secretos inline. Validar candidato con el binario instalado, copiar atómicamente y recargar:
/home/monxas/.local/bin/runwisp -c /home/monxas/runwisp/runwisp.toml --data /home/monxas/runwisp/data validate --json
/home/monxas/.local/bin/runwisp -c /home/monxas/runwisp/runwisp.toml --data /home/monxas/runwisp/data reload
Los scripts deben tener lock propio para ejecuciones manuales concurrentes, timeout total, límite de trabajo, dry-run sin escrituras operativas, estados atómicos y códigos de error reales. Escribir temporal en el mismo filesystem que el destino; publicar sin sobrescribir si el destino puede aparecer. Cargar estado dentro del lock. Guardar contadores aunque el motor esté apagado; distinguir offline, sin datos, obsoleto y error. Evitar recorrer o hashear todo el NAS en cada pasada; conservar inventario incremental y tratar archivos cambiantes como pendientes.
El contrato nuevo se incorpora al directorio contracts/ para entrar en el chequeo periódico service_contracts. El chequeo visita solo endpoints backend explícitos de contenedores que ya están en marcha; si duermen, no hace HTTP. No consulta URLs protegidas ni sigue redirects. Guardar último informe en /home/monxas/appdata/service-onboarding/latest.json y revisar fallos/frescura en RunWisp. service_routes comprueba diariamente a las 12:23 los route_hostnames de esos mismos contratos y deja routes.json; reutiliza un snapshot por lote y no visita las aplicaciones. No hay bot nuevo ni notificaciones de prueba.
La primera cobertura registrada es Audiobookshelf, Immich y Karakeep. No representa un audit automático de todo el homelab. Añadir otros contratos cuando se revise cada producto. Códigos no cero reflejan fallos; referencias de pruebas caducadas aparecen como pendientes, no falsos fallos de runtime. Una tarea RunWisp disponible no demuestra que su última ejecución funcionó; revisar el resultado y fecha del JSON. Conectar notificaciones al mecanismo existente únicamente con destinatario y alcance autorizados.
7. Copia, recuperación y prueba funcional¶
Para un producto con DB, demostrar una restauración aislada: red deshabilitada, puertos sin publicar, medios originales de solo lectura, nada que envíe notificaciones ni sincronice clientes reales. Verificar identidad de registros, progreso, configuraciones y muestra de contenido. Integridad del archivo/dump no equivale a restauración de la app. Registrar exactamente qué cubre la prueba y cuánto tardó.
PBS de VM208 no incluye por sí solo datos NFS. Revisar cobertura NAS independiente. Las copias nativas pueden omitir medios/portadas; comprobar el código o documentación de la versión instalada. No reactivar el backup completo redundante retirado ni añadir una copia masiva diaria por defecto. El exportador PostgreSQL existente debe incluir cualquier DB nueva y su política si duerme.
Prueba funcional mínima específica: guardar/buscar un marcador, abrir un documento, subir/leer una foto o encontrar/reproducir un audio según el producto. Una escritura de prueba debe ser acotada y reversible; no iniciar bibliotecas enteras, descargas o reproducción en altavoces reales para un smoke test. Registrar antes/después y eliminar únicamente el objeto de prueba si procede. Healthcheck, HTTP 200, login y operación de negocio son pruebas diferentes.
Un servicio lazy necesita además comprobar arranque del grupo, readiness y vuelta a reposo sin monitores que lo mantengan vivo. En Sablier 1.9, consultar GET /api/strategies/dynamic crea/renueva una sesión incluso si responde ready: no es una inspección pasiva. Retirar el middleware de Caddy no cancela necesariamente una sesión existente; verificar su expiración para evitar una parada tardía. Servicios que procesen tareas pendientes pueden requerir un calendario independiente de las visitas web.
8. Cierre, actualización y retirada¶
Guardar fuente reconciliada, contrato, guía de acceso, backup/rollback, versión/digest, resultados, hora UTC y límites. Enlazar desde la documentación principal y AGENTS.md; guardar commit local sin pisar trabajo anterior. No publicar secretos ni contenido privado. La siguiente sesión empieza por el JSON compacto y solo abre evidencia de los fallos o cuestiones pendientes.
Para actualizar, repetir preflight, copia coherente, compatibilidad de migraciones, target único y prueba de conservación. Una migración de DB puede impedir volver solo a la imagen vieja: la reversión debe contemplar datos coherentes y cambios posteriores.
Para retirar, eliminar el dueño declarativo, contenedor/dependencias exclusivos, tarea, monitor, ruta en ambos nodos, ingress, DNS exacto y Access. Decidir explícitamente qué datos/copia conservar. No borrar red compartida, wildcard, imagen compartida ni backup histórico por asociación. Comprobar después que el reconciliador no lo recreará. Changedetection retirado el 11 de septiembre sirve como ejemplo; qBittorrent conserva datos por una decisión distinta.
Lección de Audiobookshelf: añadir un archivo puede disparar la relectura de etiquetas viejas y reemplazar metadatos editados en la app. El importador debe verificar la conservación de los elementos existentes, no solo contar el archivo nuevo. Distinguir cambios derivados del catálogo (p.ej. ctime contrastado con stat) de pérdidas reales de título/fecha/descripción, y proteger estas últimas mediante las API del producto.