Caddy como reverse proxy de Docker: configuración con Compose y HTTPS automático
Ejecuta Caddy como reverse proxy de Docker: un stack de compose, una red compartida, reverse_proxy a nombres de servicio y HTTPS automático, y cuándo ayudan las etiquetas de caddy-docker-proxy.
Respuesta corta: ejecuta Caddy en su propio contenedor que publique los puertos 80 y 443, conéctalo junto con los contenedores de tus aplicaciones a una red Docker compartida y apunta `reverse_proxy` a cada nombre de servicio y su puerto interno (por ejemplo `reverse_proxy app:3000`). Caddy resuelve el nombre mediante el DNS integrado de Docker y obtiene automáticamente un certificado TLS para cada dirección de sitio.
Esta guía muestra primero el enfoque con un Caddyfile simple, luego la variante con etiquetas de caddy-docker-proxy, y explica cuándo encaja cada una en un único VPS. Si ejecutas Caddy directamente en el host en lugar de en Docker, consulta la guía básica de reverse proxy con Caddy.
Cómo encajan las piezas
Solo el contenedor de Caddy publica puertos hacia el exterior. Cada contenedor backend se une a una red bridge definida por el usuario (aquí llamada `web`) y expone su puerto solo en esa red. El servidor DNS integrado de Docker, disponible en las redes definidas por el usuario, permite que Caddy llegue a `app:3000` por nombre de servicio o de contenedor, así que nunca tienes que escribir a mano IP de contenedores que cambian cada vez que se recrean.
No publicar los backends importa más de lo que parece: Docker escribe sus propias reglas de iptables, así que un puerto publicado con `ports:` puede ser accesible desde internet aunque ufw diga que está bloqueado. Usa `expose:` o ningún mapeo de puertos para todo lo que deba quedar detrás del proxy.
| Componente | Función | ¿Publicado en el host? |
|---|---|---|
| contenedor caddy | Terminación TLS, enrutado por nombre de host, redirección de HTTP a HTTPS | Sí: 80/tcp, 443/tcp, 443/udp |
| red web | Red bridge compartida con DNS integrado para nombres de servicio | No |
| contenedores de aplicaciones | Tus servicios (apps web, APIs, paneles) | No, accesibles solo a través de Caddy |
| volumen caddy_data | Certificados, claves privadas, cuenta ACME | No, pero debe persistir |
Paso a paso: Caddyfile simple con docker compose
- 01.Apunta un registro A (y un AAAA si usas IPv6) de cada nombre de host al VPS antes de arrancar Caddy, o la emisión de certificados fallará.
- 02.Crea la red compartida una sola vez: `docker network create web`.
- 03.Crea `/opt/proxy/compose.yaml` y `/opt/proxy/Caddyfile` como se muestra abajo y ejecuta `docker compose up -d` en `/opt/proxy`.
- 04.Conecta cada stack de aplicación a la red externa `web` y arráncalo con `docker compose up -d`.
- 05.Sigue la emisión del primer certificado con `docker compose logs -f caddy` en `/opt/proxy`.
- 06.Tras editar el Caddyfile, recarga sin cortes: `docker compose exec -w /etc/caddy caddy caddy reload`.
# /opt/proxy/compose.yaml
services:
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data # certificates and ACME account keys
- caddy_config:/config
networks:
- web
volumes:
caddy_data:
caddy_config:
networks:
web:
external: trueConecta una aplicación y escribe el Caddyfile
Cada stack de aplicación declara la misma red externa. El nombre del servicio se convierte en el nombre de host al que se conecta Caddy. El volumen `caddy_data` es el que nunca debes perder: si lo borras, Caddy tendrá que volver a solicitar todos los certificados, lo que puede chocar con los límites de tasa de ACME.
# /opt/app/compose.yaml
services:
app:
image: ghcr.io/example/app:latest
restart: unless-stopped
expose:
- "3000" # reachable on the network, NOT published on the host
networks:
- web
networks:
web:
external: true
# /opt/proxy/Caddyfile
{
email [email protected]
}
app.example.com {
reverse_proxy app:3000
}
grafana.example.com {
reverse_proxy grafana:3000
}
api.example.com {
reverse_proxy api-1:8080 api-2:8080 {
lb_policy round_robin
health_uri /healthz
health_interval 10s
}
}Backends en Caddy: upstreams, balanceo de carga y health checks
En la terminología de Caddy, un backend es un upstream de `reverse_proxy`. Puedes listar varios upstreams en una sola directiva, elegir una `lb_policy` como `round_robin`, `least_conn` o `ip_hash` y añadir health checks activos con `health_uri` para que los contenedores caídos salgan de la rotación. Los checks pasivos (`fail_duration`, `max_fails`) también son útiles cuando la aplicación no tiene endpoint de salud.
Caddy añade por defecto `X-Forwarded-For`, `X-Forwarded-Proto` y `X-Forwarded-Host` a las peticiones que pasa por el proxy. Configura tu aplicación para que confíe en el rango de red del proxy, de modo que vea la IP real del cliente y genere enlaces `https://`. WebSockets y gRPC funcionan a través de `reverse_proxy` sin ajustes adicionales; para gRPC en texto plano, usa `reverse_proxy h2c://service:50051`.
Etiquetas en lugar de Caddyfile: caddy-docker-proxy
Quien busca un "Caddy proxy manager" suele querer lo que ofrece Nginx Proxy Manager: añadir un contenedor y obtener un nombre de host y un certificado sin editar un archivo central. Caddy no tiene interfaz gráfica oficial, pero el proyecto comunitario caddy-docker-proxy (github.com/lucaslorentz/caddy-docker-proxy) genera el Caddyfile a partir de las etiquetas de los contenedores y recarga Caddy cuando los contenedores arrancan o se detienen.
Necesita acceso de lectura al socket de Docker, lo que en la práctica equivale a root en el host. Móntalo en solo lectura, mantén actualizada la imagen del proxy y quédate con el Caddyfile simple si solo ejecutas unos pocos servicios.
services:
caddy:
image: lucaslorentz/caddy-docker-proxy:ci-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
environment:
- CADDY_INGRESS_NETWORKS=web
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- caddy_data:/data
networks:
- web
whoami:
image: traefik/whoami
networks:
- web
labels:
caddy: whoami.example.com
caddy.reverse_proxy: "{{upstreams 80}}"
volumes:
caddy_data:
networks:
web:
external: trueCaddyfile simple frente a caddy-docker-proxy
| Caddyfile simple | Etiquetas de caddy-docker-proxy | |
|---|---|---|
| La configuración vive en | Un único archivo que versionas en git | Etiquetas repartidas por los archivos compose |
| Acceso al socket de Docker | No necesario | Obligatorio (sensible para la seguridad) |
| Servicio nuevo | Editar el Caddyfile, ejecutar `caddy reload` | Añadir etiquetas, arrancar el contenedor |
| Imagen | Oficial `caddy:2` | Imagen comunitaria basada en Caddy |
| Depuración | Leer el archivo directamente | Revisar la configuración generada en los logs |
| Ideal para | Configuraciones de VPS pequeñas y estables | Muchos servicios efímeros o que cambian a menudo |
Ejecutarlo en un VPS
En un VPS KVM de NoctisNode tienes root completo y una IPv4 dedicada, así que publicar el 80 y el 443 desde un único contenedor de Caddy y ocultar todo lo demás es sencillo. Un Wisp (1 vCPU, 1 GB de RAM) basta para Caddy y un par de contenedores ligeros; los stacks más pesados encajan en Pulse o Umbra. Los detalles de la emisión de certificados y sus fallos están en cómo funciona el HTTPS automático de Caddy, y el reenvío de TCP en bruto o SSH en proxy TCP y SSH con Caddy. Protege el SSH del host con la guía de hardening de SSH.
Preguntas frecuentes
Q¿Cómo hago que Caddy haga de reverse proxy hacia un contenedor Docker?
APon Caddy y el contenedor en la misma red Docker definida por el usuario y usa `reverse_proxy service-name:port` en el Caddyfile. No uses `localhost`, porque dentro del contenedor de Caddy se refiere al propio Caddy.
Q¿Por qué Caddy devuelve 502 Bad Gateway para mi contenedor?
ANormalmente los contenedores no están en una red compartida, el puerto es incorrecto (usa el puerto interno del contenedor, no un puerto publicado en el host) o la aplicación solo escucha en 127.0.0.1 dentro de su contenedor. Haz que la aplicación escuche en 0.0.0.0 y revisa `docker network inspect web`.
Q¿Es seguro usar caddy-docker-proxy?
AFunciona bien, pero requiere el socket de Docker, que otorga un control equivalente a root sobre el host. Móntalo en solo lectura, mantén la imagen actualizada y evita exponer el endpoint de administración de Caddy.
Q¿Dónde guarda Caddy los certificados en Docker?
AEn `/data` dentro de la imagen oficial, así que monta ahí un volumen con nombre. Perderlo significa volver a emitir todos los certificados y posiblemente alcanzar los límites de tasa de ACME.
Q¿Existe un Caddy proxy manager con interfaz web?
ACaddy no tiene interfaz web oficial; se configura con un Caddyfile, JSON o su API de administración. Herramientas basadas en etiquetas como caddy-docker-proxy ofrecen un flujo parecido de añadir un contenedor y listo.