SSD Nodes Learn Hosting plans →
Guias Matt ConnorPor Matt Connor

Traefik no Docker Compose: 5 apps com um IP

Veja como publicar 5 apps com Traefik v3 e Docker Compose, roteando por Host e usando TLS automático. Evite o erro acme.json que bloqueia a emissão.

Um IP, cinco aplicações, uma porta 443

O seu VPS tem um único endereço IPv4 público e uma única porta TCP 443. Você quer alojar nele o Gitea, uma cópia de staging da sua aplicação, um dashboard interno, uma página de estado e um recetor de webhooks: cinco nomes de host, uma máquina. Um reverse proxy é o processo que controla as portas :80 e :443, lê o cabeçalho Host em cada pedido e encaminha-o para o contentor correto. O Traefik faz isso e obtém e renova um certificado para cada nome de host sem que você precise de executar o certbot manualmente. O Nginx e o Caddy também terminariam corretamente o TLS para os mesmos cinco nomes de host. Por isso, se a escolha ainda estiver em aberto, vale a pena comparar os três proxies quanto à gestão de certificados e ao custo de configuração por aplicação antes de ligar tudo a um deles.

O que distingue o Traefik de um bloco server {} do nginx é a origem da configuração. Com o nginx, você edita um ficheiro e recarrega o serviço. O ciclo de vida dos certificados continua a ser uma tarefa separada. Esse é o fluxo que você segue quando emite certificados Let's Encrypt com certbot no nginx, com um temporizador de renovação completamente fora do servidor Web. O provider Docker do Traefik monitoriza o fluxo de eventos do Docker e lê labels dos seus contentores: inicie um contentor com uma label de regra Host() e ele ficará acessível em menos de um segundo; pare-o e a rota desaparece. Esse também é o risco. A configuração armazenada em labels fica distribuída por cinco locais ao mesmo tempo. Uma label incorreta falha silenciosamente: o contentor simplesmente não é encaminhado e o Traefik não informa o problema.

Os quatro substantivos

  • Entrypoints são sockets de escuta. Irá definir dois: web em :80 e websecure em :443.
  • Routers associam um pedido (Host(...)) a um serviço. Os certificados são pedidos por router, através de tls.certresolver.
  • Services são o backend: um contentor e a porta onde este escuta dentro da rede Docker.
  • Middlewares ficam entre o router e o serviço: autenticação básica, listas de permissões de IP, reescrita de cabeçalhos e redirecionamentos.

Estes quatro substantivos são os nomes que o Traefik dá a tarefas que, de outro modo, teria de escrever manualmente: o router é um server_name, o serviço é um destino proxy_pass e os middlewares são as diretivas de cabeçalhos e autenticação que definiria ao criar linha a linha um bloco server de reverse proxy nginx.

A configuração estática (entrypoints, providers e ACME) é transmitida ao Traefik na linha de comandos ou em traefik.yml. Alterá-la implica reiniciar o Traefik. A configuração dinâmica (routers, services e middlewares) chega através das labels dos contentores e é recarregada sem interrupção. Confundir as duas é a causa habitual de a "minha flag não fazer nada".

O arquivo Compose

Uma rede Docker partilhada chamada proxy é a base da configuração. O Traefik só consegue aceder a um contentor se ambos estiverem nessa rede.

name: edge

networks:
  proxy:
    name: proxy

services:
  traefik:
    image: traefik:v3.7
    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 e, depois, docker compose logs -f traefik. Cada aplicação adicional é uma cópia do bloco gitea, com o seu próprio nome de router, o seu próprio Host() e a sua própria porta interna. Uma instalação do Nextcloud a correr no Docker com TLS e backups integra-se da mesma forma: remova as portas publicadas, ligue-o a proxy e deixe que os labels do router tratem do hostname e do certificado.

Há cinco detalhes importantes.

exposedByDefault=false torna um contentor invisível para o Traefik até este receber traefik.enable=true. Se o omitir, todos os contentores que iniciar, incluindo o contentor temporário postgres que executou para verificar algo, recebem uma rota.

providers.docker.network=proxy indica ao Traefik que rede deve usar quando um contentor está ligado a várias redes. Se o omitir, o Traefik pode escolher o IP errado do contentor, o que aparece como um erro 502 que parece ser causado pela aplicação.

loadbalancer.server.port=3000 é a porta dentro do contentor; o Gitea escuta nessa porta 3000. Note que nenhum contentor de aplicação publica uma porta. Apenas o Traefik publica portas.

O redirecionamento no entrypoint web é permanente por predefinição. Assim, um GET em texto simples, como o enviado por um browser, recebe um 301 para HTTPS. O Traefik responde com 308 apenas para outros métodos, como POST, para preservar o método durante o redirecionamento. A porta 80 continua aberta: o desafio HTTP do ACME precisa dela, assim como os utilizadores que escrevem um hostname sem esquema.

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

O certificado e a armadilha do acme.json

tlschallenge=true seleciona TLS-ALPN-01: o Let's Encrypt liga-se ao seu servidor na porta 443 e o Traefik responde ao desafio durante o handshake TLS. A alternativa é 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. As duas exigem que o DNS público do hostname já aponte para o seu VPS e que a autoridade certificadora consiga resolver o nome e ligar-se a partir do exterior. Crie primeiro o registo A (e AAAA), confirme com dig +short git.example.com e só depois inicie o Traefik.

Agora, a armadilha que pode custar uma noite inteira. O Traefik guarda a chave da conta ACME e todos os certificados emitidos num único acme.json. Se esse ficheiro tiver permissões de leitura para o grupo ou para todos os utilizadores, o Traefik regista uma mensagem muito semelhante a esta e descarta o resolver. Continua a executar e a encaminhar tráfego, mas nunca solicita um certificado. Por isso, todos os hostnames recebem TRAEFIK DEFAULT CERT:

ERR The ACME resolve is skipped from the resolvers list error="unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600" resolver=le

A correção adequada é a indicada acima: faça bind mount do diretório e deixe o Traefik criar o ficheiro com o modo correto. Se criou acme.json com touch, o seu umask definiu as permissões como 644. Corrija-o no host:

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

Inclua esse diretório nas cópias de segurança juntamente com os volumes da aplicação. Perder o diretório uma vez é recuperável, porque os certificados são emitidos novamente. No entanto, apagá-lo em cada novo deployment faz com que atinja o limite de cinco certificados por conjunto exato de hostnames a cada 7 dias.

Use a CA de staging enquanto faz testes. Retire o comentário da linha caserver, faça todas as rotas funcionarem, volte a comentá-la e elimine acme.json para solicitar novos certificados de produção. O Let's Encrypt de produção permite cinco certificados duplicados por semana para um conjunto idêntico de hostnames e limita validações repetidas que falhem para o mesmo nome. O staging emite certificados não confiáveis, pelo que o navegador apresenta um aviso. Esse aviso confirma que o processo funcionou e que os limites são muito menos restritivos.

O dashboard é uma superfície de controlo, não uma demonstração

A maioria dos quickstarts define --api.insecure=true, que disponibiliza o dashboard na porta 8080 sem autenticação. Num servidor com um IP público, isso expõe a topologia de encaminhamento, os nomes dos hosts, os nomes dos middlewares e as portas dos backends a qualquer pessoa que faça uma varredura.

Os labels do serviço traefik acima são a alternativa: o dashboard é encaminhado como qualquer outra aplicação, num hostname real, através de TLS e protegido por basicauth. service=api@internal liga o router à API integrada do Traefik. Pode reforçar a proteção encadeando uma lista de permissões de IP, aplicada da esquerda para a direita. Se o endereço do escritório for dinâmico, defina o intervalo para a sub-rede atribuída por uma VPN WireGuard alojada por si no mesmo VPS e aceda ao dashboard apenas através do túnel:

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

Uma única password basicauth partilhada deixa de ser defensável quando cinco aplicações precisam das suas próprias contas. O mesmo slot de middleware aceita um forwardauth que transfere a decisão para o Authentik, um servidor self-hosted de single sign-on, para que o dashboard e todas as rotas adjacentes fiquem protegidos por um único login que pode revogar num só local.

O socket do Docker dá acesso a root

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

Mantenha :ro no mount, mas deixe claro o que essa opção garante: torna o ficheiro do socket apenas de leitura. Não impede que pedidos POST à API do Docker passem por ele. A mitigação real consiste em nunca fornecer o socket ao Traefik e colocar um proxy de filtragem entre ambos:

  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 acesso de leitura aos contentores e às redes, mas deixa de poder criar qualquer recurso.

Firewall, portas e a regra que toda a gente interpreta mal

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 as suas próprias regras do iptables, que são avaliadas antes das cadeias do ufw. Por isso, um contentor iniciado com ports: ["3000:3000"] fica acessível a partir da Internet, mesmo que exista uma regra ufw deny para essa porta. A defesa é estrutural, não depende da configuração da firewall: publique portas apenas a partir do Traefik e dê a todos os outros contentores networks: [proxy], sem mais nada. Se algo tiver realmente de aceder ao host, associe-o ao loopback, "127.0.0.1:3000:3000".

Solução de problemas: erros que irá realmente encontrar

404 page not found, servido pelo Traefik. Nenhum router correspondeu ao pedido. Por ordem de probabilidade: o contentor não tem traefik.enable=true (com exposedByDefault=false definido); a regra Host() não corresponde ao nome que introduziu; o nome do router numa label difere do nome noutra (routers.gitea.rule e routers.gitea.entrypoints têm de ser a mesma palavra); ou colocou o hostname entre aspas simples. O Traefik v3 aceita crases ou aspas duplas escapadas (\") dentro dos matchers e rejeita aspas simples.

502 Bad Gateway. Um router correspondeu, mas não foi possível aceder ao backend. Quase sempre, o contentor não está na rede proxy; verifique docker inspect -f '{{json .NetworkSettings.Networks}}' gitea. A outra possibilidade é um loadbalancer.server.port incorreto: indicou uma porta publicada ou a aplicação escuta noutra porta. O log identifica a tentativa: dial tcp 172.18.0.5:8080: connect: connection refused.

O browser avisa e o certificado foi emitido para TRAEFIK DEFAULT CERT. Não existe nenhum certificado para esse hostname e o Traefik serviu o seu certificado autoassinado predefinido. Consulte 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 o servidor. Corrija o registo, aguarde o TTL e reinicie o Traefik.

Invalid response from http://git.example.com/.well-known/acme-challenge/... no desafio HTTP: a porta 80 não chega ao Traefik a partir do exterior. Normalmente, existe uma firewall ao nível do fornecedor à frente do VPS, e não uma regra do ufw.

Os certificados nunca são emitidos e o DNS está no Cloudflare com a nuvem laranja ativada. O Cloudflare termina o TLS na sua edge e o TLS-ALPN-01 não consegue ser concluído através dela. Defina o registo como DNS-only durante a emissão ou mude para o desafio DNS-01 com um token de API. O DNS-01 também é o único desafio que emite certificados wildcard.

Loop de redirecionamento. Algo à frente do Traefik já termina o TLS e encaminha tráfego sem encriptação para :80; o redirecionamento do entrypoint envia-o novamente para HTTPS. Remova um dos dois redirecionamentos.

Manter o serviço em execução

A unidade do Docker tem de estar configurada para arrancar no boot (systemctl is-enabled docker), e restart: unless-stopped repõe a stack depois de um reboot. Para ter um controlo explícito, uma unidade systemd pequena que execute docker compose -f /srv/edge/compose.yml up -d com RemainAfterExit=yes fornece systemctl status edge e controlo da ordem de arranque.

Fixe a tag do Traefik (traefik:v3.7, nunca latest) e fixe uma branch que ainda publique releases. Em 2026-10-12, o Traefik indica a v3.7 como a única branch v3 com suporte. A v3.5 publicou o último release, v3.5.6, em 7 November 2025, e o seu Docker provider tem a versão da API 1.24 definida diretamente no código. O Docker Engine 29 e versões posteriores recusam-na com client version 1.24 is too old, pelo que nenhum contentor é encaminhado. O Traefik 3.6.1 adicionou a negociação da versão da API do Docker, razão pela qual o ficheiro compose acima usa v3.7. A atualização da v2 para a v3 alterou a sintaxe das regras e os nomes dos providers. Um latest não supervisionado recarrega sem problemas uma configuração que já não compreende. Faça a atualização de forma deliberada: leia as notas de migração, atualize a tag, docker compose up -d traefik e monitorize o log. Se ainda estiver a usar uma tag v2, o guia de migração do Traefik v2 para v3 explica cada renomeação, o modo de compatibilidade e um rollback que preserva os seus certificados.

Faça backup de ./letsencrypt e do volume de dados de cada aplicação. O Traefik não mantém outro estado que não possa reconstruir a partir do ficheiro compose.

O que falha em escala

O primeiro limite não é o throughput, mas o servidor único: um Traefik num único VPS é um ponto único de falha para cinco aplicações, e acme.json é armazenamento em ficheiro simples; duas instâncias do Traefik a escrever nesse ficheiro vão corrompê-lo. Escalar horizontalmente significa retirar o armazenamento dos certificados de um ficheiro ou terminar o TLS noutro local.

O segundo limite são as ligações de longa duração. Server-sent events, uploads grandes e clientes lentos atingem os timeouts de resposta do entrypoint; --entryPoints.websecure.transport.respondingTimeouts.readTimeout e os seus equivalentes writeTimeout e idleTimeout são os parâmetros de controlo. WebSockets passam sem configuração adicional.

O terceiro limite é o disco. --accesslog=true escreve em stdout, e o driver json-file do Docker mantém esses dados indefinidamente, a menos que exista um limite. Defina logging.options.max-size no serviço Traefik ou escreva o access log num ficheiro e faça a sua rotação.

Nada disto exige um orquestrador. É necessário um servidor sob o seu controlo, com um IP real e as portas 80 e 443 abertas para a Internet. Um VPS pequeno é toda a lista de dependências.

FAQ

Ainda preciso do certbot se utilizar o Traefik?

Não. O resolvedor ACME do Traefik solicita e renova o certificado para cada nome de host que encaminha, armazenando tudo em acme.json. O certbot continua a ser a ferramenta correta quando o nginx ou outro servidor termina o TLS por conta própria; executar ambos para os mesmos nomes de host apenas consome os limites de taxa da Let's Encrypt.

Por que o meu contentor devolve um 404 através do Traefik?

Um 404 servido pelo Traefik significa que nenhum router correspondeu ao pedido. Verifique se o contentor possui traefik.enable=true (obrigatório quando exposedByDefault=false está definido), se o valor de Host() corresponde ao nome que digitou e se o nome do router é idêntico em todas as labels dessa aplicação. O Traefik v3 também exige crases ou aspas duplas escapadas dentro do matcher; aspas simples são rejeitadas.

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

Um 404 significa que o encaminhamento nunca ocorreu; um 502 significa que um router correspondeu ao pedido, mas o backend recusou a ligação. Os candidatos habituais a um 502 são um contentor que não está ligado à rede proxy e um loadbalancer.server.port que aponta para uma porta publicada em vez da porta em que a aplicação escuta dentro do contentor. O log de acesso indica o endereço exato para o qual o Traefik tentou ligar-se.

Montar o socket do Docker como apenas de leitura é suficiente?

A flag :ro torna o ficheiro de socket apenas de leitura, mas não a API por trás dele; pedidos POST continuam a passar por ele, e o acesso à API do Docker é equivalente a ter privilégios de root no host. A configuração mais robusta é o contentor docker-socket-proxy mostrado acima, que expõe apenas leituras de contentores e redes ao Traefik, bloqueando totalmente as escritas.

O Traefik pode emitir um certificado wildcard?

Apenas através do desafio DNS-01, com um token de API para o seu fornecedor de DNS. O TLS-ALPN-01 e o HTTP-01 validam cada um apenas um nome de host e não podem produzir um wildcard. O DNS-01 é também a solução quando uma CDN, como o Cloudflare, termina o TLS à frente do seu VPS e os outros dois desafios nunca são concluídos.