Traefik vs Caddy vs Nginx: reverse proxy Docker comparado
Tres stacks Docker Compose funcionales para Traefik, Caddy y Nginx como reverse proxy en un VPS. Mismo backend, benchmarks reales y un marco de decisión para elegir el adecuado.
Tienes contenedores Docker corriendo en un VPS. Necesitas HTTPS, enrutamiento por nombre de dominio y un único punto de entrada. Traefik, Caddy y Nginx resuelven este problema. Lo resuelven de forma diferente.
Este artículo te da tres stacks Docker Compose funcionales que despliegan el mismo backend detrás de cada proxy. Copia el que se ajuste a tu situación. La tabla comparativa y el marco de decisión al final te ayudan a elegir.
Todos los ejemplos usan una red proxy dedicada, redirección HTTP a HTTPS y valores por defecto listos para producción. Los stacks están orientados a Ubuntu 24.04 en un VPS de Virtua Cloud.
¿Qué hace un reverse proxy para contenedores Docker en un VPS?
Un reverse proxy se sitúa entre internet y tus contenedores Docker. Termina TLS (HTTPS), enruta las peticiones al contenedor correcto según el hostname y expone un solo par de puertos (80/443) en lugar de un puerto por servicio. Tus contenedores nunca gestionan certificados ni se vinculan directamente a puertos públicos.
Sin un reverse proxy, cada contenedor necesitaría su propio puerto público. Los visitantes accederían a example.com:3000 para un servicio y example.com:8080 para otro. Un reverse proxy permite usar app.example.com y api.example.com en puertos estándar.
Los tres stacks asumen:
- Docker y Docker Compose están instalados Docker en producción en un VPS: qué falla y cómo solucionarlo
- Un registro DNS A apunta tu dominio a la IP del VPS
- Los puertos 80 y 443 están abiertos en tu firewall Docker ignora UFW: 4 soluciones probadas para tu VPS
- Tienes acceso SSH como usuario no-root con sudo
Cada ejemplo despliega el mismo backend: la imagen traefik/whoami, que devuelve cabeceras HTTP e información del contenedor. Sustitúyela por tu aplicación real después.
¿Cómo se configura Traefik como reverse proxy Docker con HTTPS automático?
Traefik descubre contenedores automáticamente leyendo labels de Docker. Añades las reglas de enrutamiento como labels en cada servicio. Cuando un contenedor arranca, Traefik lo detecta, solicita un certificado de Let's Encrypt y empieza a enrutar tráfico. No hace falta recargar la configuración.
Crea el directorio del proyecto:
mkdir -p ~/traefik-proxy && cd ~/traefik-proxy
Crea la red Docker que compartirán todos los servicios con proxy:
docker network create proxy
Crea docker-compose.yml:
services:
traefik:
image: traefik:v3.6
container_name: traefik
restart: unless-stopped
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=proxy"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--certificatesresolvers.letsencrypt.acme.email=you@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/acme.json"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "--log.level=WARN"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./acme.json:/acme.json
networks:
- proxy
security_opt:
- no-new-privileges:true
whoami:
image: traefik/whoami
container_name: whoami
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`app.example.com`)"
- "traefik.http.routers.whoami.entrypoints=websecure"
- "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
networks:
- proxy
networks:
proxy:
external: true
Antes de arrancar, crea el archivo de almacenamiento de certificados con permisos restringidos:
touch acme.json && chmod 600 acme.json
Traefik se niega a arrancar si acme.json tiene permisos demasiado abiertos. El 600 asegura que solo el propietario pueda leer las claves privadas almacenadas dentro.
Arranca el stack:
docker compose up -d
Comprueba que ambos contenedores están corriendo:
docker compose ps
Tanto traefik como whoami deben mostrar estado Up. Ahora prueba desde tu máquina local (no el servidor):
curl https://app.example.com
La respuesta contiene la salida de whoami con las cabeceras de la petición. La cabecera X-Forwarded-For en la respuesta te indica que Traefik está reenviando el tráfico y terminando TLS.
Qué hacen los labels:
traefik.enable=trueactiva este contenedor (ya queexposedbydefault=falseestá definido)traefik.http.routers.whoami.rule=Host(...)filtra peticiones por hostnametraefik.http.routers.whoami.tls.certresolver=letsencryptindica a Traefik que obtenga un certificado para este dominio
Para añadir otro servicio, agrégalo en cualquier archivo Compose en la misma red proxy con los labels correspondientes. Traefik lo detecta automáticamente.
¿Es seguro montar el socket de Docker en Traefik?
Montar /var/run/docker.sock da a Traefik acceso completo a la API de Docker. Si un atacante compromete Traefik, puede crear contenedores, leer variables de entorno (incluidos secrets) y escalar a root en el host. El flag :ro solo evita escrituras a nivel de sistema de archivos. No restringe las llamadas a la API de Docker.
En producción, usa un proxy de socket Docker. Se sitúa entre Traefik y el daemon de Docker, filtrando las llamadas API para permitir solo operaciones de lectura en los metadatos de contenedores.
Añade esto a tu docker-compose.yml:
services:
socket-proxy:
image: tecnativa/docker-socket-proxy:0.4
container_name: socket-proxy
restart: unless-stopped
environment:
CONTAINERS: 1
NETWORKS: 1
SERVICES: 0
TASKS: 0
POST: 0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- socket-proxy
security_opt:
- no-new-privileges:true
traefik:
image: traefik:v3.6
container_name: traefik
restart: unless-stopped
depends_on:
- socket-proxy
command:
- "--providers.docker.endpoint=tcp://socket-proxy:2375"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=proxy"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--certificatesresolvers.letsencrypt.acme.email=you@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/acme.json"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "--log.level=WARN"
ports:
- "80:80"
- "443:443"
volumes:
- ./acme.json:/acme.json
networks:
- proxy
- socket-proxy
security_opt:
- no-new-privileges:true
networks:
proxy:
external: true
socket-proxy:
driver: bridge
internal: true
Traefik ya no monta el socket de Docker directamente. La red socket-proxy es internal: true, lo que significa que no tiene acceso a internet saliente. El proxy de socket solo permite peticiones GET a los endpoints de containers y networks.
¿Cómo se configura Caddy como reverse proxy Docker con HTTPS automático?
Caddy gestiona HTTPS automáticamente sin más configuración que el nombre de dominio. Apunta un dominio a tu servidor, ponlo en el Caddyfile y Caddy obtiene y renueva los certificados de Let's Encrypt. Sin configuración de resolver, sin ajustes ACME. Es el camino más corto hacia HTTPS para un reverse proxy Docker.
Crea el directorio del proyecto:
mkdir -p ~/caddy-proxy && cd ~/caddy-proxy
Crea la red proxy compartida (omite este paso si ya la creaste para Traefik):
docker network create proxy
Crea el Caddyfile:
app.example.com {
reverse_proxy whoami:80
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
Esa es toda la configuración del proxy. Caddy lee el nombre de dominio, solicita un certificado y reenvía al contenedor whoami en el puerto 80. Sin resolver de certificados, sin email ACME (Caddy usa el predeterminado de tu máquina, o puedes definirlo globalmente), sin rutas de almacenamiento que gestionar.
Crea docker-compose.yml:
services:
caddy:
image: caddy:2.11
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- proxy
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
whoami:
image: traefik/whoami
container_name: whoami
restart: unless-stopped
networks:
- proxy
networks:
proxy:
external: true
volumes:
caddy_data:
caddy_config:
El puerto 443:443/udp habilita HTTP/3 (QUIC), que Caddy soporta de forma nativa. cap_drop: ALL con cap_add: NET_BIND_SERVICE elimina todas las capabilities de Linux excepto la necesaria para vincular puertos por debajo de 1024.
Arranca el stack:
docker compose up -d
Comprueba el estado de los contenedores:
docker compose ps
Ambos contenedores deben mostrar Up. Prueba desde tu máquina local con salida detallada:
curl -v https://app.example.com
Busca HTTP/2 200 en la salida. También deberías ver las cabeceras de seguridad del Caddyfile (Strict-Transport-Security, X-Content-Type-Options, etc.).
Para añadir otro servicio, añade un nuevo bloque en el Caddyfile con el dominio y la directiva reverse_proxy, luego recarga:
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
No hace falta reiniciar el contenedor. Caddy no necesita el socket de Docker. No descubre contenedores automáticamente. Tú gestionas el enrutamiento en el Caddyfile.
¿Cómo se configura Nginx como reverse proxy Docker con Let's Encrypt?
Nginx te da control total sobre cada directiva de proxy, cabecera, tamaño de buffer y regla de caché. La contrapartida es configuración manual. Nginx no obtiene certificados TLS por sí solo. Lo combinas con Certbot, que gestiona los desafíos ACME y la renovación de certificados.
Crea el directorio del proyecto:
mkdir -p ~/nginx-proxy && cd ~/nginx-proxy
Crea la red proxy compartida:
docker network create proxy
Crea la configuración de Nginx en nginx/conf.d/app.conf:
mkdir -p nginx/conf.d
server {
listen 80;
server_name app.example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
http2 on;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers off;
server_tokens off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
proxy_pass http://whoami:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server_tokens off; oculta la versión de Nginx en las cabeceras de respuesta. Revelar la versión ayuda a los atacantes a atacar vulnerabilidades conocidas.
Crea docker-compose.yml:
services:
nginx:
image: nginx:1.28
container_name: nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- certbot_webroot:/var/www/certbot:ro
- certbot_certs:/etc/letsencrypt:ro
networks:
- proxy
depends_on:
- whoami
certbot:
image: certbot/certbot
container_name: certbot
restart: unless-stopped
volumes:
- certbot_webroot:/var/www/certbot
- certbot_certs:/etc/letsencrypt
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
whoami:
image: traefik/whoami
container_name: whoami
restart: unless-stopped
networks:
- proxy
networks:
proxy:
external: true
volumes:
certbot_webroot:
certbot_certs:
Nginx requiere que los archivos de certificado existan antes de arrancar. La configuración de arriba referencia /etc/letsencrypt/live/app.example.com/fullchain.pem, que aún no existe. Para el certificado inicial, sustituye temporalmente app.conf por una versión solo HTTP:
server {
listen 80;
server_name app.example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
Arranca Nginx y el backend:
docker compose up -d nginx whoami
Solicita el certificado inicial:
docker compose run --rm certbot certonly \
--webroot \
--webroot-path=/var/www/certbot \
-d app.example.com \
--email you@example.com \
--agree-tos \
--no-eff-email
Una vez obtenido el certificado, restaura el app.conf completo (la versión con el bloque de servidor SSL mostrado arriba) y levanta el stack completo:
docker compose up -d
Comprueba que todos los contenedores están corriendo:
docker compose ps
Prueba desde tu máquina local:
curl -v https://app.example.com
La cabecera de respuesta server: debería mostrar nginx sin número de versión, lo que confirma que server_tokens off está activo.
Para añadir otro servicio, crea un nuevo archivo .conf en nginx/conf.d/ y recarga:
docker compose exec nginx nginx -s reload
Para la renovación de certificados, el contenedor Certbot ejecuta certbot renew cada 12 horas. Tras la renovación, recarga Nginx para aplicar los nuevos certificados. Automatiza esto con un cron job o un script que compruebe las fechas de modificación de los certificados. Para profundizar en la configuración de Nginx como reverse proxy, consulta Cómo configurar Nginx como reverse proxy.
¿Cómo se comparan Traefik, Caddy y Nginx para reverse proxying con Docker?
Traefik gana en autodescubrimiento. Caddy gana en simplicidad. Nginx gana en control. La tabla siguiente desglosa las ventajas y desventajas que importan al ejecutar contenedores Docker en un VPS.
| Característica | Traefik v3 | Caddy 2.11 | Nginx 1.28 |
|---|---|---|---|
| Autodescubrimiento | Sí (labels Docker) | No (Caddyfile manual) | No (archivos conf manuales) |
| Automatización TLS | ACME integrado | ACME integrado | Requiere sidecar Certbot |
| Método de configuración | Labels Docker + YAML/CLI estático | Caddyfile o API JSON | Archivos nginx.conf |
| Recarga de config | Automática en eventos de contenedor | caddy reload (sin interrupción) |
nginx -s reload (sin interrupción) |
| Socket Docker requerido | Sí (o proxy de socket) | No | No |
| HTTP/3 (QUIC) | Experimental | Sí (por defecto) | Mediante módulo de terceros |
| Middleware/plugins | Integrado (rate limit, auth, headers) | Integrado + plugins Go | Mediante directivas de config |
| Comunidad/docs | Grande, activa, buena documentación | Más pequeña, documentación excelente | La más grande, documentación extensa |
| Curva de aprendizaje | Media (labels + config estática) | Baja (Caddyfile es intuitivo) | Alta (muchas directivas) |
¿Qué reverse proxy consume menos memoria?
El consumo de memoria en reposo importa en un VPS donde cada megabyte cuenta. Estas cifras provienen de docker stats --no-stream en un VPS Virtua Cloud con 4 vCPU / 8 GB de RAM con Ubuntu 24.04. Cada proxy corrió en reposo sin tráfico antes de la medición.
| Proxy | RAM en reposo | Tamaño de imagen |
|---|---|---|
| Traefik v3.6 | ~17 MB | ~242 MB |
| Caddy 2.11 | ~14 MB | ~88 MB |
| Nginx 1.28 | ~5 MB | ~240 MB |
| Nginx + Certbot | ~5 MB + ~25 MB | ~240 MB + ~298 MB |
Nginx consume con diferencia menos memoria. Caddy se sitúa en el medio. El mayor consumo de Traefik viene de mantener en memoria el estado del proveedor Docker y la tabla de enrutamiento. Los tres usan las imágenes por defecto (basadas en Debian/Alpine). Las variantes Alpine reducirían el tamaño de las imágenes a costa de posibles problemas de compatibilidad con ciertas extensiones.
Bajo carga ligera (100 peticiones concurrentes con wrk), los tres manejan el tráfico sin aumento significativo de CPU o memoria en este tamaño de VPS. Las diferencias solo importan a gran escala o en los planes VPS más pequeños.
¿Cómo elegir el reverse proxy adecuado para tu configuración Docker?
La elección correcta depende de cuántos servicios ejecutas, con qué frecuencia cambian y lo que ya conoces.
Elige Traefik cuando:
- Ejecutas muchos contenedores que cambian con frecuencia (añadir/eliminar servicios semanalmente)
- Quieres enrutamiento sin intervención: despliega un contenedor con labels y está en línea
- Usas Docker Swarm o necesitas descubrimiento de servicios en múltiples nodos
- Aceptas la exposición del socket Docker (con un proxy de socket en producción)
Elige Caddy cuando:
- Ejecutas pocos servicios que cambian raramente
- Quieres el camino más sencillo hacia HTTPS automático
- No quieres montar el socket de Docker
- Valoras una imagen pequeña y bajo consumo de memoria
- Quieres soporte HTTP/3 sin configuración adicional
Elige Nginx cuando:
- Ya conoces la configuración de Nginx
- Necesitas control detallado sobre el comportamiento del proxy (buffers, caché, cabeceras personalizadas por location)
- Quieres el menor consumo de memoria posible
- Tu equipo de infraestructura tiene herramientas y monitoreo Nginx existentes
- No te importa gestionar Certbot por separado
Árbol de decisión:
- ¿Ejecutas más de 5 servicios Docker que cambian regularmente? Sí -> Traefik
- ¿Necesitas ajuste fino del proxy o ya usas Nginx? Sí -> Nginx
- ¿Quieres el menor número de piezas móviles y la configuración más rápida? Sí -> Caddy
Para la mayoría de indie hackers desplegando uno o dos proyectos personales, Caddy es el mejor punto de partida. Para equipos DevOps gestionando una flota de contenedores, el autodescubrimiento de Traefik se paga solo. Para equipos que ya usan Nginx en otro lugar, mantener Nginx conserva la coherencia del stack Redes Docker en un VPS: bridge, host y macvlan explicados.
Endurecimiento de seguridad para los tres proxys
Sea cual sea el proxy que elijas, aplica estas prácticas de seguridad básicas.
Cabeceras de seguridad. Los tres ejemplos anteriores incluyen HSTS, X-Content-Type-Options, X-Frame-Options y Referrer-Policy. Para Traefik, añádelas como labels de middleware:
labels:
- "traefik.http.middlewares.security-headers.headers.stsSeconds=31536000"
- "traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true"
- "traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.security-headers.headers.frameDeny=true"
- "traefik.http.routers.whoami.middlewares=security-headers"
Limitación de tasa. Traefik tiene middleware de rate limiting integrado. Caddy dispone de una directiva rate_limit como plugin. Nginx usa limit_req_zone en su configuración. La limitación de tasa protege tu backend de ataques de fuerza bruta y abuso.
Aislamiento de red Docker. Cada ejemplo usa una red proxy externa. Los servicios backend no deben estar en la red bridge por defecto. Solo los contenedores que necesitan proxy se unen a la red proxy. Los contenedores de base de datos y servicios internos permanecen en redes separadas e internas Hardening de seguridad en Docker: modo rootless, seccomp y AppArmor en un VPS.
Firewall. Solo los puertos 80 y 443 deben ser accesibles públicamente. Docker manipula iptables directamente, lo que puede saltarse las reglas de UFW. Consulta Docker ignora UFW: 4 soluciones probadas para tu VPS para la solución.
Logs. Revisa los logs del proxy cuando algo falla:
# Traefik
docker logs traefik -f
# Caddy
docker logs caddy -f
# Nginx
docker logs nginx -f
Para Traefik, establece --log.level=DEBUG temporalmente para diagnosticar problemas de enrutamiento o certificados. Para Caddy, activa la opción global debug en el Caddyfile. Para Nginx, revisa error.log dentro del contenedor en /var/log/nginx/error.log.
¿Algo salió mal?
| Síntoma | Causa probable | Solución |
|---|---|---|
| Certificado no emitido | Registro DNS A no apunta a la IP del VPS | Verifica con dig app.example.com |
| Traefik 404 en todas las rutas | Contenedor no está en la red proxy |
Comprueba docker network inspect proxy |
| Caddy "permission denied" en puerto 80 | Falta capability NET_BIND_SERVICE |
Añade cap_add: NET_BIND_SERVICE |
| Nginx "no such file" para certificado | Certbot aún no se ha ejecutado | Ejecuta certbot certonly primero |
ERR_CONNECTION_REFUSED |
Firewall bloqueando 80/443 | Comprueba ufw status o iptables -L |
Error de permisos acme.json en Traefik |
Permisos del archivo demasiado abiertos | Ejecuta chmod 600 acme.json |
| El proxy funciona en el servidor, falla externamente | Probando solo en localhost | Prueba con curl desde tu máquina local |
Para endurecimiento en producción más allá del reverse proxying, consulta Límites de recursos, healthchecks y políticas de reinicio en Docker Compose para límites de recursos y health checks en tus stacks Compose.
Copyright 2026 Virtua.Cloud. Todos los derechos reservados. Este contenido es una obra original del equipo de Virtua.Cloud. La reproducción, republicación o redistribución sin permiso escrito está prohibida.
¿Listo para probarlo?
Despliega tu propio servidor en segundos. Linux, Windows o FreeBSD.
Ver planes VPS