Traefik vs Caddy vs Nginx: reverse proxy Docker comparado

12 min de lectura·Matthieu·docker-composelets-encryptreverse-proxynginxcaddytraefikdocker|

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:

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=true activa este contenedor (ya que exposedbydefault=false está definido)
  • traefik.http.routers.whoami.rule=Host(...) filtra peticiones por hostname
  • traefik.http.routers.whoami.tls.certresolver=letsencrypt indica 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:

  1. Ejecutas muchos contenedores que cambian con frecuencia (añadir/eliminar servicios semanalmente)
  2. Quieres enrutamiento sin intervención: despliega un contenedor con labels y está en línea
  3. Usas Docker Swarm o necesitas descubrimiento de servicios en múltiples nodos
  4. Aceptas la exposición del socket Docker (con un proxy de socket en producción)

Elige Caddy cuando:

  1. Ejecutas pocos servicios que cambian raramente
  2. Quieres el camino más sencillo hacia HTTPS automático
  3. No quieres montar el socket de Docker
  4. Valoras una imagen pequeña y bajo consumo de memoria
  5. Quieres soporte HTTP/3 sin configuración adicional

Elige Nginx cuando:

  1. Ya conoces la configuración de Nginx
  2. Necesitas control detallado sobre el comportamiento del proxy (buffers, caché, cabeceras personalizadas por location)
  3. Quieres el menor consumo de memoria posible
  4. Tu equipo de infraestructura tiene herramientas y monitoreo Nginx existentes
  5. No te importa gestionar Certbot por separado

Árbol de decisión:

  1. ¿Ejecutas más de 5 servicios Docker que cambian regularmente? -> Traefik
  2. ¿Necesitas ajuste fino del proxy o ya usas Nginx? -> Nginx
  3. ¿Quieres el menor número de piezas móviles y la configuración más rápida? -> 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
Traefik vs Caddy vs Nginx: reverse proxy Docker