Traefik no Docker Compose: 5 apps em um IP
Configure 5 apps com Traefik v3 e Docker Compose usando regras Host e Let's Encrypt. Evite o erro de inicialização causado por permissões incorretas no acme.json.
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:
webem:80ewebsecureem:443. - Routers associam um pedido (
Host(...)) a um serviço. Os certificados são pedidos por router, através detls.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 ficheiro Compose
Uma rede Docker partilhada, chamada proxy, é a base da configuração. O Traefik só consegue alcançar um contentor se ambos estiverem nessa rede.
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=3000docker 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 em Docker com TLS e cópias de segurança é integrada da mesma forma: remova as portas publicadas, ligue-a a proxy e deixe os labels do router tratarem do hostname e do certificado.
Cinco detalhes desta configuração são importantes.
exposedByDefault=false torna um contentor invisível para o Traefik até este ter traefik.enable=true. Se o omitir, todos os contentores iniciados posteriormente, incluindo o contentor temporário postgres que executou para verificar algo, passam a ter uma rota gerada.
providers.docker.network=proxy indica ao Traefik qual 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 resultado é 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. Repare que nenhum contentor de aplicação publica uma porta. Apenas o Traefik o faz.
O redirecionamento no entrypoint web converte os pedidos em texto simples num redirecionamento 308 para HTTPS. A porta 80 continua aberta: o desafio HTTP do ACME precisa dela, assim como as pessoas que introduzem um hostname sem protocolo.
O $$ duplicado no hash de basic auth é um mecanismo de escape do Compose, não um erro de escrita. Gere-o com htpasswd -nbB admin 'your-password' (pacote apache2-utils) e duplique 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=webAmbas 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, em seguida, inicie o Traefik.
Agora, a armadilha que pode consumir uma noite de trabalho. O Traefik mantém a chave da conta ACME e todos os certificados emitidos num único acme.json. Se esse ficheiro puder ser lido pelo grupo ou por qualquer utilizador, o Traefik imprime uma linha muito semelhante a esta e para:
error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600A correção adequada é a indicada acima: faça bind mount do diretório e deixe o Traefik criar o ficheiro com as permissões corretas. Se criou acme.json com touch, a sua umask definiu as permissões como 644. Corrija no host:
chmod 600 ./letsencrypt/acme.json
docker compose restart traefikInclua esse diretório nas cópias de segurança juntamente com os volumes da aplicação. Se o perder, é possível recuperar: os certificados serão emitidos novamente. No entanto, emitir novamente cinco hostnames ao mesmo tempo pode atingir os limites de taxa.
Use a CA de staging enquanto faz iterações. Remova o comentário da linha caserver, faça todas as rotas funcionarem, depois volte a comentá-la e elimine acme.json para que sejam solicitados 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 as validações falhadas repetidas para o mesmo nome. O staging emite certificados não confiáveis. O seu browser apresenta um aviso, e esse aviso confirma que o processo funcionou, com limites muito mais permissivos.
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-authUma ú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:
- proxyRemova o volume do socket do Traefik e aponte o provider para o proxy:
--providers.docker.endpoint=tcp://dockerproxy:2375O 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 enableAs 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 você realmente verá
404 page not found, servido pelo Traefik. Nenhum router correspondeu. Por ordem de probabilidade: o contentor não tem traefik.enable=true (com exposedByDefault=false definido); a regra Host() não corresponde ao nome introduzido; o nome do router num label é diferente do nome noutro (routers.gitea.rule e routers.gitea.entrypoints têm de ser a mesma palavra); ou o hostname foi colocado entre aspas em vez de crases. O Traefik v3 exige crases dentro dos matchers.
502 Bad Gateway. Um router correspondeu, mas o backend estava inacessível. 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: foi indicada 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 navegador apresenta um aviso e o certificado foi emitido para TRAEFIK DEFAULT CERT. Não existe um certificado para esse hostname e o Traefik serviu o seu certificado autoassinado de substituição. 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.comO 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 o ufw.
Os certificados nunca são emitidos e o DNS está no Cloudflare com a nuvem laranja ativa. 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 wildcards.
Loop de redirecionamento. Algo à frente do Traefik já termina o TLS e encaminha texto simples 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 deve estar ativada para arrancar no boot (systemctl is-enabled docker), e restart: unless-stopped repõe a stack depois de um reboot. Para 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.5, nunca latest). A atualização da v2 para a v3 alterou a sintaxe das regras e os nomes dos providers, e um latest não supervisionado irá recarregar sem hesitar uma configuração que já não compreende. Faça a atualização de forma controlada: leia as notas de migração, altere a tag, docker compose up -d traefik e monitorize o log. Se ainda estiver numa tag v2, o guia de migração do Traefik v2 para v3 descreve 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 eu executar o Traefik?
Não. O resolvedor ACME do Traefik solicita e renova o certificado para cada nome de host que encaminha e armazena tudo em acme.json. O Certbot continua a ser a ferramenta adequada quando o nginx ou outro servidor termina o TLS diretamente; executar ambos para os mesmos nomes de host apenas consome os limites de taxa do Let's Encrypt.
Por que o meu contentor devolve 404 através do Traefik?
Um 404 servido pelo Traefik significa que nenhum router correspondeu ao pedido. Verifique se o contentor tem traefik.enable=true (obrigatório quando exposedByDefault=false está definido), se o valor de Host() corresponde ao nome que introduziu e se o nome do router é idêntico em todas as labels dessa aplicação. O Traefik v3 também exige crases dentro do matcher, não aspas.
Qual é a diferença entre 404 e 502 neste caso?
Um 404 significa que o encaminhamento não ocorreu; um 502 significa que um router correspondeu, mas o backend recusou a ligação. As causas habituais de 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 somente leitura é suficiente?
A flag :ro torna o ficheiro do socket somente leitura, mas não a API disponibilizada por esse socket; os pedidos POST continuam a passar por ele, e o acesso à API do Docker equivale a root no host. A configuração mais segura é o contentor docker-socket-proxy apresentado acima, que expõe ao Traefik apenas operações de leitura sobre contentores e redes e bloqueia diretamente as operações de escrita.
O Traefik pode emitir um certificado wildcard?
Apenas através do desafio DNS-01, com um token de API do seu fornecedor de DNS. O TLS-ALPN-01 e o HTTP-01 validam cada um um único nome de host e não podem produzir um wildcard. O DNS-01 também é a solução quando uma CDN como a Cloudflare termina o TLS à frente do seu VPS e os outros dois desafios nunca são concluídos.