docker-compose.cloudflared.yml
Rama main · réplica de lectura del repositorio original.
# 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=<token do túnel>
#
# 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"]