# docker-compose.cloudflared.yml — override para instalação ATRÁS DE NAT, onde # nada pode publicar porta e quem resolve o acesso é o Cloudflare Tunnel. # # docker compose -f docker-compose.prod.yml -f docker-compose.cloudflared.yml --env-file .env up -d # # QUANDO USAR # ----------- # O `caddy` do stack base publica 80/443 e emite o certificado Let's Encrypt. # Isso pressupõe que a internet alcança a máquina nessas portas. Atrás de NAT # (ou de CGNAT, ou de uma rede que proíbe inbound) não alcança: o Caddy nunca # obtém certificado e o domínio não abre. O Cloudflare Tunnel inverte o sentido: # o `cloudflared` abre uma conexão de SAÍDA contínua para a borda da Cloudflare, # e é a borda que atende o domínio. Nenhuma porta de entrada é necessária. # # Com este override: # # 1. o serviço `caddy` ganha um profile que nunca é ativado — o compose # simplesmente não o cria (não há 80/443 para publicar); # 2. sobe o `cloudflared`, que fala com `app:3000` pela rede interna. # 3. o `app` passa a ler o endereço de quem acessa do `cf-connecting-ip` # (IP_DO_CLIENTE_HEADER, lib/http/ip-do-cliente.ts). A borda da # Cloudflare ACRESCENTA ao `x-forwarded-for` que recebe, em vez de # substituí-lo como o Caddy faz; o `cf-connecting-ip` é ela quem escreve. # # O comportamento padrão (sem este arquivo) fica EXATAMENTE como antes: quem não # passa o `-f docker-compose.cloudflared.yml` não é afetado por nada aqui. # # SÓ O TOKEN MORA AQUI # -------------------- # O túnel é "remotely-managed": a configuração inteira (qual hostname aponta # para qual serviço, TLS, Access) vive no painel da Cloudflare, não em arquivo # local. O `.env` guarda só o token do túnel: # # CLOUDFLARE_TUNNEL_TOKEN= # # Passos no painel (Zero Trust > Networks > Tunnels): # # 1. crie um túnel e copie o token para CLOUDFLARE_TUNNEL_TOKEN no .env; # 2. em Public Hostnames, aponte o seu domínio para o serviço # http://app:3000 # (o nome `app` existe na rede interna do compose; NÃO use https, NÃO use # o IP do host — o TLS é terminado na borda da Cloudflare); # 3. aponte DOMAIN, NEXT_PUBLIC_APP_URL e NEXT_PUBLIC_ADMIN_URL no .env para # esse mesmo hostname (https://...). Os redirects de login/recuperação do # Supabase Auth também precisam listá-lo. # # O WEBHOOK GLOBAL DO WAHA # ------------------------ # No stack base o Caddy responde 403 a `/api/v1/webhooks/waha` (webhook global, # sem token na URL). Sem o Caddy, quem recusa é o próprio app: a rota global só # atende a rede interna e responde 404 ao que chega pela borda — o túnel marca # a requisição com `cf-connecting-ip` (`chegouPelaBorda`, lib/http/ip-do-cliente.ts). # Uma regra de WAF no painel é camada extra, opcional: # # Security > WAF > Custom rules # Se: http.host eq "SEU-DOMINIO" and http.request.uri.path eq "/api/v1/webhooks/waha" # Então: Block # # A assinatura do remetente (HMAC) é opcional e vem DESLIGADA de fábrica; quem # quiser exigi-la liga em /admin/sistema. Não conte com ela sem ter ligado. # # ⚠️ O TETO DE 100 SEGUNDOS DA CLOUDFLARE # --------------------------------------- # A borda da Cloudflare corta requisições HTTP acima de ~100s (504/524), e isso # não é configurável no plano padrão. O WAHA chama `http://app:3000` direto pela # rede interna e não passa pelo túnel. O runner legado do agente, que passava pelo # endereço público e podia levar minutos, saiu junto com a rota # `/api/internal/agents/run` e o dispatcher que a chamava. # # A única porta que continua publicada por um serviço nosso não é HTTP (o áudio # WebRTC do `wacalls`, UDP) — proxy L7 nenhum carrega UDP, e NAT também não. # Esse módulo é opcional e desligado por padrão; quem o ligar atrás de NAT # precisa de outro caminho para a mídia, e isso está fora do escopo deste overlay. services: # Só acrescenta uma variável ao `app` do arquivo base (o compose mescla o # mapa `environment`). Ela vive AQUI, e não no .env, porque só vale para # esta topologia: sem a borda da Cloudflare na frente, quem escreveria o # `cf-connecting-ip` seria o próprio cliente. app: environment: IP_DO_CLIENTE_HEADER: cf-connecting-ip cloudflared: # Imagem UPSTREAM: referencia-se com tag fixa, nunca se republica # (docs/doctrine/packaging.md). Tag de versão é imutável aqui, então # `pull_policy: missing` — `always` amarraria a subida do CRM à # disponibilidade do Docker Hub. image: ${CLOUDFLARED_IMAGE:-cloudflare/cloudflared:2026.10.0} pull_policy: missing restart: unless-stopped # O token vem do .env. `:?` faz o compose RECUSAR subir sem ele em vez de # iniciar um contêiner mudo que reinicia em loop — falha cedo e alto. environment: TUNNEL_TOKEN: ${CLOUDFLARE_TUNNEL_TOKEN:?CLOUDFLARE_TUNNEL_TOKEN ausente no .env} # `tunnel run` lê o token de TUNNEL_TOKEN (o modo remotely-managed). Sem # `--no-autoupdate`, a imagem tenta se atualizar sozinha e só reinicia. command: tunnel --no-autoupdate run depends_on: app: condition: service_healthy logging: driver: json-file options: max-size: "10m" max-file: "3" # Só a rede INTERNA. O cloudflared não publica porta nenhuma e alcança o # público porque ELE abre a conexão de saída pelos servidores da Cloudflare # — é justamente o que dispensa NAT e porta de entrada. networks: [internal] # O Caddy não é removido do compose (isso exigiria editar o arquivo base e # quebraria quem usa o padrão) — ele só é posto num profile que nunca é # ligado. ATENÇÃO: nomear o serviço explicitamente (`docker compose up -d # caddy`) ATIVA o profile dele e o contêiner sobe assim mesmo, tentando # publicar 80/443. É por isso que o update.sh confere REVERSE_PROXY antes de # recriar o proxy. caddy: profiles: ["caddy-nao-usado-com-proxy-externo"]