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
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: falseobliga a habilitar explícitamente cada contenedor con etiquetas.- El
certificatesResolvers.letsencryptusa el reto HTTP en el puerto 80 (necesita que el dominio resuelva al servidor). httpChallenge.entryPoint: webindica que el desafío se sirve por el entrypointweb.
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: falsepara evitar exponer contenedores accidentalmente. - Restringe el acceso al socket de Docker: solo el contenedor Traefik lo monta en modo lectura (
:ro). - Usa la red
proxyexterna para aislar el proxy de las redes internas de cada stack. - Habilita logs en
INFO, noDEBUGen 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
basicautho 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.