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 · Publicado el · 8 min de lectura

Grúas portuarias metálicas con cabinas y brazos contra un cielo nublado
Metáfora visual para balanceo de tráfico e infraestructura en servicios Foto: NoName_13 / Pixabay

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 arquitectura

Ver también