Jugando con Docker: Proxy Inverso con Caddy

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

Fuentes técnicas

Continúa leyendo

Corre modelos de IA locales en tu homelab con Ollama

¿Quieres seguir explorando?

Puedes descubrir más artículos, seguir mis publicaciones en LinkedIn o escribirme.

Deja un comentario

Julio Rodas

Tecnología sin humo. Cómo pienso, pruebo y tomo decisiones tecnológicas.

Cómo trabajo  ·  Sobre mí  ·  Artículos  ·  Contacto

Invítame a un café

Aviso Legal  ·  Política de Privacidad  ·  Política de Cookies

Las opiniones expresadas en esta web son personales y no representan necesariamente la posición de mi empleador ni de ninguna organización con la que mantenga una relación profesional.

© 2026 Julio Rodas