DevOps / Infraestructura 7 min lectura

Proxy inverso con Traefik y Let's Encrypt en Docker: guía paso a paso con renovación automática real

Aprende a exponer múltiples servicios Docker con HTTPS automático, certificados wildcard y renovación sin intervención manual, evitando configuraciones repetitivas de Nginx. Todo el stack con Docker Compose listo para producción.

Por Equipo Starbyte

Proxy inverso con Traefik y Let's Encrypt en Docker: guía paso a paso con renovación automática real

Proxy inverso con Traefik y Let's Encrypt en Docker: guía paso a paso con renovación automática real

Problema real: Tienes un VPS con Docker y varios servicios (WordPress, una API, Grafana, etc.). Quieres exponerlos por HTTPS sin instalar Nginx, sin editar archivos de configuración por cada nuevo dominio y, sobre todo, sin preocuparte por renovar certificados manualmente cada 90 días. La solución moderna y mantenible es Traefik como proxy inverso nativo para Docker.

Este post te da una implementación concreta, probada, que puedes copiar y desplegar en minutos.


Requisitos previos

  • Servidor Linux (Ubuntu 22.04/24.04) con Docker y Docker Compose v2 instalados.
  • Un dominio (o subdominios) con registros DNS tipo A apuntando a la IP pública del servidor.
  • Puertos 80 y 443 abiertos en el firewall.
  • Acceso SSH con permisos sudo.
  • (Opcional) Acceso API de tu proveedor DNS si quieres certificados wildcard (*.dominio.com).

1. Estructura de archivos

Crea el directorio del proyecto:

mkdir -p /opt/traefik
cd /opt/traefik
mkdir data
touch data/acme.json
chmod 600 data/acme.json

El archivo acme.json almacenará los certificados. El permiso 600 es obligatorio o Traefik fallará.

Estructura final:

/opt/traefik
├── docker-compose.yml
├── data/
│   ├── acme.json
│   ├── traefik.yml
│   └── config.yml (opcional, para middlewares)

2. Configuración principal de Traefik (data/traefik.yml)

Este archivo define los puntos de entrada, los resolvers de certificados y el proveedor Docker.

# data/traefik.yml
global:
  sendAnonymousUsage: false

api:
  dashboard: true
  insecure: false  # Solo accesible desde localhost o con autenticación

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
  file:
    filename: /etc/traefik/config.yml
    watch: true

certificatesResolvers:
  letsencrypt:
    acme:
      email: tu-email@dominio.com
      storage: /etc/traefik/acme.json
      httpChallenge:
        entryPoint: web
      # Para wildcard, descomenta y usa dnsChallenge con tu proveedor
      # dnsChallenge:
      #   provider: cloudflare
      #   delayBeforeCheck: 0

log:
  level: INFO

Puntos clave:

  • exposedByDefault: false obliga a habilitar explícitamente cada contenedor con etiquetas.
  • El certificatesResolvers.letsencrypt usa el reto HTTP en el puerto 80 (necesita que el dominio resuelva al servidor).
  • httpChallenge.entryPoint: web indica que el desafío se sirve por el entrypoint web.

3. Docker Compose del stack

docker-compose.yml:

version: "3.9"

services:
  traefik:
    image: traefik:v3.1
    container_name: traefik
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    networks:
      - proxy
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./data/traefik.yml:/etc/traefik/traefik.yml:ro
      - ./data/acme.json:/etc/traefik/acme.json
      - ./data/config.yml:/etc/traefik/config.yml:ro
    environment:
      - CF_API_EMAIL=${CF_API_EMAIL}   # solo si usas DNS challenge
      - CF_API_KEY=${CF_API_KEY}       # solo si usas DNS challenge
    labels:
      - "traefik.enable=true"
      # Dashboard seguro con autenticación básica
      - "traefik.http.routers.dashboard.rule=Host(`traefik.tu-dominio.com`)"
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.routers.dashboard.middlewares=auth"
      - "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$hashedpassword"
      - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"

networks:
  proxy:
    external: true

Generar hash de contraseña para el dashboard:

sudo apt install apache2-utils
htpasswd -nbB admin tu-contraseña

Copia la salida (sin el admin:) después de admin: en la etiqueta basicauth.users. Escapa los $ como $$ en Compose.

Crea la red externa:

docker network create proxy

4. Levantar Traefik

docker compose up -d

Verifica logs:

docker logs traefik -f --tail 50

Debes ver mensajes como "Starting provider *docker.Provider" y "Configuration received". El dashboard debe responder en https://traefik.tu-dominio.com con autenticación.


5. Exponer un servicio de ejemplo

Crea un docker-compose.yml para una app cualquiera (p.ej., Nginx de prueba) en otro directorio, por ejemplo /opt/apps/hello/:

version: "3"
services:
  hello:
    image: nginxdemos/hello
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.hello.rule=Host(`hello.tu-dominio.com`)"
      - "traefik.http.routers.hello.entrypoints=websecure"
      - "traefik.http.routers.hello.tls.certresolver=letsencrypt"
      - "traefik.http.services.hello.loadbalancer.server.port=80"

networks:
  proxy:
    external: true

Ejecuta docker compose up -d. En segundos Traefik detecta el contenedor, solicita el certificado y expone https://hello.tu-dominio.com.

Comprobación de certificado:

curl -vI https://hello.tu-dominio.com 2>&1 | grep -E "CN=|expire date"

6. Renovación automática real

Traefik renueva los certificados 30 días antes de la expiración. No necesitas cron ni scripts. Verifica el estado del certificado:

docker exec traefik cat /etc/traefik/acme.json | jq '.letsencrypt.Certificates[].domain.main'

La renovación se registra en logs:

msg="Renewing certificate" domain="hello.tu-dominio.com"

Problema común y solución: Si cambias el correo en traefik.yml, borra acme.json y reinicia. Los certificados se volverán a emitir (límite de 5 por semana por dominio, no abuses).


7. Errores frecuentes y soluciones

Error Causa Solución
Unable to obtain ACME certificate ... 403 El dominio no resuelve a la IP del servidor Configura el registro DNS tipo A correcto y espera propagación.
Permissions 644 for acme.json are too open Permisos incorrectos chmod 600 data/acme.json
traefik reading provided configuration: field not found Error de indentación en traefik.yml Usa yamllint o copia exactamente el YAML de esta guía.
No certificate for domain ... pero el contenedor está corriendo Falta etiqueta tls.certresolver o el router no usa websecure Revisa que el router de la app tenga la entrada tls.certresolver=letsencrypt.
Dashboard da 404 o no pide contraseña Middleware de autenticación no enlazado Verifica las etiquetas del service Traefik y que el hash se haya escapado con $$.

8. Casos prácticos de uso

8.1 Múltiples aplicaciones con middlewares comunes

Archivo data/config.yml para añadir cabeceras de seguridad globales:

http:
  middlewares:
    secHeaders:
      headers:
        frameDeny: true
        sslRedirect: true
        browserXssFilter: true
        contentTypeNosniff: true
        stsSeconds: 31536000
        stsIncludeSubdomains: true
        stsPreload: true

Luego en las etiquetas del servicio:

- "traefik.http.routers.miapp.middlewares=secHeaders@file"

8.2 Wildcard con DNS Challenge (Cloudflare)

En traefik.yml sustituye httpChallenge por:

dnsChallenge:
  provider: cloudflare
  delayBeforeCheck: 0

Variables de entorno en docker-compose.yml:

environment:
  - CF_API_EMAIL=email@cloudflare.com
  - CF_API_KEY=tu-api-key

Y en las apps usa:

- "traefik.http.routers.app.tls.domains[0].main=*.tu-dominio.com"

9. Buenas prácticas

  • Siempre habilita exposedByDefault: false para evitar exponer contenedores accidentalmente.
  • Restringe el acceso al socket de Docker: solo el contenedor Traefik lo monta en modo lectura (:ro).
  • Usa la red proxy externa para aislar el proxy de las redes internas de cada stack.
  • Habilita logs en INFO, no DEBUG en producción para no llenar disco.
  • Monitorea la expiración con un simple script que alerta si queda menos de una semana (aunque Traefik renueva automáticamente, no está de más).
  • No expongas el dashboard sin autenticación. Usa el middleware basicauth o integra OAuth con forward auth.

10. Cierre con idea clave

Traefik convierte el doloroso proceso de configurar HTTPS para cada servicio en una simple etiqueta de Docker. La renovación de certificados deja de ser una tarea pendiente para ser un automatismo transparente. Con este stack de una sola responsabilidad, ganas tiempo, reduces errores y mantienes el foco en tus aplicaciones, no en la infraestructura TLS.

Etiquetas: #docker #traefik #ssl #lets-encrypt #proxy-inverso #https #docker-compose