Problema
En muchos homelabs los servicios expuestos al exterior se acceden mediante un subdominio gestionado por un proveedor DNS externo (por ejemplo app.my.domain). Cuando el mismo subdominio se resuelve dentro de la red local, el tráfico suele salir a Internet y volver a entrar, lo que genera latencia innecesaria y, sobre todo, problemas de TLS: el certificado emitido para la IP pública no coincide con la IP interna y aplicaciones como Jellyfin rechazan la conexión. El patrón típico es:
- Un reverse proxy (Traefik) escucha en 80/443 en la LAN.
- Un DNS recursivo interno (AdGuard, Pi‑hole, etc.) devuelve la IP local para
*.app.my.domain. - Los contenedores están en la misma red Docker que Traefik y usan etiquetas para la ruta.
- El certificado se gestiona con ACME a través del DNS del proveedor externo (Hetzner, Cloudflare, etc.).
El síntoma más frecuente es “HTTPS error: certificado no válido” o “la aplicación no reconoce la URL”. La raíz del problema suele estar en la forma en que se solicitan los certificados y en la separación entre DNS externo e interno.
Causa
-
Desacuerdo entre DNS y ACME
El desafío DNS‑01 necesita que el registro_acme-challengeapunte al proveedor que controla la zona pública. Si el dominio está delegada a un servicio externo (Pangolin) y el proxy interno intenta resolverlo con el DNS local, el cliente ACME no puede validar el desafío y no genera el certificado. -
Resolución interna que evita el proxy TLS
Cuando el cliente interno resuelvepaperless.app.my.domaina192.168.0.252, la conexión se establece directamente con el contenedor sin pasar por Traefik. Las etiquetas de Traefik (tls: true) quedan sin efecto y el contenedor responde sin TLS, provocando que la aplicación reclame “no HTTPS”. -
Puertos publicados solo en la LAN
Si el contenedor expone el puerto 8000 directamente y el DNS interno apunta a la IP del host, el tráfico bypassa el entrypointwebsecurede Traefik. El certificado se sirve solo en el puerto 443 del proxy, no en el puerto interno del contenedor. -
Configuración de red Docker aislada
Unmacvlanpara AdGuard funciona, pero si Traefik y los servicios están en una red bridge distinta, la resolución de nombres internos puede colisionar con la IP externa del mismo subdominio, generando rutas ambiguas.
Solución
La solución consiste en unificar la resolución DNS y centralizar la gestión de certificados en Traefik, de modo que tanto el tráfico interno como el externo pase siempre por el mismo reverse proxy. El flujo recomendado:
-
Mantener la zona DNS pública en el proveedor original (Hetzner, Cloudflare, etc.) y crear los registros
A/CNAMEque apunten a la IP pública del router. -
Configurar un DNS recursivo interno que haga “split‑horizon”: para
*.app.my.domaindevuelve la IP local del host (192.168.0.252), pero para_acme-challenge.*delega a los servidores autoritativos públicos. En AdGuard esto se logra con una regla “Conditional Forwarding” que envía los sub‑dominios_acme-challengeal resolutor público. -
Habilitar el provider DNS de Traefik que apunte al mismo proveedor que gestiona la zona pública. Por ejemplo, si la zona está en Hetzner, usar
providers.dnsChallenge.provider = "hetzner"y proporcionar la API‑key. De esta forma Traefik puede completar el desafío DNS‑01 sin interferir con la resolución interna. -
Forzar que todo el tráfico interno use el entrypoint
websecure. En Docker‑compose, no exponer puertos internos de los servicios; solo exponer los puertos del proxy (80/443). Cada contenedor debe escuchar en su puerto interno (ej. 8000) y confiar en Traefik para TLS. -
Etiquetas de Traefik – ejemplo mínimo:
labels: - "traefik.enable=true" - "traefik.http.routers.paperless.rule=Host(`paperless.app.my.domain`)" - "traefik.http.routers.paperless.entrypoints=websecure" - "traefik.http.routers.paperless.tls=true" - "traefik.http.services.paperless.loadbalancer.server.port=8000" -
Configurar el static config de Traefik para que el entrypoint
websecureuse TLS con ACME y el provider DNS correcto:# see Código section -
Re‑generar los certificados: eliminar los certificados antiguos (
/letsencrypt) y reiniciar Traefik. El primer request ahttps://paperless.app.my.domaindisparará el desafío DNS‑01 y creará un certificado válido tanto para la IP pública como para la interna (el certificado no depende de la IP, sino del nombre).
Con este enfoque, la resolución interna sigue devolviendo la IP local, pero el desafío ACME se envía a los servidores públicos, evitando el “loop” de validación. Todas las aplicaciones reciben HTTPS directamente del proxy, sin necesidad de exponer puertos internos.
Cuándo aplicar esta solución
- Homelabs con acceso externo y dominio propio gestionado por un DNS externo.
- Servicios que requieren HTTPS estricto (Jellyfin, Paperless, Nextcloud, etc.).
- Redes Docker con varios contenedores que comparten un único reverse proxy.
- Entornos donde el DNS interno es administrado por AdGuard, Pi‑hole o similar y se necesita split‑horizon.
No es necesario si:
- Solo se accede a los servicios desde la LAN y no se necesita certificado externo.
- Se usa un único proveedor DNS que permite crear registros
Aapuntando a la IP local sin necesidad de split‑horizon.
Código
# traefik.yml (static configuration)
log:
level: INFO
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
certificatesResolvers:
dns_hetzner:
acme:
email: "[email protected]"
storage: "/letsencrypt/acme.json"
dnsChallenge:
provider: "hetzner"
# opcional: delayBeforeCheck: 0
# docker-compose.yml (fragmento relevante)
services:
traefik:
image: traefik:v2.11
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "[email protected]"
- "--certificatesresolvers.dns_hetzner.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.dns_hetzner.acme.dnschallenge.provider=hetzner"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./letsencrypt:/letsencrypt"
networks:
- proxy
paperless:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.http.routers.paperless.rule=Host(`paperless.app.my.domain`)"
- "traefik.http.routers.paperless.entrypoints=websecure"
- "traefik.http.routers.paperless.tls=true"
- "traefik.http.services.paperless.loadbalancer.server.port=8000"
networks:
- proxy
networks:
proxy:
external: true
Verificación
-
Comprobar resolución DNS interna
dig @127.0.0.1 paperless.app.my.domain +short # debe devolver 192.168.0.252 dig @127.0.0.1 _acme-challenge.paperless.app.my.domain +short # debe devolver la IP pública del resolutor DNS del proveedor (p.e. 1.1.1.1) -
Validar certificado
openssl s_client -connect paperless.app.my.domain:443 -servername paperless.app.my.domain </dev/null | openssl x509 -noout -dates -subject # La fecha de expiración debe ser futura y el CN debe coincidir con paperless.app.my.domain -
Acceso desde la LAN
Abrirhttps://paperless.app.my.domainen un navegador dentro de la red. No debe aparecer advertencia de certificado y la página debe cargar rápidamente (sin salto a la IP pública). -
Acceso desde fuera
Repetir el paso anterior desde una conexión móvil o VPN externa. El mismo certificado debe ser válido y la latencia debe ser comparable a la del acceso externo directo. -
Logs de Traefik
Revisardocker logs traefikpara asegurarse de que el resolver DNS‑01 se completó sin errores y que los routers están “UP”.
Notas adicionales
- TTL bajo en los registros
_acme-challenge: Hetzner permite TTL de 60 s, lo que acelera la renovación automática. - AdGuard split‑horizon: la regla “Conditional Forwarding” debe apuntar a los servidores DNS del proveedor (p.e.
ns1.hetzner.com). De lo contrario, el desafío fallará porque AdGuard intentará resolver_acme-challengeinternamente. - No exponer puertos internos: si algún contenedor necesita acceso directo (por ejemplo, una base de datos), colócalo en una red separada y usa
network_mode: "service:traefik"solo para los servicios HTTP. - Renovación: Traefik renueva automáticamente los certificados 30 días antes de su expiración. Mantén el volumen
/letsencryptpersistente para evitar perder el estado. - Fallback HTTP → HTTPS: añade una redirección en el router
webpara forzar HTTPS y evitar que alguna aplicación caiga en HTTP por error de configuración.
Con esta arquitectura, el mismo nombre DNS funciona sin problemas tanto dentro como fuera de la red, los certificados se gestionan de forma automática y las aplicaciones reciben siempre una conexión segura.