Adding a new service¶
Cómo exponer un servicio Docker nuevo bajo *.monxas.casa con Caddy + CF Tunnel.
La fuente única de configuración es homelab-ctl.py (en scripts/, desplegado
en VM 208 como /home/monxas/scripts/homelab-ctl.py) que lee labels Docker,
genera managed.caddy y reconfigura el ingress de CF Tunnel.
Reescrito 2026-08-01 tras verificar contra el script real
Esta guía describía un esquema de labels (tunnel.auth, tunnel.sablier,
tunnel.upstream, tunnel.lan_only, tunnel.timeout) que no existe en
el homelab-ctl.py actual (verificado leyendo el fichero + diff md5 contra
lo desplegado en VM 208, ambos idénticos a 2026-08-01). El script de hoy es
un CLI unificado (status/sync/watch/update/smoke/remove) que
reemplazó al viejo tunnel-sync.py + sablier-watchdog.py. Contenido
corregido abajo.
Dos archivos tocan, un comando reconcilia
docker-compose.ymldel stack — labels del container.managed.caddygenerado — no editar a mano (homelab-ctl.py synclo valida y despliega directo por SSH a los 2 nodos Caddy, no hay rsync).
El ingress de CF Tunnel también lo reconcilia sync (vía API Cloudflare).
forward_auth no se materializa en Caddy — no hay label de auth
El script no tiene ningún label de autenticación (no existe
tunnel.auth, ni nada equivalente). El SSO real es OIDC nativo per-app
(cada app con soporte OIDC apunta directa a pocketid.monxas.casa), y el
borde lo cubre Cloudflare Access (apps/policies via API — ver
Creating a subdomain Modo B/C). Ninguno de los dos
deja rastro en managed.caddy.
Workflow¶
1. Añadir labels al container¶
En el docker-compose.yml del stack en VM 208:
services:
newservice:
image: example/newservice:latest
ports:
- "12345:8080" # host_port:container_port
labels:
- tunnel.hostname=newservice.monxas.casa
- tunnel.port=12345
# Opcional: lazy-start via Sablier (apaga cuando no hay tráfico)
- tunnel.lazy=true
- tunnel.lazy.session_duration=30m
- tunnel.lazy.theme=shuffle
- tunnel.lazy.display_name=New Service
Labels reales leídos por homelab-ctl.py (verificado en el fuente,
collect_desired_labels()):
| Label | Valor | Default |
|---|---|---|
tunnel.hostname |
FQDN(s) bajo monxas.casa, admite lista separada por comas |
requerido |
tunnel.port |
Puerto publicado del container (host o expuesto, según red) | fallback: primer puerto publicado |
tunnel.target_port |
Puerto interno del container si el nombre difiere del publicado (containers en la red compartida stacks_shared) |
— |
tunnel.lazy |
true | false — activa Sablier (lazy-start/stop) |
false |
tunnel.lazy.names |
Nombre(s) de container que Sablier debe despertar | nombre del container |
tunnel.lazy.theme |
Tema de la pantalla de espera Sablier | shuffle |
tunnel.lazy.session_duration |
Cuánto se mantiene despierto tras el último request | 30m |
tunnel.lazy.display_name |
Nombre mostrado en la pantalla de espera | nombre del container |
No existen labels tunnel.auth, tunnel.sablier (renombrado a tunnel.lazy),
tunnel.upstream ni tunnel.timeout — si los ves en un compose viejo son
restos de un esquema anterior y no hacen nada.
2. Aplicar labels al runtime¶
Labels solo afectan al container nuevo, así que hay que recrearlo:
ssh [email protected] 'cd ~/stacks/<stack> && docker compose up -d newservice'
3. Reconciliar Caddy + CF Tunnel¶
Desde la Mac (o el host VM 208 directamente):
ssh [email protected] 'python3 ~/scripts/homelab-ctl.py sync --yes'
Esto (verificado leyendo cmd_sync()):
- Lee labels de TODOS los containers en VM 208 (
docker ps -a, incluidos lazy parados) +~/scripts/tunnel-static.yamlpara hosts no-Docker. - Genera
managed.caddy, lo valida y lo despliega por SSH directo a los dos nodos Caddy HA (192.168.0.40/.41= LXC 270/271) — no hay paso de rsync intermedio. - Si algún nodo falla la validación, hace rollback en todos (local + remoto).
- Reconcilia el ingress de CF Tunnel (API Cloudflare) apuntando siempre
al VIP de Caddy (
192.168.0.250:443) — nunca directo a VM 208 (nada escucha ya en VM208:443 desde el cutover a Caddy HA). - Recarga (o reinicia, si hay un bloque
lazynuevo — recarga sola no basta para que el plugin Sablier lo recoja) Caddy en ambos nodos. - Hace un smoke-test HTTP de cada hostname y reporta fallos 5xx.
Otros subcomandos útiles del mismo script:
| Comando | Qué hace |
|---|---|
homelab-ctl.py status |
Muestra rutas deseadas + drift vs tunnel real, sin aplicar nada |
homelab-ctl.py watch |
Recrea cualquier container que debería existir y no está (cron cada 5min) |
homelab-ctl.py update |
Actualiza imágenes de servicios routeados (cron diario 4am) |
homelab-ctl.py smoke |
Solo el smoke-test HTTP, sin tocar nada |
homelab-ctl.py remove <hostname> |
Borra el hostname del ingress CF Tunnel (labels quedan; edítalas y vuelve a sync) |
4. Verificar DNS¶
CF tiene un wildcard *.monxas.casa desde 2026-05-20, así que no hace falta
crear un registro DNS por servicio nuevo. Verifica que resuelve:
Internamente (LAN), Pi-hole intercepta y devuelve 192.168.0.250 (VIP Caddy)
para *.monxas.casa.
5. Test¶
Desde dentro del cluster (bypass de DNS):
Desde fuera:
homelab-ctl.py sync ya hace este mismo smoke-test por ti al final (paso 3);
esto es solo para verificar a mano.
El restart manual de Caddy tras activar Sablier ya no hace falta
Versiones antiguas de esta guía documentaban un systemctl restart caddy
manual en LXC 270/271 cuando se activaba un lazy-route nuevo, porque un
reload no recogía el plugin Sablier. homelab-ctl.py sync ya lo hace
solo: needs_caddy_restart() detecta si el diff añade un bloque lazy
nuevo y usa restart en vez de reload automáticamente (verificado en el
fuente del script). No hace falta ningún paso manual.
Casos especiales¶
Servicio NO en VM 208¶
No existe un label tunnel.upstream. Para un backend que vive en otro
container/nodo (e.g. n8n en LXC 200), el mecanismo real es un fichero
estático ~/scripts/tunnel-static.yaml en VM 208 (leído por
collect_static_routes(), no depende de labels Docker en absoluto):
routes:
- hostname: n8n.monxas.casa
target: n8n.lan:5678
note: n8n vive en LXC 200 del cluster Proxmox
target es lo que Caddy usa en su reverse_proxy; puede ser un hostname LAN
(*.lan, resuelto por Pi-hole) o una IP:puerto directa. sync fusiona estas
rutas estáticas con las de labels Docker (las de labels ganan si hay
colisión de hostname).
Servicio LAN-only (sin ingress externo)¶
No verificado como soportado hoy — TODO
Versiones antiguas de esta guía describían un label tunnel.lan_only=true
para servir un hostname en Caddy sin añadirlo al ingress de CF Tunnel. Leyendo
el homelab-ctl.py actual, tunnel_reconcile() añade al ingress CF Tunnel
TODO hostname presente en tunnel.hostname/tunnel-static.yaml, sin ningún
flag de exclusión — no encontré ningún mecanismo actual para lograr
"en Caddy pero no en el Tunnel" solo con labels. Si necesitas esto hoy,
probablemente haga falta editar tunnel_reconcile() o gestionar el ingress
a mano tras cada sync. Verificar con el operador antes de asumir que existe.
Servicio con WebSockets / SSE¶
Caddy maneja WebSockets por defecto (reverse_proxy los pasa transparente).
No existe un label tunnel.timeout — no encontré ningún soporte de timeout
por-ruta en el script. Si hace falta un timeout distinto al default de Caddy,
hoy tocaría editar la plantilla Caddyfile del rol caddy_lxc a mano (fuera
del alcance de labels).
Troubleshooting¶
| Síntoma | Fix |
|---|---|
curl: ... 502 Bad Gateway |
Backend container muerto. docker ps \| grep <service> |
curl: ... 404 desde Caddy |
Label tunnel.hostname typo o homelab-ctl.py sync no corrió |
curl: ... Could not resolve externo |
CF wildcard inactivo o DNS propagation. Espera 1min |
curl: ... SSL_ERROR_SYSCALL interno |
Pi-hole devuelve IP vieja (cache). pihole -f para flush DNS |
sync dice "container port not published to host — skip" |
Falta tunnel.target_port o el puerto no está en HostConfig.PortBindings |
| Login PocketID falla | El servicio no tiene su propio OIDC client en PocketID. Ver Creating OIDC client |