Tutorial: configurar HAProxy con Let's Encrypt y healthchecks para balanceo de tráfico entre contenedores Docker en VPS (paso a paso)
Redes & Segurança · Por Kevin Marcondes · Publicado el · 8 min de lectura
Este es el tutorial para configurar HAProxy con Let's Encrypt y healthchecks para balancear tráfico entre contenedores Docker en una VPS. Montarás la arquitectura, emitirás certificados con ACME HTTP‑01, activarás HTTPS, comprobarás la salud de los backends y automatizarás la renovación. El paso a paso usa Docker Compose y archivos listos para adaptar a tu dominio.
Visión general y requisitos previos
Vamos a crear un balanceador HAProxy en Docker, terminando TLS y repartiendo las solicitudes entre dos contenedores de aplicación. La emisión de certificados usará el desafío HTTP‑01, atendido por un contenedor dedicado al path de validación.
Requisitos prácticos:
- VPS Linux con acceso root o sudo. Abre los puertos 80 y 443 en el firewall. Esto expone el servicio públicamente.
- Un dominio apuntando a la VPS (registros A/AAAA). La propagación DNS puede tardar.
- Docker y Docker Compose instalados. Sigue esta guía si lo necesitas: instalar Docker y Docker Compose.
- Firewall configurado para permitir 80/tcp y 443/tcp. Ver: firewall UFW en Ubuntu.
- Buenas prácticas de seguridad en la VPS: SSH, actualizaciones y backups. Consulta el checklist de seguridad.
Arquitectura y puertos (qué vamos a levantar)
La solución tiene cuatro servicios: haproxy (puertos 80/443), acme (servidor estático para el desafío HTTP‑01), certbot (emisión/renovación) y dos aplicaciones de ejemplo. Todos comparten una red Docker para descubrimiento por nombre.
Flujo de tráfico:
- HTTP 80: atiende /.well-known/acme-challenge/ (ACME)
- HTTP 80: redirige todo lo demás a HTTPS
- HTTPS 443: frontend principal del sitio
- Backends: app1 y app2 en 8080 con healthcheck
Compose del proyecto: servicios, red y volúmenes
Crea un directorio de proyecto, guarda el archivo abajo como docker-compose.yml y ajústalo según necesites. Define HAProxy, un servidor estático para el desafío ACME, el contenedor certbot y dos apps de ejemplo. No olvides mapear tu dominio después en haproxy.cfg.
version: "3.8"
services:
haproxy:
image: haproxy:2.9-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
- letsencrypt:/etc/letsencrypt:ro
depends_on:
- app1
- app2
- acme
networks:
- edge
restart: unless-stopped
command: ["haproxy","-W","-db","-f","/usr/local/etc/haproxy/haproxy.cfg"]
# Servidor estático que entrega o desafio HTTP-01 do Let's Encrypt
acme:
image: nginx:alpine
networks:
- edge
volumes:
- acme-challenge:/usr/share/nginx/html:ro
restart: unless-stopped
# Container utilitário para emitir/renovar certificados
certbot:
image: certbot/certbot:latest
networks:
- edge
volumes:
- letsencrypt:/etc/letsencrypt
- acme-challenge:/var/www/certbot
entrypoint: ["sh","-c"]
command: "echo 'Use: docker compose run --rm certbot ...' && sleep infinity"
restart: unless-stopped
# Apps de exemplo (substitua pelos seus serviços)
app1:
image: hashicorp/http-echo:1.0
command: ["-text=app1","-listen=:8080"]
networks:
- edge
restart: unless-stopped
app2:
image: hashicorp/http-echo:1.0
command: ["-text=app2","-listen=:8080"]
networks:
- edge
restart: unless-stopped
networks:
edge:
volumes:
letsencrypt:
acme-challenge:
Configurar HAProxy: HTTPS, redirección y healthchecks
Crea haproxy.cfg al lado del docker-compose.yml. Sustituye EXEMPLO.COM por tu dominio. Usamos un backend para el desafío ACME, redirección HTTP→HTTPS y comprobación de salud (/healthz) en los backends. El archivo haproxy.pem se creará tras emitir el certificado.
Consejo para la primera emisión: si todavía no existe haproxy.pem, comenta temporalmente el frontend HTTPS (puerto 443), arranca sólo con el puerto 80 para atender el HTTP‑01 y, después de generar el certificado, reactiva el 443 y recrea el contenedor.
global
log stdout format raw local0
maxconn 2000
tune.ssl.default-dh-param 2048
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5s
timeout client 30s
timeout server 30s
retries 2
frontend fe_http
bind *:80
acl is_acme path_beg /.well-known/acme-challenge/
use_backend be_acme if is_acme
http-request redirect scheme https code 301 unless is_acme
frontend fe_https
bind *:443 ssl crt /etc/letsencrypt/live/EXEMPLO.COM/haproxy.pem
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
default_backend be_apps
backend be_acme
server acme acme:80 check
backend be_apps
balance roundrobin
option httpchk GET /healthz
http-check expect status 200
server app1 app1:8080 check inter 2s fall 3 rise 2
server app2 app2:8080 check inter 2s fall 3 rise 2
Emitir certificados con Let's Encrypt (ACME HTTP-01)
Con HAProxy (puerto 80 activo) y el contenedor acme en marcha, emite el certificado usando webroot. Después, genera haproxy.pem concatenando fullchain.pem y privkey.pem en el volumen compartido. Ejecuta los comandos con un usuario que tenga permiso para controlar Docker. Para entender más sobre ACME/LE, consulta también generar SSL gratis.
# 1) Ajuste seu domínio e e-mail
export DOMAIN=seu-dominio.com.br
export [email protected]
# 2) Suba o servidor do desafio ACME e o HAProxy (porta 80 ativa)
# Se for a primeira emissão, deixe a seção HTTPS comentada no haproxy.cfg
docker compose up -d acme haproxy
# 3) Emita o certificado (HTTP-01 via webroot)
docker compose run --rm certbot certonly \
--webroot -w /var/www/certbot \
-d "$DOMAIN" --email "$EMAIL" --agree-tos --no-eff-email
# 4) Gere o PEM combinado esperado pelo HAProxy (no volume LE)
docker compose run --rm certbot sh -lc \
"cat /etc/letsencrypt/live/$DOMAIN/fullchain.pem \
/etc/letsencrypt/live/$DOMAIN/privkey.pem \
> /etc/letsencrypt/live/$DOMAIN/haproxy.pem"
# 5) Recrie o HAProxy para carregar o certificado (reative o 443 se estava comentado)
docker compose up -d --no-deps --force-recreate haproxy
Verificar balanceo y healthchecks
Prueba HTTPS, el balanceo y la eliminación automática de nodos del pool. Debes ver las respuestas alternando entre app1 y app2. Si los puertos 80/443 están bloqueados en el firewall o el DNS no resuelve a la VPS, la emisión y las pruebas fallarán.
# Teste o certificado e o redirecionamento
curl -I http://$DOMAIN
curl -skI https://$DOMAIN
# Observe alternância entre backends
for i in {1..6}; do curl -sk https://$DOMAIN/; done
# Simule falha de um backend e confira alta disponibilidade
docker compose stop app1
for i in {1..4}; do curl -sk https://$DOMAIN/; done
# Volte com o backend e veja o pool normalizar
docker compose up -d app1
Renovación automática con systemd (opcional)
Automatiza la renovación con systemd timers en el host. El servicio ejecuta renew, reconstruye haproxy.pem y recrea el contenedor para aplicar el nuevo certificado. La renovación sólo ocurre cuando el certificado está cerca de expirar; ejecutarlo a diario es seguro. Ajusta la ruta de WorkingDirectory.
# /etc/systemd/system/letsencrypt-renew.service
[Unit]
Description=Renew Let's Encrypt certs via Docker Compose
Wants=network-online.target
After=network-online.target
WorkingDirectory=/opt/edge
[Service]
Type=oneshot
ExecStart=/usr/bin/docker compose -f docker-compose.yml run --rm certbot renew --webroot -w /var/www/certbot
ExecStartPost=/usr/bin/docker compose -f docker-compose.yml run --rm certbot sh -lc 'cat /etc/letsencrypt/live/seu-dominio.com.br/fullchain.pem /etc/letsencrypt/live/seu-dominio.com.br/privkey.pem > /etc/letsencrypt/live/seu-dominio.com.br/haproxy.pem'
ExecStartPost=/usr/bin/docker compose -f docker-compose.yml up -d --no-deps --force-recreate haproxy
# /etc/systemd/system/letsencrypt-renew.timer
[Unit]
Description=Run LE renew twice daily
[Timer]
OnCalendar=*-*-* 03,15:00:00
Persistent=true
[Install]
WantedBy=timers.target
Solución de problemas comunes
Si algo no funciona, revisa estos puntos antes de depurar configuraciones complejas. Muchas fallas en la emisión y el balanceo vienen de DNS, firewall y nombres de servicio incorrectos.
- DNS no apunta a la VPS o no ha propagado aún. Confirma con dig o nslookup.
- Firewall/Cloud bloqueando 80/443. Abre los puertos públicos y prueba desde fuera de la VPS.
- Ruta del certificado en haproxy.cfg no corresponde a tu dominio.
- Compose no creó la red o los nombres de servicio difieren (app1/app2/acme).
- Certificado generado, pero HAProxy no fue recreado. Reaplica el contenedor.
- Reloj de la VPS desajustado. Activa NTP para evitar fallos con ACME.
Preguntas frecuentes
¿Puedo usar varios dominios con el mismo HAProxy?
Sí. Emite certificados para cada dominio y genera un haproxy.pem por dominio. Puedes apuntar el bind a un directorio con varios .pem y usar SNI. Luego recarga HAProxy al añadir un nuevo certificado.
¿Es posible usar IPv6 con esta configuración?
Sí. Crea un registro AAAA en el DNS para el dominio y asegúrate de que los puertos 80 y 443 están abiertos en IPv6 del servidor. HAProxy puede escuchar en IPv4 e IPv6 configurando los binds apropiadamente.
¿Cómo reducir el downtime al recargar HAProxy?
Inicia HAProxy en modo master‑worker (-W) y haz reload mediante reexecución controlada. En Docker, recrear el servicio es sencillo y suele ser rápido. Para zero‑downtime, evalúa técnicas de blue‑green: ve el despliegue blue‑green.
¿Puedo usar DNS-01 en vez de HTTP-01?
Sí, si tu DNS ofrece API. Cambia el flujo de webroot por un plugin DNS del certbot. Esto permite emitir sin exponer el puerto 80, pero requiere credenciales del proveedor y configuración específica.
¿Dónde configuro reglas por path o host?
En el frontend HTTPS usa ACLs como hdr(host) y path_beg para enrutar por dominio y ruta. Crea backends dedicados y asócialos con use_backend según las ACLs. Este enfoque escala para múltiples servicios.
¿Necesitas adaptar el setup a tu escenario?
Esta guía cubre lo esencial para HAProxy, Let's Encrypt y healthchecks en Docker. Si quieres discutir ajustes para tu aplicación, stack y dominio, ponte en contacto.
Hablar sobre tu arquitecturaVer también
- Tutorial: implementar Canary releases con Traefik en Docker Compose usando headers y weighted routing para despliegues graduales de aplicaciones PHP
- Tutorial práctico: implementar mTLS entre Nginx y microservicios usando smallstep CA en Docker Compose
- Tutorial: configurar cgroup v2 y systemd ResourceControl para limitar CPU, memoria e I/O de contenedores Docker en Debian
- Tutorial: automatizar snapshots LVM y subidas incrementales a S3 con systemd timers y Restic en Debian