Tutorial: configurar HAProxy com Let's Encrypt e healthchecks para balanceamento de tráfego entre containers Docker em VPS (passo a passo)
Redes & Segurança · Por Kevin Marcondes · Publicado em · 8 min de leitura
Este é o tutorial: configurar HAProxy com Let's Encrypt e healthchecks para balanceamento de tráfego entre containers Docker em VPS. Você vai montar a arquitetura, emitir certificados com ACME HTTP-01, ativar HTTPS, checar saúde dos backends e automatizar a renovação. O passo a passo usa Docker Compose e arquivos prontos para adaptar ao seu domínio.
Visão geral e pré-requisitos
Vamos criar um balanceador HAProxy em Docker, terminando TLS e distribuindo requisições entre dois containers de aplicação. A emissão de certificados usará o desafio HTTP‑01, atendido por um container dedicado ao path de validação.
Pré-requisitos práticos:
- VPS Linux com acesso root ou sudo. Abra as portas 80 e 443 no firewall. Isso expõe o serviço publicamente.
- Um domínio apontando para a VPS (registros A/AAAA). Propagação DNS pode levar tempo.
- Docker e Docker Compose instalados. Siga este guia se precisar: instalar Docker e Docker Compose.
- Firewall configurado para liberar 80/tcp e 443/tcp. Veja: firewall UFW no Ubuntu.
- Boas práticas de segurança na VPS: SSH, atualizações e backups. Confira o checklist de segurança.
Arquitetura e portas (o que vamos subir)
A solução tem quatro serviços: haproxy (porta 80/443), acme (servidor estático para o desafio HTTP‑01), certbot (emissão/renovação) e dois apps de exemplo. Todos compartilham uma rede Docker para descoberta por nome.
Fluxo de tráfego:
- HTTP 80: atende /.well-known/acme-challenge/ (ACME)
- HTTP 80: redireciona todo o resto para HTTPS
- HTTPS 443: frontend principal do site
- Backends: app1 e app2 em 8080 com healthcheck
Compose do projeto: serviços, rede e volumes
Crie um diretório de projeto, salve o arquivo abaixo como docker-compose.yml e ajuste quando necessário. Ele define HAProxy, um servidor estático para o desafio ACME, o container do certbot e dois apps de exemplo. Não esqueça de mapear seu domínio depois no 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, redirecionamento e healthchecks
Crie haproxy.cfg ao lado do docker-compose.yml. Substitua EXEMPLO.COM pelo seu domínio. Usamos um backend para o desafio ACME, redirecionamento HTTP→HTTPS e checagem de saúde (/healthz) nos backends. O arquivo haproxy.pem será criado após emitir o certificado.
Dica para a primeira emissão: se ainda não existir o haproxy.pem, comente temporariamente o frontend HTTPS (porta 443), suba apenas com a porta 80 para atender o HTTP‑01 e, depois de gerar o certificado, reative o 443 e recrie o container.
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 com Let's Encrypt (ACME HTTP-01)
Com o HAProxy (porta 80 ativa) e o container acme no ar, emita o certificado usando webroot. Depois, gere o haproxy.pem concatenando fullchain.pem e privkey.pem no volume compartilhado. Execute os comandos com um usuário que tenha permissão para controlar o Docker. Para entender mais sobre a ACME/LE, veja também como gerar SSL grátis.
# 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 balanceamento e healthchecks
Teste o HTTPS, o balanceamento e a remoção automática de nós do pool. Você deve ver as respostas alternando entre app1 e app2. Se a porta 80/443 estiver bloqueada no firewall ou DNS não resolver para a VPS, a emissão e testes falham.
# 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
Renovação automática com systemd (opcional)
Automatize a renovação com systemd timers no host. O serviço executa o renew, reconstrói haproxy.pem e recria o container para aplicar o novo certificado. A renovação só ocorre quando o certificado está próximo do vencimento; rodar todo dia é seguro. Ajuste o caminho do 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
Solução de problemas comuns
Se algo não funcionar, verifique estes pontos antes de depurar configs complexas. Muitas falhas na emissão e no balanceamento são causadas por DNS, firewall e nomes de serviço incorretos.
- DNS não aponta para a VPS ou não propagou ainda. Confirme com dig ou nslookup.
- Firewall/Cloud bloqueando 80/443. Libere as portas públicas e teste de fora da VPS.
- Caminho do certificado no haproxy.cfg não corresponde ao seu domínio.
- Compose não criou a rede ou nomes de serviço divergentes (app1/app2/acme).
- Certificado gerado, mas HAProxy não foi recriado. Reaplique o container.
- Relógio da VPS fora de sincronia. Ative NTP para evitar falhas no ACME.
Perguntas frequentes
Posso usar vários domínios com o mesmo HAProxy?
Sim. Emita certificados para cada domínio e gere um haproxy.pem por domínio. Você pode apontar o bind para um diretório com vários .pem e usar SNI. Depois, recarregue o HAProxy ao adicionar um novo certificado.
É possível usar IPv6 com esta configuração?
Sim. Crie um registro AAAA no DNS para o domínio e garanta que as portas 80 e 443 estão abertas no IPv6 do servidor. O HAProxy pode escutar em IPv4 e IPv6 ao configurar os binds adequadamente.
Como reduzir downtime ao recarregar o HAProxy?
Inicie o HAProxy em modo master-worker (-W) e faça reload via reexecução controlada. Em Docker, recriar o serviço é simples e costuma ser rápido. Para zero‑downtime, avalie técnicas de blue‑green: veja deploy blue‑green.
Posso usar DNS-01 em vez de HTTP-01?
Pode, se seu DNS oferecer API. Troque o fluxo de webroot por um plugin DNS do certbot. Isso permite emitir sem expor a porta 80, mas exige credenciais do provedor e configuração específica.
Onde configuro regras por caminho ou host?
No frontend HTTPS, use ACLs como hdr(host) e path_beg para rotear por domínio e caminho. Crie backends dedicados e associe com use_backend conforme as ACLs. Essa abordagem escala para múltiplos serviços.
Precisa adaptar o setup ao seu cenário?
Este guia cobre o essencial para HAProxy, Let's Encrypt e healthchecks no Docker. Se você quer discutir ajustes para sua aplicação, stack e domínio, entre em contato.
Falar sobre a sua arquiteturaVeja também
- tutorial: implementar Canary releases com Traefik em Docker Compose usando headers e weight routing para deploys graduais de aplicações PHP
- Tutorial prático: implementar mTLS entre Nginx e microserviços usando smallstep CA em Docker Compose
- Tutorial: configurar cgroup v2 e systemd ResourceControl para limitar CPU, memória e I/O de containers Docker no Debian
- Tutorial: automatizar snapshots LVM e uploads incrementais para S3 usando systemd timers e Restic no Debian