👉 Artigo completo sobre o assunto.
Um setup de Traefik v3 com Docker Compose que serve como porta de entrada única para seus containers. Você sobe o Traefik uma vez e, a partir daí, qualquer outro projeto é exposto apenas adicionando labels — sem mexer em portas, nginx ou certificados manualmente.
O repositório traz dois ambientes prontos:
- 🚀
development/— local, HTTP + HTTPS (certificado confiável viamkcert), sem senha, logs em modo debug. - 🔒
production/— VPS com domínio, TLS automático (Let's Encrypt), redirecionamento HTTP→HTTPS e dashboard protegido.
flowchart LR
Internet([Internet / Browser]) -->|:80 / :443| Traefik
subgraph proxy-network
Traefik[Traefik v3]
Traefik --> App1[app-1]
Traefik --> App2[app-2]
Traefik --> App3[dashboard]
end
Todos os serviços conversam pela mesma rede Docker (proxy-network). Basta o container ter o label traefik.enable=true e estar nessa rede para o Traefik descobri-lo automaticamente.
| 🚀 Development | 🔒 Production | |
|---|---|---|
| Quando usar | Desenvolvimento local | VPS / servidor público |
| Protocolo | HTTP (:80) e HTTPS (:443) |
HTTPS (:443), com redirect de :80 |
| TLS / Certificado | Local, confiável, via mkcert |
Let's Encrypt automático |
| Autenticação | Nenhuma | Basic Auth no dashboard |
| Dashboard | localhost:8080 (aberto) |
Domínio próprio, atrás de auth |
| Domínio | *.localhost (sem config) |
Domínio real apontando pra VPS |
| Logs | DEBUG (texto) | Padrão |
traefik/
├── traefik.sh # script raiz — gerencia os dois ambientes
├── production/ # VPS: TLS, HTTPS, Basic Auth, security headers
│ ├── docker-compose.yml
│ ├── .env.example
│ ├── setup-traefik-prod.sh
│ └── backup-acme.sh # backup dos certificados (agendar no cron)
└── development/ # local: HTTP+HTTPS, sem auth, logs de debug
├── docker-compose.yml
├── .env.example
├── setup-traefik-dev.sh
└── traefik/
├── certs/ # certificado mkcert (git-ignored, gerado no setup)
├── dynamic/ # config dinâmica apontando pro certificado
└── dev-domains.txt # domínios .dev extras cobertos pelo certificado
Atalho para rodar docker compose no ambiente certo, de qualquer lugar do repositório:
./traefik.sh <dev|prod> [comando do docker compose]| Comando | O que faz |
|---|---|
./traefik.sh dev |
docker compose up -d em development/ |
./traefik.sh prod |
docker compose up -d em production/ |
./traefik.sh dev logs -f |
Acompanha os logs em dev |
./traefik.sh prod restart |
Reinicia a produção |
./traefik.sh prod down |
Para a produção |
Feito para uso local — sem domínio real, sem senha, mas com HTTPS de verdade via certificado local (mkcert). Comece por aqui.
# 1. Setup único (cria a rede proxy-network e o certificado TLS local — pode pedir sua senha do sudo)
bash development/setup-traefik-dev.sh
# 2. Sobe o Traefik
./traefik.sh devDashboard disponível em http://localhost:8080/dashboard/ — funciona de imediato, sem nenhuma configuração. Via traefik.local (veja abaixo), também dá pra acessar em https://traefik.local/dashboard/.
No docker-compose.yml do seu projeto, adicione os labels e conecte-o à proxy-network:
services:
my-app:
image: my-app:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.my-app.rule=Host(`myapp.localhost`)"
- "traefik.http.routers.my-app.entrypoints=web"
networks:
- proxy-network
networks:
proxy-network:
external: truePronto — acesse http://myapp.localhost. Domínios .localhost resolvem para 127.0.0.1 nativamente na maioria dos navegadores e sistemas, sem precisar editar /etc/hosts (mas é recomendado fazer essa configuração em /etc/hosts).
O Traefik de dev carrega um certificado gerado pelo mkcert (via setup-traefik-dev.sh) que cobre *.localhost, *.local, traefik.local, localhost, 127.0.0.1 e qualquer domínio .dev listado em development/traefik/dev-domains.txt — como é confiável pelo navegador/SO da sua máquina, não há avisos de certificado inválido. Para o serviço aceitar HTTPS, basta adicionar um segundo router apontando pro entrypoint websecure com tls=true — sem certresolver, o certificado é resolvido automaticamente por SNI:
labels:
- "traefik.enable=true"
# router HTTP (como já existia)
- "traefik.http.routers.my-app.rule=Host(`myapp.localhost`)"
- "traefik.http.routers.my-app.entrypoints=web"
# router HTTPS — mesmo host, outro entrypoint
- "traefik.http.routers.my-app-secure.rule=Host(`myapp.localhost`)"
- "traefik.http.routers.my-app-secure.entrypoints=websecure"
- "traefik.http.routers.my-app-secure.tls=true"Pronto — acesse https://myapp.localhost sem warnings. Se development/traefik/certs/ estiver vazio (setup nunca rodado), o websecure cai no certificado padrão (não confiável) do próprio Traefik — o HTTP em :80 continua funcionando normalmente.
Usando
myapp.localem vez de.localhost? O certificado cobre esse wildcard também, mas ele não resolve sozinho para127.0.0.1— adicione a entrada no/etc/hosts(.localtambém pode conflitar com mDNS/Bonjour em alguns sistemas).
Usando domínio .dev (ex.: myapp.dev)?
.dev não pode usar wildcard. Diferente de .localhost/.local, .dev é um TLD público de verdade (administrado pelo Google) e está na Public Suffix List — o Chrome recusa qualquer certificado wildcard emitido direto sobre um TLD público, mesmo com CA confiável localmente (*.dev poderia, em teoria, autenticar o domínio .dev de qualquer pessoa). O erro típico é net::ERR_CERT_COMMON_NAME_INVALID — e curl/openssl não reproduzem esse erro, porque só o navegador aplica essa regra.
A solução é listar cada domínio .dev explicitamente:
- Adicione o domínio (uma linha, sem wildcard) em
development/traefik/dev-domains.txt:myapp.dev - Rode
bash development/setup-traefik-dev.shde novo pra regerar o certificado - Garanta a entrada no
/etc/hosts(domínios.devtambém não resolvem sozinhos pra127.0.0.1):echo "127.0.0.1 myapp.dev" | sudo tee -a /etc/hosts
De quebra: .dev é HSTS-preloaded no navegador inteiro — HTTP puro simplesmente não funciona nesse TLD, então o router websecure/tls=true já é obrigatório ali (não é opcional como em .localhost).
Meu container não escuta na porta 80
Se o serviço escuta em outra porta (ex.: 3000), informe ao Traefik com o label loadbalancer.server.port:
labels:
- "traefik.enable=true"
- "traefik.http.routers.my-app.rule=Host(`myapp.localhost`)"
- "traefik.http.routers.my-app.entrypoints=web"
# container escuta na 3000 em vez da 80
- "traefik.http.services.my-app.loadbalancer.server.port=3000"A porta é a interna do container — ela não precisa ser publicada com ports: no compose.
Acessar o dashboard via traefik.local
Como alternativa ao localhost:8080, adicione uma entrada no /etc/hosts:
echo "127.0.0.1 traefik.local" | sudo tee -a /etc/hostsDepois acesse http://traefik.local/dashboard/ ou https://traefik.local/dashboard/ (certificado confiável via mkcert, sem avisos).
Para uma VPS Linux com domínio público. Cuida do TLS automático (Let's Encrypt), do redirecionamento HTTP→HTTPS e de um dashboard protegido por Basic Auth.
- Docker e Docker Compose v2
- Um domínio (ou subdomínio) apontando para o IP público do servidor
- Portas
80e443liberadas no firewall apache2-utils(para ohtpasswd) — o script de setup instala se faltar
# 1. Setup único (cria diretórios, acme.json, rede e credenciais)
bash production/setup-traefik-prod.sh
# 2. Edita as variáveis de ambiente
nano production/.env
# 3. Sobe o Traefik
./traefik.sh prod| Variável | Descrição | Exemplo |
|---|---|---|
ACME_EMAIL |
E-mail para avisos do Let's Encrypt | admin@example.com |
TRAEFIK_DASHBOARD_HOST |
Domínio do dashboard do Traefik | traefik.example.com |
ACME_CASERVER |
(Opcional) CA do ACME — sem definir, usa a CA de produção do Let's Encrypt | CA de staging, para testes |
Copie production/.env.example para production/.env e preencha. O .env é ignorado pelo git e nunca deve ser commitado.
O
./traefik.sh prodse recusa a subir se o.envestiver ausente, vazio ou com os valores de exemplo — assim o erro aparece na hora, e não minutos depois na emissão do certificado.
services:
my-app:
image: my-app:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.my-app.rule=Host(`app.example.com`)"
- "traefik.http.routers.my-app.entrypoints=websecure"
- "traefik.http.routers.my-app.tls.certresolver=letsencrypt"
networks:
- proxy-network
networks:
proxy-network:
external: trueSecurity headers no meu serviço
Os middlewares dashboard-* (auth, headers, rate limit) pertencem apenas ao dashboard — não os reutilize: middlewares definidos via labels do Docker são resolvidos por nome global, e compartilhar o mesmo nome entre projetos causa conflito.
Para aplicar security headers no seu serviço, defina um middleware com nome próprio nos labels dele, copiando as diretivas de production/docker-compose.yml:
labels:
- "traefik.http.routers.my-app.middlewares=my-app-security-headers@docker"
- "traefik.http.middlewares.my-app-security-headers.headers.stsSeconds=31536000"
- "traefik.http.middlewares.my-app-security-headers.headers.stsIncludeSubdomains=true"
- "traefik.http.middlewares.my-app-security-headers.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.my-app-security-headers.headers.frameDeny=true"
- "traefik.http.middlewares.my-app-security-headers.headers.referrerPolicy=strict-origin-when-cross-origin"Testar TLS sem estourar o rate limit (staging)
O Let's Encrypt tem um limite de 5 certificados duplicados por semana. Use a CA de staging durante os testes:
- Descomente
ACME_CASERVERnoproduction/.env(sem a variável, o default é sempre a CA de produção — não há como esquecer configuração de teste no compose):ACME_CASERVER=https://acme-staging-v02.api.letsencrypt.org/directory
- Esvazie
production/traefik/acme/acme.jsone reinicie - Verifique que o certificado foi emitido (será não confiável — esperado em staging)
- Comente a variável de novo, esvazie o
acme.jsonoutra vez e reinicie para obter o certificado real
Backup dos certificados (acme.json)
O acme.json guarda os certificados e as chaves privadas emitidos pelo Let's Encrypt. Perdê-lo força a reemissão de tudo — e pode esbarrar no rate limit (5 certificados duplicados por semana).
Como funciona: production/backup-acme.sh copia o acme.json para acme.json.<dia-da-semana> (1 = segunda … 7 = domingo), mantendo 7 cópias rotativas — cada dia sobrescreve a cópia da semana anterior. As cópias preservam o modo 600 e o destino (production/backups/ por padrão) é ignorado pelo git.
Rodar manualmente (ex.: antes de mexer na config de TLS):
bash production/backup-acme.sh # → production/backups/
bash production/backup-acme.sh /outro/destino # destino customizadoAgendar no cron do servidor (o jeito recomendado — configure uma vez e esqueça):
crontab -e
# todo dia às 3h da manhã
0 3 * * * /bin/bash /caminho/para/production/backup-acme.shVerificar se está rodando:
ls -la production/backups/
# deve listar acme.json.1 … acme.json.7 com datas recentes e modo -rw-------Restaurar um backup (acme.json corrompido/perdido):
./traefik.sh prod down
cp production/backups/acme.json.<dia> production/traefik/acme/acme.json
chmod 600 production/traefik/acme/acme.json
./traefik.sh prod
⚠️ As cópias ficam no mesmo disco do servidor — protegem contra deleção acidental e corrupção, não contra perda da VPS. Para cobrir esse caso, sincronizeproduction/backups/para fora da máquina (rsync, restic, object storage). Trate as cópias como segredo: contêm as chaves privadas dos certificados.
Operações comuns e health check
./traefik.sh prod logs -f traefik
./traefik.sh prod restart
./traefik.sh prod down
# Health check (produção)
curl -s http://127.0.0.1:8082/pingCertificado não é emitido (produção)
- Confirme que o registro DNS A do domínio aponta para o IP deste servidor
- Garanta que a porta 80 está acessível pela internet (o desafio HTTP exige isso)
- Veja os logs:
./traefik.sh prod logs -f traefik | grep -i acme
Dashboard não carrega (produção)
- Verifique se
TRAEFIK_DASHBOARD_HOSTnoproduction/.envbate com o domínio acessado - Confirme o DNS:
dig +short seu-dominio.com
Serviço não é detectado pelo Traefik
- Confirme que o serviço está conectado à
proxy-network - Verifique se o label
traefik.enable=trueestá presente - Veja os logs:
./traefik.sh dev logs traefik
HTTPS em dev mostra aviso de certificado inválido
- Rode
bash development/setup-traefik-dev.sh— ele instala omkcert, registra a CA local nos navegadores/SO (mkcert -install) e gera o certificado - Domínio
.dev? É esperado darnet::ERR_CERT_COMMON_NAME_INVALIDaté você listar o domínio explicitamente emdevelopment/traefik/dev-domains.txte rodar o setup de novo — veja "Usando domínio.dev" na seção de Desenvolvimento acima.curl/opensslnão reproduzem esse erro, só o navegador - Confirme que o certificado cobre o host acessado:
openssl s_client -connect 127.0.0.1:443 -servername myapp.localhost </dev/null 2>/dev/null | openssl x509 -noout -ext subjectAltName - Veja se há erro de TLS nos logs:
./traefik.sh dev logs traefik | grep -i tls
Erro de permissão no acme.json (produção)
O arquivo precisa estar com modo 600:
chmod 600 production/traefik/acme/acme.jsonExpandir
- O Traefik não acessa o socket do Docker diretamente: um sidecar
socket-proxy(tecnativa/docker-socket-proxy) numa rede interna libera apenas os endpoints read-only necessários (listar containers, eventos, versão) e nega qualquer escrita (POST=0). Uma eventual RCE no Traefik não vira acesso root ao host via API do Docker. no-new-privileges: trueestá definido em todos os containers (inclusive em dev)- O dashboard não é acessível por HTTP puro nem exposto na porta 8080
- As credenciais do dashboard usam bcrypt (fator de custo 12) via
htpasswd -B, arquivo com modo600 - A porta interna de ping (8082) não fica vinculada a nenhuma interface pública
- Logs com rotação (
max-size: 10m,max-file: 5) — o access log não enche o disco da VPS - Em desenvolvimento, as portas 80/443/8080 ficam vinculadas a
127.0.0.1— o dashboard sem autenticação não fica visível para outras máquinas da rede local. O certificado HTTPS de dev (mkcert) é só local: a CA fica confiável apenas nesta máquina, não expõe nada à rede
MIT