SSD Nodes Learn
Guias Matt ConnorPor Matt Connor · Atualizado 2026-07-24

Traefik v3 Docker Compose: 5 apps em um IP

Configure Traefik v3 com Docker Compose para gerenciar 5 apps via Host rule e Let's Encrypt. Evite erros no acme.json e automatize o TLS para múltiplos hosts.

Um IP, cinco apps, uma porta 443

Sua VPS possui um único endereço IPv4 público e uma única porta TCP 443. Você deseja rodar o Gitea, uma cópia de staging do seu app, um dashboard interno, uma página de status e um receptor de webhook — cinco hostnames em uma única máquina. Um reverse proxy é o processo que escuta as portas :80 e :443, lê o header Host em cada requisição e a encaminha para o container correto. O Traefik faz isso, obtendo e renovando certificados para cada hostname sem que você precise executar o certbot manualmente.

O que diferencia o Traefik de um bloco nginx server {} é a origem da sua configuração. No nginx, você edita um arquivo e recarrega o serviço, e o ciclo de vida do certificado permanece como uma tarefa separada — o fluxo de trabalho usado quando você emite certificados Let's Encrypt com certbot no nginx, onde um timer de renovação opera totalmente fora do servidor web. O provider Docker do Traefik monitora o stream de eventos do Docker e lê labels dos seus containers: ao iniciar um container com uma label de regra Host(), ele se torna roteável em um segundo; ao pará-lo, a rota desaparece. Esse também é o problema. Configurações armazenadas em labels ficam espalhadas em cinco lugares ao mesmo tempo, e uma label incorreta não gera erro visível — o container simplesmente não é roteado e o Traefik não reporta nada.

Os quatro substantivos

  • Entrypoints são sockets de escuta. Você definirá dois: web em :80 e websecure em :443.
  • Routers fazem o match de uma requisição (Host(...)) e a conectam a um service. Certificados são solicitados por router, via tls.certresolver.
  • Services são o backend — um container e a porta na qual ele escuta dentro da rede Docker.
  • Middlewares ficam entre o router e o service: auth básica, allow-lists de IP, rewrites de header e redirects.

A configuração estática (entrypoints, providers, ACME) é passada via linha de comando do Traefik ou em traefik.yml, e alterá-la exige o reinício do Traefik. A configuração dinâmica (routers, services, middlewares) vem de labels de containers e possui hot-reload. Confundir as duas é a causa comum de "minha flag não funciona".

O arquivo compose

Uma rede Docker compartilhada chamada proxy é a base. O Traefik alcança um container apenas se ambos estiverem nela.

name: edge

networks:
  proxy:
    name: proxy

services:
  traefik:
    image: traefik:v3.5
    restart: unless-stopped
    command:
      - --providers.docker=true
      - --providers.docker.exposedByDefault=false
      - --providers.docker.network=proxy
      - --entryPoints.web.address=:80
      - --entryPoints.websecure.address=:443
      - --entryPoints.web.http.redirections.entryPoint.to=websecure
      - --entryPoints.web.http.redirections.entryPoint.scheme=https
      - --certificatesresolvers.le.acme.email=you@example.com
      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
      - --certificatesresolvers.le.acme.tlschallenge=true
      # while you iterate, point at staging so a mistake costs nothing:
      # - --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
      - --api.dashboard=true
      - --log.level=INFO
      - --accesslog=true
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
    networks:
      - proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)
      - traefik.http.routers.dashboard.entrypoints=websecure
      - traefik.http.routers.dashboard.tls.certresolver=le
      - traefik.http.routers.dashboard.service=api@internal
      - traefik.http.routers.dashboard.middlewares=dashboard-auth
      - traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$REPLACE$$THIS

  gitea:
    image: gitea/gitea:1  # major-only pin keeps this demo copy-pasteable; pin an exact release in production
    restart: unless-stopped
    volumes:
      - ./gitea:/data
    networks:
      - proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.gitea.rule=Host(`git.example.com`)
      - traefik.http.routers.gitea.entrypoints=websecure
      - traefik.http.routers.gitea.tls.certresolver=le
      - traefik.http.services.gitea.loadbalancer.server.port=3000

docker compose up -d, depois docker compose logs -f traefik. Cada app adicional é uma cópia do bloco gitea com seu próprio nome de router, seu próprio Host() e sua própria porta interna. Uma instalação do Nextcloud rodando no Docker com TLS e backups se encaixa da mesma forma — remova as portas publicadas, conecte-a à proxy e deixe as labels do router gerenciarem o hostname e o certificado.

Cinco detalhes são fundamentais.

exposedByDefault=false torna um container invisível para o Traefik até que ele possua traefik.enable=true. Se você omitir isso, cada container iniciado — incluindo o postgres temporário que você rodou para um teste — terá uma rota gerada automaticamente.

providers.docker.network=proxy informa ao Traefik qual rede usar quando um container está conectado a várias redes. Se omitir, o Traefik pode escolher o IP errado do container, resultando em um erro 502 que parece uma falha da aplicação.

loadbalancer.server.port=3000 é a porta dentro do container; o Gitea escuta na porta 3000 lá. Note que nenhum container de app publica portas — apenas o Traefik faz isso.

O redirect no entrypoint web transforma requisições em texto puro em um 308 para HTTPS. A porta 80 permanece aberta: o desafio ACME HTTP precisa dela, assim como usuários que digitam apenas o hostname.

O $$ duplicado no hash do basic-auth é o escape do Compose, não um erro de digitação. Gere o hash com htpasswd -nbB admin 'your-password' (pacote apache2-utils) e depois duplique cada $.

O certificado e a armadilha do acme.json

tlschallenge=true seleciona TLS-ALPN-01: O Let's Encrypt conecta-se ao seu servidor na porta 443 e o Traefik responde ao desafio durante o handshake TLS. A alternativa é o HTTP-01, na porta 80 — substitua a linha tlschallenge na lista command: do Traefik por estas duas:

      - --certificatesresolvers.le.acme.httpchallenge=true
      - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web

Ambas funcionam. Ambas exigem que o DNS público para o hostname já aponte para o seu VPS — a autoridade certificadora resolve o nome e conecta-se externamente. Crie o registro A (e AAAA) primeiro, confirme com dig +short git.example.com e então inicie o Traefik.

Agora, a armadilha que faz as pessoas perderem uma noite inteira. O Traefik armazena sua chave de conta ACME e todos os certificados emitidos em um único acme.json. Se esse arquivo estiver legível para o grupo ou para outros usuários (world-readable), o Traefik exibirá uma linha muito próxima a esta e parará:

error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600

A solução correta é a mencionada acima: faça um bind-mount do diretório e deixe o Traefik criar o arquivo com as permissões corretas. Se você criou o acme.json usando touch, o seu umask definiu a permissão como 644. Corrija no host:

chmod 600 ./letsencrypt/acme.json
docker compose restart traefik

Faça backup desse diretório junto com os volumes do seu app. Perder o arquivo é contornável — os certificados podem ser reemitidos — mas reemitir cinco hostnames de uma vez fará você atingir os limites de taxa (rate limits).

Use a CA de staging enquanto estiver testando. Descomente a linha caserver, faça todas as rotas funcionarem, então comente a linha novamente e delete o acme.json para que os certificados de produção sejam solicitados do zero. O Let's Encrypt de produção permite cinco certificados duplicados por semana para um conjunto idêntico de hostnames, e aplica throttling para validações repetidas que falharam para o mesmo nome. O staging emite certificados não confiáveis — seu navegador exibirá um aviso, e esse aviso é o sinal de que funcionou — com limites muito mais flexíveis.

O dashboard é uma interface de controle, não uma demonstração

A maioria dos guias rápidos configura o --api.insecure=true, que disponibiliza o dashboard na porta 8080 sem autenticação. Isso é perigoso em servidores com IP público, pois expõe sua topologia de roteamento, hostnames, nomes de middleware e portas de backend para qualquer scanner.

Os labels no serviço traefik acima são a alternativa: o dashboard roteado como qualquer outra aplicação, em um hostname real, via TLS e atrás do basicauth. O service=api@internal é o que conecta o roteador à API nativa do Traefik. Reforce a segurança adicionando uma lista de permissão de IP (allow-list), aplicada da esquerda para a direita. Se o endereço do seu escritório for dinâmico, defina o intervalo para a sub-rede fornecida por uma VPN WireGuard hospedada por você no mesmo VPS e acesse o dashboard apenas pelo túnel:

- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-auth

O socket do Docker é root

/var/run/docker.sock é uma API que pode criar um container que monta o / do host. O acesso a ela equivale ao root na máquina, e o Traefik precisa disso para ler labels.

Mantenha o :ro no mount, mas entenda o que isso oferece: isso torna o arquivo de socket apenas leitura. Isso não impede requests POST para a Docker API através dele. A mitigação real é nunca entregar o socket ao Traefik e colocar um proxy de filtragem entre eles:

  dockerproxy:
    image: tecnativa/docker-socket-proxy   # pin the current tag
    restart: unless-stopped
    environment:
      CONTAINERS: 1
      NETWORKS: 1
      POST: 0
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - proxy

Remova o volume do socket do Traefik e aponte o provider para o proxy:

--providers.docker.endpoint=tcp://dockerproxy:2375

O Traefik mantém o acesso de leitura para containers e redes, e perde a capacidade de criar qualquer coisa.

Firewall, portas e a regra que todos erram

Duas portas abertas, além do SSH:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

As portas publicadas pelo Docker ignoram o ufw. O Docker insere suas próprias regras de iptables, que são avaliadas antes das chains do ufw. Por isso, um container iniciado com ports: ["3000:3000"] fica acessível pela internet mesmo com uma regra de deny no ufw. A defesa é estrutural, não de configuração de firewall: publique portas apenas através do Traefik e atribua a todos os outros containers networks: [proxy] e nada mais. Se algo realmente precisar alcançar o host, faça o bind no loopback — "127.0.0.1:3000:3000".

Troubleshooting: erros que você realmente verá

404 page not found, servido pelo Traefik. Nenhum router correspondeu. Em ordem de probabilidade: o container não possui traefik.enable=true (com exposedByDefault=false configurado); a regra Host() não corresponde ao nome digitado; o nome do router em uma label é diferente do nome do router em outra (routers.gitea.rule e routers.gitea.entrypoints devem ser a mesma palavra); ou você envolveu o hostname com aspas em vez de backticks. O Traefik v3 exige backticks dentro de matchers.

502 Bad Gateway. Um router correspondeu, mas o backend estava inacessível. Quase sempre o container não está na rede proxy — verifique docker inspect -f '{{json .NetworkSettings.Networks}}' gitea. A outra causa provável é um loadbalancer.server.port incorreto: você forneceu uma porta publicada, ou o app escuta em outro local. O log indica a tentativa: dial tcp 172.18.0.5:8080: connect: connection refused.

O navegador emite um aviso e o certificado é emitido para TRAEFIK DEFAULT CERT. Não existe certificado para esse hostname e o Traefik serviu seu placeholder autoassinado. Leia as linhas do ACME:

unable to obtain ACME certificate for domains "git.example.com" ...
acme: error: 400 ... DNS problem: NXDOMAIN looking up A for git.example.com

O DNS ainda não aponta para a máquina. Corrija o registro, aguarde o TTL e reinicie o Traefik.

Invalid response from http://git.example.com/.well-known/acme-challenge/... no HTTP challenge: a porta 80 não alcança o Traefik externamente — geralmente é um firewall de nível de provedor antes da VPS, não o ufw.

Os certificados nunca são emitidos e seu DNS está no Cloudflare com a nuvem laranja ativada. O Cloudflare termina o TLS em sua borda e o TLS-ALPN-01 não consegue completar através dele. Configure o registro como DNS-only durante a emissão, ou mude para o DNS-01 challenge com um API token. O DNS-01 também é o único challenge que emite wildcards.

Redirect loop. Algo antes do Traefik já termina o TLS e encaminha texto puro para a porta :80; o redirect do entrypoint o envia de volta para HTTPS. Remova um dos dois redirects.

Mantendo o serviço ativo

A unit do Docker deve estar habilitada na inicialização (systemctl is-enabled docker), e o restart: unless-stopped restaura a stack após um reboot. Para um controle explícito, uma unit pequena do systemd executando docker compose -f /srv/edge/compose.yml up -d com RemainAfterExit=yes oferece systemctl status edge e controle de ordenação.

Fixe a tag do Traefik (traefik:v3.5, nunca latest). O upgrade da v2 para a v3 alterou a sintaxe das regras e os nomes dos providers; um latest não supervisionado tentará recarregar uma configuração que ele não reconhece mais. Faça o upgrade de forma deliberada: leia as notas de migração, atualize a tag, docker compose up -d traefik e monitore o log. Se você ainda estiver usando uma tag v2, o guia de migração do Traefik v2 para v3 detalha cada renomeação, o modo de compatibilidade e um rollback que preserva seus certificados.

Faça backup do ./letsencrypt e do volume de dados de cada app. O Traefik não armazena nenhum outro estado que não possa ser reconstruído a partir do arquivo compose.

O que falha em escala

O primeiro limite não é o throughput, é o servidor único: um Traefik em um VPS é um ponto único de falha para cinco apps, e o acme.json é armazenamento em arquivo — duas instâncias do Traefik escrevendo nele causarão corrupção. Escalar significa mover o armazenamento de certificados para fora de um arquivo ou terminar o TLS em outro lugar.

O segundo limite são conexões de longa duração. Server-sent events, uploads grandes e clientes lentos atingem os timeouts de resposta do entrypoint; o --entryPoints.websecure.transport.respondingTimeouts.readTimeout e seus irmãos writeTimeout e idleTimeout são os parâmetros de ajuste. WebSockets funcionam via pass-through sem configuração extra.

O terceiro limite é o disco. O --accesslog=true escreve no stdout, e o driver json-file do Docker mantém esses dados para sempre, a menos que haja um limite. Configure o logging.options.max-size no serviço do Traefik, ou escreva o access log em um arquivo e faça o log rotation.

Nada disso exige um orquestrador. É necessário um servidor sob seu controle, com um IP real e as portas 80 e 443 abertas para a internet — um único VPS pequeno é toda a lista de dependências.

FAQ

Eu ainda preciso do certbot se eu uso o Traefik?

Não. O resolver ACME do Traefik solicita e renova o certificado para cada hostname que ele roteia, e armazena tudo em acme.json. O Certbot continua sendo a ferramenta correta quando o nginx ou outro servidor faz o término do TLS. Rodar ambos para os mesmos hostnames apenas consome os limites de taxa do Let's Encrypt.

Por que meu container retorna 404 através do Traefik?

Um 404 servido pelo Traefik significa que nenhum router correspondeu à requisição. Verifique se o container possui traefik.enable=true (obrigatório após configurar exposedByDefault=false), se o valor de Host() coincide com o nome digitado e se o nome do router é idêntico em todas as labels daquele app. O Traefik v3 também exige crases dentro do matcher, não aspas.

Qual é a diferença entre um 404 e um 502 aqui?

Um 404 significa que o roteamento não ocorreu; um 502 significa que um router correspondeu à requisição, mas o backend recusou a conexão. Os candidatos comuns para 502 são um container que não está conectado à rede proxy e um loadbalancer.server.port apontando para uma porta publicada em vez da porta que o app escuta dentro do container. O access log mostra o endereço exato que o Traefik tentou acessar.

Montar o Docker socket como read-only é suficiente?

A flag :ro torna o arquivo do socket read-only, não a API por trás dele — requisições POST ainda trafegam por ele, e o acesso à Docker API equivale ao root no host. A configuração mais segura é o container docker-socket-proxy mostrado acima, que expõe apenas leituras de container e rede para o Traefik e bloqueia escritas completamente.

O Traefik pode emitir um certificado wildcard?

Apenas via desafio DNS-01, utilizando um token de API do seu provedor de DNS. TLS-ALPN-01 e HTTP-01 validam apenas um único hostname e não podem gerar um wildcard. O DNS-01 também é a solução quando um CDN como o Cloudflare faz o término do TLS antes do seu VPS e os outros dois desafios falham.