Files
DeskcommCRM/docker-compose.traefik.yml
Rafael MelgaçoandClaude Opus 5 1d963a0132 fix(kit): o ramo do Traefik em modo host nunca executava
O diagnóstico da issue #139 está certo: numa VPS Hostinger o Traefik da
hospedagem roda com `--network host`, e a detecção gravava TRAEFIK_NETWORK="host"
— uma rede de driver host, inutilizável. A correção anterior, porém, não chegava
a rodar, e onde rodava não subia.

1. Ramo inalcançável. `traefik_container` só era atribuído dentro do `case` de
   `decide_proxy`, que só devolve "traefik" quando alguém casa `:80->`/`:443->`
   na coluna Ports do `docker ps`. Medido no docker 28.3.2: contêiner em
   `--network host` sai com Ports=[] mesmo subido com `-p 80:80` (o daemon avisa
   "Published ports are discarded when using host network mode"); controle
   positivo, o mesmo nginx numa bridge com `-p 8080:80` sai com
   "0.0.0.0:8080->80/tcp". Ou seja: um Traefik em modo host nunca virava
   `traefik_container`. Na VPS real o instalador morria no painel "porta 80 já
   ocupada", que manda pôr REVERSE_PROXY=traefik no .env — caminho que pulava o
   `case` e morria adiante em "Não consegui descobrir a rede do seu Traefik".
   Agora há uma segunda varredura (`docker ps --filter network=host`) e a
   atribuição de `traefik_container` saiu de dentro do `case`. As duas falham
   FECHADO no plural: com dois Traefiks ninguém é eleito.

2. A rede escolhida não existia. O ramo gravava `<projeto>_internal`, criada
   pelo `dc up -d` lá na frente — e o compose declara TRAEFIK_NETWORK como rede
   EXTERNA, checada ANTES de criar qualquer coisa. Medido com o compose v2.38.2
   contra o próprio docker-compose.traefik.yml: `up -d` morre em "network
   crmreal_internal declared as external, but could not be found". Só instalava
   quem já tivesse instalado antes. Agora o instalador usa uma bridge dedicada
   `<projeto>_proxy` e a CRIA antes de precisar dela — medido: contêiner em
   `--network host` alcança por IP um contêiner numa bridge separada (HTTP 200),
   então qualquer bridge serve, e uma separada evita colidir com a rede que o
   compose cria e não expõe o redis sem senha da `internal`. O guard segue
   recusando driver != bridge e rede inexistente que não seja a nossa, e a
   mensagem parou de mandar quem está em modo host procurar "a rede do seu
   Traefik", que não existe.

3. `basename "$PWD"` cru ignorava `nome_do_projeto_compose()`, que existe no
   mesmo arquivo para replicar o NormalizeProjectName do compose. Numa pasta
   `CRM.Host_Teste` o instalador criaria `CRM.Host_Teste_proxy` e o compose
   procuraria `crmhost_teste_proxy`.

Os comentários que a correção anterior inverteu foram reescritos com os DOIS
cenários lado a lado, sem apagar a medição antiga do Traefik em bridge própria
(HTTP 000 x HTTP 200). Em docker-compose.traefik.yml some a menção a
TRAEFIK_DOCKER_NETWORK — variável que não existe em lugar nenhum do repo.

Testes: os fixtures do kit só tinham proxy com porta publicada, então o cenário
da issue nunca era exercitado. Entram 15 casos de unidade e um teste de
integração que roda o install.sh inteiro contra um `docker` dublê que imita a
Hostinger. Provado reprovando: contra o código anterior o teste de integração cai
em "As portas 80 e 443 já estão ocupadas por um programa do próprio servidor" —
a mensagem da VPS real. Cada asserção foi sabotada em separado e mordeu a sua.

O dublê passou a incluir `crontab`: o teste vai até o fim do instalador, e sem
isso a suíte agendava cron na máquina de quem a roda, apontando para o diretório
temporário que ela mesma apaga.

Refs #139

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HstH7nNmrTvCtZsasppeHj
2026-08-05 11:59:00 -03:00

144 lines
7.9 KiB
YAML

# DeskcommCRM — override para VPS que JÁ TEM um proxy reverso Traefik.
#
# docker compose -f docker-compose.prod.yml -f docker-compose.traefik.yml up -d
#
# QUANDO USAR
# -----------
# O stack padrão sobe um Caddy que publica as portas 80 e 443. Isso é o certo
# num VPS "cru". Mas várias hospedagens entregam a VPS com um Traefik próprio
# já ocupando essas portas — Hostinger, Coolify, Dokploy, CapRover e afins. É
# esse Traefik que dá HTTPS automático a tudo que o painel instala; desligá-lo
# quebraria as automações da hospedagem, presentes e futuras.
#
# Dois processos não podem ouvir a mesma porta, então o `up -d` padrão falha no
# bind e a instalação morre no meio. Com este override:
#
# 1. o serviço `caddy` ganha um profile que nunca é ativado — o compose
# simplesmente não o cria (o Traefik da hospedagem faz o papel dele);
# 2. o serviço `app` ganha labels do Traefik que reproduzem, uma a uma, as
# regras do Caddyfile.
#
# O comportamento padrão (sem este arquivo) fica EXATAMENTE como antes: quem
# não passa o `-f docker-compose.traefik.yml` não é afetado por nada aqui.
#
# EQUIVALÊNCIA COM O Caddyfile
# ----------------------------
# - `reverse_proxy app:3000` → router `deskcomm` + loadbalancer.server.port
# - `encode gzip` → middleware `deskcomm-compress`
# - `respond @waha_global 403`→ router `deskcomm-waha-block` (ver abaixo)
#
# Os timeouts longos que o Caddyfile dá a /api/internal/agents/run* NÃO precisam
# de equivalente: no Traefik o writeTimeout e o responseHeaderTimeout já são
# ilimitados por padrão, então uma resposta de 5 minutos passa inteira.
#
# PRÉ-REQUISITOS NO TRAEFIK DA HOSPEDAGEM
# ---------------------------------------
# Espera-se um entrypoint `websecure` (443) e um certresolver `letsencrypt` —
# os nomes padrão dessas instalações. Se a sua usa outros nomes, ajuste
# TRAEFIK_ENTRYPOINT e TRAEFIK_CERTRESOLVER no .env.
#
# A REDE onde o Traefik encontra o app é TRAEFIK_NETWORK, e ela varia por
# instalação. O install.sh a descobre e grava no .env — são dois cenários com
# respostas OPOSTAS, ambos medidos (o porquê de cada um está na função
# `rede_do_traefik`, em hostgator-setup-kit/install.sh):
#
# Traefik numa bridge PRÓPRIA (Coolify, Dokploy) → a rede DELE, que já existe.
# Traefik em `--network host` (Hostinger) → uma bridge "<projeto>_proxy"
# que o install.sh CRIA. Em modo host o proxy não está em rede nenhuma do
# Docker, e alcança qualquer bridge pela stack do host.
#
# O default abaixo ("traefik") cobre a instalação manual mais comum.
services:
app:
# Só o `app` entra na rede do proxy. O inverso — juntar o Traefik à rede
# `internal` — colocaria um contêiner que serve OUTROS tenants da VPS dentro
# de uma rede onde o redis roda sem senha (docker-compose.prod.yml: `redis-server
# --save '' --appendonly no`, sem requirepass). waha, redis e srh ficam de fora.
networks:
- internal
- proxy
labels:
traefik.enable: "true"
# Este label diz ao Traefik em qual rede pegar o IP do contêiner. Com o
# Traefik numa bridge própria, é a rede do PROXY — apontando para a
# `internal` ele mira um IP que não alcança e a requisição pendura até dar
# timeout, mesmo com o contêiner já conectado nas duas redes. Medido com
# Traefik v3.3 real:
# app só na internal, label=internal -> HTTP 000 (timeout)
# app nas duas redes, label=internal -> HTTP 000 (ainda o IP errado)
# app nas duas redes, label=proxy -> HTTP 200
# Com o Traefik em modo host a conclusão não se inverte, ela se generaliza:
# continua sendo a rede `proxy` — só que lá ela é a bridge que o install.sh
# cria para este projeto, porque um proxy em modo host não tem rede própria
# (e alcança qualquer bridge pela stack do host).
traefik.docker.network: "${TRAEFIK_NETWORK:-traefik}"
# Rota principal — equivale ao `reverse_proxy app:3000` do Caddyfile.
traefik.http.routers.deskcomm.rule: "Host(`${DOMAIN}`)"
traefik.http.routers.deskcomm.entrypoints: "${TRAEFIK_ENTRYPOINT:-websecure}"
traefik.http.routers.deskcomm.tls.certresolver: "${TRAEFIK_CERTRESOLVER:-letsencrypt}"
traefik.http.routers.deskcomm.middlewares: "deskcomm-compress"
traefik.http.services.deskcomm.loadbalancer.server.port: "3000"
# `encode gzip` do Caddyfile.
traefik.http.middlewares.deskcomm-compress.compress: "true"
# `respond @waha_global 403` do Caddyfile. O webhook GLOBAL do WAHA não
# pode ser alcançável da internet: quem soubesse o endereço injetaria
# mensagem falsa no CRM e faria o WhatsApp do dono responder a um número
# arbitrário. Aqui dentro o WAHA fala com o app pela rede do Docker, então
# esta rota nunca precisou ser pública.
#
# O Traefik não tem um "responda 403" direto. O equivalente é um
# ipallowlist cuja única faixa permitida é 192.0.2.1/32 — TEST-NET-1
# (RFC 5737), um endereço reservado para documentação que nenhum cliente
# real tem. Toda requisição àquele caminho cai fora da lista e recebe 403,
# que é exatamente o comportamento desejado. A prioridade alta é o que faz
# esta rota vencer a principal, que casaria o mesmo Host.
traefik.http.routers.deskcomm-waha-block.rule: "Host(`${DOMAIN}`) && Path(`/api/v1/webhooks/waha`)"
traefik.http.routers.deskcomm-waha-block.entrypoints: "${TRAEFIK_ENTRYPOINT:-websecure}"
traefik.http.routers.deskcomm-waha-block.tls.certresolver: "${TRAEFIK_CERTRESOLVER:-letsencrypt}"
traefik.http.routers.deskcomm-waha-block.priority: "1000"
traefik.http.routers.deskcomm-waha-block.service: "deskcomm"
traefik.http.routers.deskcomm-waha-block.middlewares: "deskcomm-deny"
traefik.http.middlewares.deskcomm-deny.ipallowlist.sourcerange: "192.0.2.1/32"
# HTTP -> HTTPS. O Caddy fazia isto automaticamente; no Traefik precisa ser
# dito. Sem este router, quem digitar http:// não recebe resposta nenhuma.
traefik.http.routers.deskcomm-http.rule: "Host(`${DOMAIN}`)"
traefik.http.routers.deskcomm-http.entrypoints: "${TRAEFIK_ENTRYPOINT_HTTP:-web}"
traefik.http.routers.deskcomm-http.middlewares: "deskcomm-https"
traefik.http.routers.deskcomm-http.service: "deskcomm"
traefik.http.middlewares.deskcomm-https.redirectscheme.scheme: "https"
traefik.http.middlewares.deskcomm-https.redirectscheme.permanent: "true"
# 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. Sem profile ativo, o compose não cria o contêiner e as portas 80/443
# seguem com o Traefik da hospedagem.
#
# ATENÇÃO a um detalhe do Compose: nomear o serviço explicitamente
# (`docker compose up -d caddy`) ATIVA o profile dele automaticamente e o
# contêiner sobe assim mesmo, indo bater de frente com o Traefik. Por isso o
# update.sh checa REVERSE_PROXY antes de recriar o proxy.
caddy:
profiles: ["caddy-nao-usado-com-proxy-externo"]
# `external: true` = quem cria esta rede não é o compose. Nos dois cenários ela
# já existe quando o `up -d` roda: ou é a rede do proxy da hospedagem (coolify,
# dokploy-network, traefik...), ou é a bridge que o install.sh criou para este
# projeto quando o Traefik está em modo host.
#
# Rede externa AUSENTE é recusada ANTES de o compose criar qualquer coisa —
# medido com o compose v2.38.2 contra este mesmo arquivo: "network X declared as
# external, but could not be found", sem dizer de onde saiu o nome. Vale
# inclusive quando X é a `<projeto>_internal` que o próprio compose criaria neste
# mesmo `up` (a checagem de rede externa vem antes de qualquer criação); por isso
# a bridge do modo host é uma rede separada, e o install.sh a cria antes de subir
# a stack.
networks:
proxy:
external: true
name: "${TRAEFIK_NETWORK:-traefik}"