Services routing — Caddy + Cloudflared + Sablier¶
Last updated: 2026-07-12
Documento canónico de cómo se publica un servicio en este homelab y cómo decides si se duerme o no. Single source of truth: las labels tunnel.* en el compose.yaml del servicio.
Arquitectura¶
Internet → Cloudflare → cloudflared (LXC 123)
│
▼ (tunnel ingress → VIP .250:443)
VIP keepalived .250
│ (VRRP: MASTER LXC 270 / BACKUP LXC 271)
▼
Caddy (LXC 270/271) ← único ingress
│
├─ [Sablier middleware] ← solo en hostnames "lazy"
│ │
│ ▼ (si container parado)
│ waiting page + arrancar via docker socket (VM 208)
│
└─ reverse_proxy → 192.168.0.208:<host_port>
- cloudflared (LXC 123): tunnel managed via dashboard (token-based). Todos los ingress van al VIP
https://192.168.0.250:443(keepalived, servido por Caddy LXC 270/271) connoTLSVerify=trueyoriginServerName=<hostname>. Caddy es el único origen. - Caddy (LXC 270
caddy-primary.247/ LXC 271caddy-secondary.248, tras VIP keepalived.250): build custom con pluginscaddy-dns/cloudflare+sablierapp/sablier/plugins/caddy. Cert wildcard*.monxas.casavia DNS-01 challenge. Admin :2019 conmetrics. El container Caddy en VM 208 fue eliminado (post-F4.f). - Sablier (plugin en Caddy): para containers de la VM 208 cuando llevan rato sin tráfico, los arranca on-demand cuando alguien hace request al hostname. Necesita el Docker socket de VM 208.
- DNS: registro wildcard
*.monxas.casaapunta a Cloudflare → cualquier subdominio nuevo funciona sin tocar DNS. - homelab-ctl.py (
~/scripts/homelab-ctl.py): reconcilia las labelstunnel.*(tunnel.hostname,tunnel.port,tunnel.target_port,tunnel.lazy*) de los containers running + las rutas estáticas de~/scripts/tunnel-static.yamlcon la config de cloudflared ymanaged.caddy. Reemplaza atunnel-sync.py+sablier-watchdog.py(subcomandosstatus/sync/watch/update/smoke/remove).
Estado actual¶
Sablier — 13 hostnames lazy¶
Todos declarativos via labels tunnel.lazy* en el compose de cada servicio.
| Hostname | Containers | Compose |
|---|---|---|
| pdf.monxas.casa | stirling-pdf | tools/compose.yaml |
| jellyseerr.monxas.casa | jellyseerr | media/compose.yaml |
| ocr.monxas.casa | paddleocr, paddleocr-ui | paddleocr-ui/docker-compose.yml |
| md, md-demo | markdown_demo_prod, markdown_backend_prod | markdown-tool/docker-compose.yml |
| md-api | (idem) | (idem) |
| airdrop.monxas.casa | pairdrop | tools/compose.yaml |
| nopor.monxas.casa | stash | tools/compose.yaml |
| japon.monxas.casa | trip-planner, trip-planner-deploy | trip-planner/docker-compose.yml |
| tunarr.monxas.casa | tunarr | media/compose.yaml |
| pinchflat.monxas.casa | pinchflat | tools/compose.yaml |
| tube.monxas.casa | metube | tools/compose.yaml |
| code.monxas.casa | code-server | tools/compose.yaml |
always-on (NO lazy):
paperless,bookmarks(karakeep),dockge— reverse_proxy directo, siempre disponibles. Eliminados del routing:ydl,gastos-app,comparador,orcaslicer,reddit-chat,open-webui.Nota (2026-04-28):
audiobookshelf(podcasts.monxas.casa) salió de Sablier por petición — siempre disponible, reverse_proxy directo. ABS conauthMethods: ["local"](sin OIDC pocketid).
Cloudflared tunnel¶
- Todos los ingress apuntan al VIP:
service: https://192.168.0.250:443+noTLSVerify+originServerName. Caddy (LXC 270/271) rutea por hostname. - Los targets cross-host (HA
.171, NAS.237, n8n.200, synology.160, RAG/Kiwix.188, etc.) se declaran en~/scripts/tunnel-static.yamly Caddy los enruta — ya no son ingress directos de cloudflared.
Caddy config (managed.caddy en LXC 270/271)¶
- Hostnames bajo el sitio único
*.monxas.casa { ... }. - Sección
# BEGIN homelab-ctl managed...# ENDregenerada porhomelab-ctl.pydesde las labelstunnel.*+tunnel-static.yaml. - 13 bloques lazy con Sablier + algunos pocos manuales legacy.
Métricas (2026-04-25)¶
- RAM antes de Sablier: 7.9 GiB used
- RAM después: 4.7 GiB used (−3.2 GiB)
- Containers running: 88 → 61 (cuando todos los lazy están durmiendo)
Cómo publicar un servicio¶
Workflow único: poner labels en el compose, ejecutar homelab-ctl sync.
services:
miapp:
image: example/app
ports:
- 8080:80
labels:
- tunnel.hostname=miapp.monxas.casa
- tunnel.lazy=true # opcional, lazy con Sablier
- tunnel.lazy.names=miapp,miapp-db # opcional, multi-container
- tunnel.lazy.display_name=Mi App # opcional
# tunnel.port=8080 # opcional (default = primer puerto publicado)
# tunnel.target_port=80 # opcional: puerto INTERNO del container (si difiere del publicado)
# tunnel.lazy.theme=shuffle # opcional (ghost/hacker-terminal/matrix/shuffle)
# tunnel.lazy.session_duration=30m # opcional
cd <stack>
docker compose up -d
python3 ~/scripts/homelab-ctl.py status # ver cambios / drift
python3 ~/scripts/homelab-ctl.py sync # aplicar (managed.caddy + tunnel + reload)
sync es idempotente. Es seguro correrlo cuantas veces quieras.
Cuándo SÍ marcar como lazy (tunnel.lazy=true)¶
- UI-driven con uso esporádico (PDF, OCR, dashboards puntuales).
- Container con RAM alta (>100 MiB) y arranque rápido (<30s).
- No es dependencia de otro always-on.
Cuándo NO¶
- Tráfico continuo (scrapers, notification listeners, eink pingers, smart TVs).
- DB compartida sola (postgres/redis/meilisearch) — solo dormir junto al stack que la usa via
tunnel.lazy.names. - Infra: caddy, cloudflared, sablier, prometheus, loki, grafana, healthchecks, monitoring agents.
- Streaming (jellyfin) o uploads móvil aleatorios (immich).
Multi-hostname para el mismo container¶
Comma-sep: tunnel.hostname=admin.monxas.casa,login.monxas.casa,wp-admin.monxas.casa (ej. krawl honeypot).
Comandos del script¶
python3 ~/scripts/homelab-ctl.py status # rutas deseadas + tunnel drift + estado
python3 ~/scripts/homelab-ctl.py sync # aplicar managed.caddy + tunnel + reload (pregunta)
python3 ~/scripts/homelab-ctl.py sync --yes # aplicar sin pregunta
python3 ~/scripts/homelab-ctl.py watch # recrea containers ausentes que deberían existir
python3 ~/scripts/homelab-ctl.py update # pull imágenes de servicios ruteados + recrea stale
python3 ~/scripts/homelab-ctl.py smoke # HEAD a cada hostname ruteado; reporta 5xx/timeouts
python3 ~/scripts/homelab-ctl.py remove HOST # quita HOST del ingress del tunnel
Caveats / gotchas conocidos¶
-
caddy reloadcachea el plugin Sablier: cuando se añade un bloque sablier nuevo (no presente al iniciar), hace faltadocker compose down && up -ddel stack reverse-proxy. El script ya hace este restart automáticamente cuando detecta sablier blocks nuevos. Editar bloques existentes sí funciona con reload. -
Movistar nullroutea Cloudflare R2 (
172.64.0.0/13):docker pullde imágenes nuevas que viven en R2 falla con TCP timeout al CDN. Workaround para Caddy: bajar binario precompilado víacaddyserver.com/api/downloady empaquetar enFROM caddy:2-alpine. Workaround general para imágenes que pullea: comprobar si están cacheadas localmente; si no, esperar a desbloqueo de R2 o usar imagen alternativa. -
El tunnel route DEBE apuntar a Caddy: si en cloudflared algún ingress apunta directo al container (e.g.,
http://192.168.0.208:8088), cuando el container está parado da 502 antes de que Sablier intercepte. Por eso unificamos al VIPhttps://192.168.0.250:443(todos pasan por Caddy). -
Cloudflare Access: hostnames protegidos con Access (e.g.,
pdf.monxas.casa) interceptan ANTES de cloudflared. Si no tienes sesión Access, ves login en lugar de la waiting page de Sablier. La waiting page aparece después del auth. -
Acceso por IP:puerto salta Sablier: si bookmarkás
192.168.0.208:8000para paperless, el container parado da 502. Para que Sablier los resucite, accede siempre por hostname. -
docker compose up -drecrea containers cuando detecta cambios en compose (incluido un label nuevo). Eso interrumpe momentáneamente sesiones activas (jellyfin streaming, etc.). Si añades labels en horario de uso intensivo, plantearlo. -
Containers parados no se ven por homelab-ctl: el script lee labels de containers running (
docker ps). Si el container tiene labels en compose pero está parado, no se reconcilia. Se resuelve la próxima vez que el container arranque (e.g., al subir el stack). -
Servicios lazy NO deben estar en blackbox probes: blackbox-exporter en su job de Prometheus. Si pones un servicio lazy y blackbox lo sondea cada minuto, todas las requests despertarán al container Y/O dispararán alerts "Probe endpoint caido" cuando duerme. Quita el target del prometheus.yml + prometheus.
-
Container ausente vs container parado (incidente 2026-04-28): Sablier sólo hace
docker start <name>. Si el container fue borrado de Docker (no sólo parado), Sablier devuelve(0/0)y la wait-page cuelga indefinidamente. Diagnóstico rápido:docker exec caddy wget -qO- "http://sablier:10000/api/strategies/dynamic?names=<n>&session_duration=30m" | grep -E 'is (ready|starting|not-ready)'. Si dice(0/0), el container no existe — recrear condocker compose -f <stack>/compose.yaml create <svc>(no arranca el container, sólo lo crea para que Sablier pueda despertarlo). -
Watchdog automático (
homelab-ctl.py watch, cron*/5 * * * *; fusiona el antiguosablier-watchdog.py): parsea losnamesde cada bloquesablier{}demanaged.caddy, comprueba que cada container exista en Docker (VM 208), y si falta lo recrea condocker compose create. Idempotente.homelab-ctl.py updateademás hace pull + recrea imágenes stale de los servicios ruteados. -
NUNCA
docker compose downen stacks con servicios lazy (incidente 2026-04-28, causa raíz):downborra containers; Sablier necesita que existan (aunque parados) para despertarlos. Si necesitas parar todo, usadocker compose stop(no borra). Para parar uno:docker stop <name>. El watchdog (homelab-ctl.py watch) recrea automáticamente containers ausentes cada 5 min, pero el spike de wait-pages colgadas hasta entonces es feo.
Rollback¶
Snapshot Proxmox de la VM 208 antes de la cirugía Sablier inicial: pre_sablier_20260425_1521.
ssh [email protected] "qm rollback 208 pre_sablier_20260425_1521"
Backups del Caddyfile generados por el script: Caddyfile.bak-<timestamp> en reverse-proxy/.
Backups de la config de tunnel API: /tmp/cf-tunnel-pre-unify-*.json en VM 208.
Reference¶
- Doc del script: ../operations/scripts/tunnel-sync.md
- Sablier docs: https://sablierapp.dev/
- Caddy plugin: github.com/sablierapp/sablier/plugins/caddy
- Build Caddy custom (con plugins, vía API oficial):
https://caddyserver.com/api/download?os=linux&arch=amd64&p=github.com%2Fcaddy-dns%2Fcloudflare&p=github.com%2Fsablierapp%2Fsablier%2Fplugins%2Fcaddy