El problema real: demasiados servicios y demasiados puertos
Cuando empiezas a montar servicios en Docker, todo parece sencillo: un contenedor para una aplicación, otro para la siguiente, cada uno con su puerto y listo.
El problema aparece cuando el laboratorio empieza a crecer y terminas accediendo a cada servicio de una forma diferente:
http://192.168.1.10:3001
http://192.168.1.10:8080
http://192.168.1.10:8123
http://192.168.1.10:9000
Funciona, pero no es cómodo, no escala bien y obliga a recordar direcciones y puertos. Si además necesitas acceder desde otra red o publicar algún servicio, la administración empieza a complicarse.
Para mí, un proxy inverso nunca ha sido simplemente una forma de publicar servicios. Es una manera de centralizar los accesos, utilizar nombres comprensibles, gestionar certificados y tener un punto claro desde el que decidir qué se publica y qué debe permanecer dentro de la red.
Qué necesitaba resolver
Mi objetivo era bastante concreto: quería acceder a los servicios de mi laboratorio mediante nombres fáciles de recordar, sin publicar directamente todos sus puertos y sin tener que gestionar manualmente un certificado para cada aplicación.
Buscaba una solución que me permitiera:
- Centralizar el acceso a varios servicios.
- Utilizar un dominio o subdominio para cada uno.
- Gestionar HTTPS de forma automática.
- Reducir el número de puertos expuestos en el host.
- Mantener una configuración que pudiera entender y recuperar con facilidad.
Como suelo hacer cuando una tecnología puede resolver una necesidad real, primero probé distintas opciones en mi laboratorio. No quería saber únicamente si funcionaban. Quería comprobar cuánto esfuerzo exigían para configurarlas, entenderlas y mantenerlas.
Por qué Traefik no terminó de encajar en mi caso
Antes de decidirme por Caddy probé Traefik en el laboratorio.
No tuve ningún problema grave con él y no considero que sea una mala solución. Al contrario: Traefik es potente y puede resultar especialmente interesante en entornos muy dinámicos, donde el descubrimiento automático de servicios y la integración mediante etiquetas aportan mucho valor.
En mi caso, sin embargo, me resultó más complejo de configurar y administrar de lo que necesitaba para el problema que intentaba resolver.
Mi comparación real no fue una competición para decidir qué proxy inverso era mejor. Fue una decisión de contexto.
Podía conseguir el resultado que buscaba con menos complejidad operativa, una configuración más legible y menos esfuerzo futuro de mantenimiento.
Por qué Caddy sí encajó
Cuando probé Caddy encontré una forma muy directa de definir cada proxy:
uptime.tudominio.com {
reverse_proxy uptime-kuma:3001
}
Con unas pocas líneas podía describir qué nombre debía recibir la petición y a qué servicio interno debía enviarla. Caddy se ocupaba además de automatizar la obtención y renovación de los certificados TLS cuando el dominio y la conectividad estaban correctamente configurados.
No elegí Caddy porque fuera el proxy inverso más potente ni porque Traefik no funcionara. Lo elegí porque, para mi entorno, era el que menos esfuerzo mental me exigía para mantener una infraestructura que iba creciendo con el tiempo.
Ese coste también forma parte de cualquier decisión técnica. Una tecnología no termina de costar cuando consigues instalarla. Sigue costando cada vez que tienes que entenderla, mantenerla, actualizarla o recuperar el servicio después de un problema.
Qué es un proxy inverso
Un proxy inverso recibe una petición y la dirige al servicio interno correspondiente.
Desde fuera puedes acceder mediante nombres comprensibles:
https://uptime.tudominio.com → Uptime Kuma
https://grafana.tudominio.com → Grafana
https://home.tudominio.com → Home Assistant
https://dns.tudominio.com → AdGuard Home
Por dentro, cada aplicación puede continuar escuchando en su dirección y puerto habituales. El usuario no necesita conocerlos y el servicio tampoco tiene que publicar necesariamente su puerto directamente en el host.
El patrón es sencillo:
Internet o LAN → Caddy → servicio interno
Cómo lo monto actualmente con Docker
El siguiente ejemplo utiliza una red externa denominada proxy. Así puedo conectar a ella contenedores definidos en proyectos Compose distintos.
Primero creo la red una sola vez:
docker network create proxy
Para el servicio de Caddy utilizo una estructura parecida a esta:
services:
caddy:
image: caddy:2.11.4
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- "./conf:/etc/caddy:ro"
- "caddy_data:/data"
- "caddy_config:/config"
networks:
- proxy
networks:
proxy:
external: true
volumes:
caddy_data:
caddy_config:
En este ejemplo fijo una versión concreta de la imagen en lugar de utilizar latest. Eso permite controlar cuándo se produce una actualización y comprobar antes sus posibles cambios.
La carpeta local ./conf contiene el archivo Caddyfile y se monta como directorio en /etc/caddy. Prefiero hacerlo así en lugar de montar directamente el archivo, porque determinados editores sustituyen el inodo al guardar y eso puede interferir con la recarga de la configuración dentro del contenedor.
El volumen caddy_data es especialmente importante. Caddy guarda allí certificados, claves privadas y otros datos relacionados con TLS. No debe tratarse como una caché ni eliminarse sin comprender las consecuencias.
El volumen caddy_config conserva información interna de configuración. Su persistencia no es tan crítica como la de /data, pero resulta útil al recrear o migrar el contenedor.
El puerto UDP 443 permite HTTP/3. Si no lo necesitas, puedes omitirlo. En algunos entornos puede ser necesario revisar también los tamaños de búfer UDP o la capacidad opcional NET_ADMIN.
Conectar los servicios sin publicar todos sus puertos
El servicio que quiero publicar se conecta a la misma red externa:
services:
uptime-kuma:
image: louislam/uptime-kuma:latest
container_name: uptime-kuma
restart: unless-stopped
volumes:
- "./data:/app/data"
networks:
- proxy
networks:
proxy:
external: true
No publico el puerto 3001 en el host porque Caddy puede acceder al contenedor directamente a través de la red proxy.
Esto no convierte automáticamente el despliegue en seguro, pero evita exponer puertos que no son necesarios y reduce la superficie accesible desde el host.
El matiz de localhost dentro de Docker
Este fue uno de los conceptos que más conviene entender cuando empiezas a interconectar contenedores.
En muchos tutoriales aparece una configuración como esta:
reverse_proxy localhost:8080
Puede ser correcta si Caddy se ejecuta directamente en el host y la aplicación escucha en ese mismo host.
Pero cuando Caddy está dentro de Docker, localhost apunta al propio contenedor de Caddy. No apunta al host ni a otro contenedor.
Si Caddy y la aplicación están en la misma red Docker, utilizo el nombre del servicio:
reverse_proxy nombre-del-servicio:puerto
Por ejemplo:
uptime.tudominio.com {
reverse_proxy uptime-kuma:3001
}
Entender bien esta separación entre host, contenedor y red Docker evita una buena parte de los errores iniciales.
Desplegar, validar y recargar
Levanto el servicio y reviso su estado:
docker compose up -d
docker ps
docker logs caddy
Antes de aplicar un cambio, valido la configuración:
docker compose exec -w /etc/caddy caddy caddy validate
Y, si es correcta, realizo una recarga sin tener que reiniciar completamente el servicio:
docker compose exec -w /etc/caddy caddy caddy reload
Separar la validación de la recarga es una práctica sencilla que evita aplicar configuraciones mal formadas.
DNS, puertos y HTTPS automático
Para el escenario público más habitual, el dominio debe apuntar a la dirección correcta y Caddy debe poder recibir las conexiones necesarias para completar los desafíos ACME y servir el tráfico.
Con UFW, una configuración básica sería:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status
Los puertos 80 y 443 son el camino habitual para los desafíos HTTP-01 y TLS-ALPN-01 y para servir HTTP y HTTPS. No son, sin embargo, la única posibilidad para obtener certificados. Un desafío DNS-01 permite validar el dominio mediante DNS, aunque normalmente exige configurar un proveedor DNS y, en muchos casos, utilizar el módulo correspondiente de Caddy.
En una conexión doméstica también hay que comprobar el reenvío de puertos del router, el firewall y la posible existencia de CGNAT. Caddy no puede resolver una limitación de conectividad anterior a él.
Un Caddyfile con varios servicios
{
email [email protected]
}
uptime.tudominio.com {
reverse_proxy uptime-kuma:3001
}
grafana.tudominio.com {
reverse_proxy grafana:3000
}
home.tudominio.com {
reverse_proxy 192.168.1.50:8123
}
dns.tudominio.com {
reverse_proxy adguard:80
}
El correo global no es obligatorio, pero permite asociar una dirección de contacto a la cuenta ACME.
El ejemplo mezcla destinos accesibles por nombre de servicio Docker y un destino situado en la red local. Esa combinación es válida siempre que el contenedor de Caddy pueda alcanzar las direcciones correspondientes.
Lo que no publicaría directamente
Que Caddy permita publicar un servicio con HTTPS no significa que sea buena idea hacerlo.
No expondría alegremente a Internet paneles como Grafana, Uptime Kuma, Portainer, AdGuard Home, Pi-hole, consolas de administración o servicios internos.
HTTPS cifra la conexión. No decide si el servicio debería estar publicado.
Cuando necesito acceder de forma remota a este tipo de herramientas, prefiero utilizar una VPN como WireGuard o Tailscale, restringir por dirección IP, mantener el acceso solo en la red local o añadir una capa adicional de autenticación.
Para un caso sencillo, Caddy utiliza actualmente la directiva basic_auth:
admin.tudominio.com {
basic_auth {
julio HASH_DE_LA_CONTRASEÑA
}
reverse_proxy servicio-interno:8080
}
Caddy no admite la contraseña en texto plano en esta directiva. El hash puede generarse dentro del contenedor:
docker compose exec caddy caddy hash-password
La autenticación básica tampoco convierte por sí sola una aplicación en segura. Es una capa adicional y debe utilizarse siempre sobre HTTPS.
Mi regla sigue siendo bastante sencilla:
Si no debe estar en Internet, no lo publiques en Internet.
Si necesitas acceso remoto, valora primero una VPN.
Si decides publicarlo, añade capas y entiende el riesgo.
Errores que revisaría primero
Usar localhost dentro del contenedor
Dentro de Caddy, localhost representa el propio contenedor de Caddy. Para alcanzar otro contenedor utiliza una red compartida y su nombre de servicio.
El dominio no apunta a la dirección correcta
Caddy no podrá completar la validación habitual ni servir correctamente el dominio si los registros DNS no apuntan al destino previsto.
Los puertos no llegan hasta Caddy
Hay que revisar firewall, NAT, redirección de puertos, proveedor de acceso y posible CGNAT.
Eliminar el volumen de datos
El volumen caddy_data contiene certificados, claves privadas y otros datos TLS. Debe incluirse en la estrategia de copia y migración.
Publicar un panel solo porque ya tiene HTTPS
El candado del navegador confirma que la conexión está cifrada. No confirma que la decisión de exposición sea correcta.
Actualizar sin perder el control
No utilizo una etiqueta mutable como latest para una instalación que quiero mantener de manera controlada.
Cuando decido actualizar, cambio explícitamente la versión de la imagen, descargo la nueva y recreo el servicio:
docker compose pull
docker compose up -d
docker logs caddy
Después compruebo los accesos, los certificados y los registros. Actualizar no consiste únicamente en descargar una imagen; consiste en confirmar que el servicio continúa cumpliendo su función.
Mi criterio después de utilizarlo
Caddy terminó encajando en mi laboratorio porque me permitió centralizar el acceso a los servicios con una configuración que podía leer, entender y mantener sin dedicar más tiempo del necesario.
Eso no convierte a Traefik en una mala opción. En una infraestructura más dinámica, con descubrimiento automático de servicios y una integración intensiva mediante etiquetas, podría ser precisamente la herramienta adecuada.
En mi caso pesaron más la sencillez, la legibilidad del Caddyfile y el coste futuro de mantenimiento.
Con el tiempo he aprendido que una decisión técnica no termina cuando el servicio empieza a responder. También hay que pensar quién entenderá la configuración dentro de un año, cómo se actualizará, cómo se recuperará y cuánto esfuerzo exigirá cada cambio.
No elegí Caddy porque las demás opciones no funcionaran. Lo elegí porque resolvía mi problema con la complejidad que estaba dispuesto a mantener.
Contenido relacionado
- Docker para empresa: contenedores, límites y criterio técnico
- Monitorizar servicios con Uptime Kuma
- Hardening básico de servidores Linux
- WireGuard en tu homelab
Fuentes técnicas
- Imagen oficial de Caddy en Docker Hub
- Documentación oficial de HTTPS automático en Caddy
- Directiva reverse_proxy del Caddyfile
- Directiva basic_auth del Caddyfile
¿Quieres seguir explorando?
Puedes descubrir más artículos, seguir mis publicaciones en LinkedIn o escribirme.
