Pular para o conteúdo principal

Troubleshooting

Sete problemas reais, com a causa provável e onde olhar primeiro. Se o seu problema não está aqui, comece pelos logs do container (docker logs ou o equivalente no EasyPanel) — eles são o sinal mais confiável disponível hoje, porque boa parte do fluxo ponta a ponta (pareamento por QR com Chromium real, envio real sob tráfego) ainda não foi validada em produção neste projeto (ver os avisos em Primeiros passos e Arquitetura).

Chromium não sobe

Sintoma: a instância fica presa em disconnected/erro ao chamar GET /instance/connect/:name, ou o log mostra o Puppeteer/Chromium falhando ao abrir o navegador (erro de sandbox, biblioteca .so ausente, ou timeout de lançamento).

Causa mais provável: falta de libs do sistema que o Chromium precisa para rodar headless dentro de um container node:24-slim (imagem base minimalista, sem as bibliotecas gráficas normalmente presentes num desktop Linux completo).

O Dockerfile já instala essas libs no estágio de runtime:

RUN apt-get update && apt-get install -y --no-install-recommends \
chromium ca-certificates fonts-liberation libnss3 libatk-bridge2.0-0 \
libatk1.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 \
libxrandr2 libgbm1 libpango-1.0-0 libasound2 curl \
&& rm -rf /var/lib/apt/lists/*

E o client já roda com as flags necessárias para o sandbox padrão do Chromium, que a maioria dos containers não permite — aplicadas internamente na fábrica do client, não configuráveis por env (src/core/realClientFactory.ts):

args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],

Onde olhar: os logs do container (docker logs <container> ou o painel de logs do EasyPanel) — o erro do Puppeteer aparece ali quando o Client.initialize() do whatsapp-web.js falha. Confirme também:

  • PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium está definido (já é o padrão da imagem, setado como ENV no Dockerfile e lido em src/index.tspuppeteerPath) — se você buildou uma imagem customizada sem o Chromium do apt-get, essa variável vai apontar para um binário que não existe.
  • Se estiver rodando fora da imagem oficial (ex.: outra base de container), confirme que todas as libs da lista acima estão instaladas.
Nunca validado com Chromium realmente conectado

O primeiro deploy real (2026-07-15) confirmou que a imagem builda com Chromium instalado, mas nenhuma sessão de WhatsApp Web chegou a conectar de fato (total: 0 instâncias) — o pareamento por QR Code com um Chromium real dentro do container ainda não foi observado ponta a ponta. Se você for a primeira pessoa a passar por isso, o log do container é a melhor fonte de diagnóstico disponível.

Container reinicia sozinho / erro de memória (OOM)

Sintoma: o container cai e sobe repetidamente (crash-loop), ou os logs/monitoramento do host mostram o processo Node morto por falta de memória (OOM killer).

Causa mais provável: MAX_INSTANCES configurado além do que a RAM disponível no container/VPS suporta. Cada instância conectada mantém seu próprio processo Chromium headless — o consumo por instância citado nesta documentação (~150–300MB) é uma estimativa, não uma medição sob carga real (ver FAQ). Mesmo abaixo do limite de MAX_INSTANCES (que só barra a criação de novas instâncias com 429 max_instances, não protege contra o consumo das já conectadas), múltiplos Chromiums ativos ao mesmo tempo podem estourar a RAM disponível.

Correção: reduza MAX_INSTANCES para um valor compatível com a RAM real do container, usando a tabela de referência em Deploy no EasyPanel § Dimensionamento de RAM como ponto de partida (e ajuste para cima ou para baixo observando o consumo real no seu ambiente, já que o número de referência não foi medido aqui). Um OOM derruba o container inteiro — todas as instâncias conectadas caem junto, não só a mais recente.

Pareamentos perdidos a cada deploy

Sintoma: toda instância volta para disconnected depois de um redeploy ou restart, e é preciso escanear o QR Code de novo para cada uma.

Causa: o volume /app/sessions (SESSIONS_PATH em src/infra/config.ts, também criado no Dockerfile com RUN mkdir -p /app/sessions /app/data) não está montado como volume persistente. Sem persistência, o diretório é recriado vazio a cada novo container, e a sessão do LocalAuth (whatsapp-web.js) some junto.

Correção:

  • No docker-compose.yml local, o volume já está declarado (sessions:/app/sessions) — confirme que não foi removido ou substituído por um bind mount temporário.
  • No EasyPanel, monte um volume persistente apontando para /app/sessions no serviço da API antes do primeiro pareamento em produção — ver o passo a passo em Deploy no EasyPanel § Volume persistente.

API não sobe

Sintoma: o container encerra logo na subida, sem chegar a responder GET /health.

Causas possíveis, na ordem mais provável:

  1. JWT_SECRET ausente. É uma variável obrigatória — o schema de validação de ambiente (src/infra/config.ts) exige no mínimo 16 caracteres (z.string().min(16, ...)), sem valor padrão. Se estiver ausente ou vazia, loadConfig() lança Configuração inválida: ... e a API nunca chega a dar listen. Gere um valor longo e aleatório — nunca reaproveite entre ambientes.
  2. DATABASE_URL ou REDIS_URL incorretas. Ambas também são obrigatórias em src/infra/config.ts (sem elas o loadConfig() já falha do mesmo jeito que o JWT_SECRET). Mesmo com o formato correto, o CMD da imagem roda npx prisma migrate deploy antes de iniciar o processo Node — se o PostgreSQL apontado por DATABASE_URL não estiver acessível, a migration falha e o container não sobe. Confirme que PostgreSQL e Redis estão no ar e acessíveis pela rede do container antes de subir a API (ver FAQ § Preciso de PostgreSQL e Redis?).

Onde olhar: a mensagem de erro de Configuração inválida: ... aparece nos primeiros logs do container, antes de qualquer log de listen — é o sinal mais rápido para distinguir "variável de ambiente faltando" de "banco/Redis inacessível" (este último aparece como erro do Prisma/prisma migrate deploy, não como Configuração inválida).

Não consigo entrar no portal (login falha / nenhum usuário existe)

Sintoma: a tela de login do Portal de Controle (/panel) não aceita nenhuma credencial, e não existe nenhum jeito óbvio de criar o primeiro usuário.

Causa: o admin inicial só é semeado automaticamente no primeiro boot, e só se duas condições forem verdadeiras ao mesmo tempo (AuthService.semearPrimeiroAdmin, src/services/AuthService.ts):

  1. a tabela de usuários está vazia (zero linhas) — depois que existe pelo menos um usuário, o seeder nunca mais roda, mesmo que ADMIN_EMAIL/ADMIN_PASSWORD continuem definidas; e
  2. ambas ADMIN_EMAIL e ADMIN_PASSWORD estão definidas no ambiente — sem as duas, nenhum admin é criado (não existe senha padrão fixa no código).

Correção: defina ADMIN_EMAIL e ADMIN_PASSWORD no ambiente antes do primeiro boot da aplicação contra um banco vazio. Se o banco já tem alguma linha em User (mesmo de um teste anterior) e ninguém sabe a senha, será preciso criar/editar o usuário diretamente no PostgreSQL — o seeder automático não vai rodar de novo. Detalhes em Portal de Controle § Primeiro admin.

Instância presa em vermelho

Sintoma: GET /instance/:name/metrics mostra healthState: "red" e os envios proativos continuam pausados por vários dias seguidos, mesmo sem incidentes novos aparentes.

Causa: o estado vermelho é um kill switch por janela, não um latch permanente — ele se recupera conforme os sinais que o dispararam envelhecem, mas as janelas usadas não são todas iguais (MetricsService.recomputeHealth, ver Guia anti-ban § Estados de saúde):

  • blocks/reports olham só a janela de hoje (1 dia, timezone da instância) — se pararem de chegar, esse gatilho some no dia seguinte.
  • sent/replied (taxa de resposta) usam janela de 7 dias.
  • noReply48h/sentProactiveWindow usam uma janela deslizante entre 48 horas e 7 dias.

Se a instância parece presa por muito mais tempo do que isso, o sinal que está mantendo o vermelho provavelmente continua sendo alimentado — por exemplo, recentDisconnects (incrementado a cada disconnected; decai 1 por rodada de 5 minutos enquanto a instância está conectada, não zera de uma vez na reconexão) continua subindo se a instância está caindo e reconectando em loop, ou POST /instance/:name/report-block está sendo chamado repetidamente contra a mesma instância.

Correção: consulte GET /instance/:name/metrics para ver qual condição específica está ativa (reports >= 3, blocks >= 10, recentDisconnects >= 5, ou sentProactiveWindow >= 20 e noReplyRate >= 0.95) e trate a causa raiz — não existe um jeito de "forçar" o estado de volta para verde manualmente, além de parar o sinal que está disparando o gatilho e esperar a janela envelhecer.

Envio retorna rejected

Sintoma: POST /message/sendText (ou qualquer outra rota de envio) devolve { status: "rejected", reason: "..." } em vez de enfileirar a mensagem.

O que isso significa: rejected só acontece com proactive: true — um dos quatro gates binários do AntiBanGuard recusou o envio antes de ele entrar na fila (src/core/AntiBanGuard.ts, evaluateProactive). O campo reason diz exatamente qual gate barrou:

reasonGateO que fazer
Numero sem opt-in registradoOpt-inRegistre o consentimento com POST /optin antes de enviar, ou use force: true só se o consentimento existir por outro canal (ver Guia anti-ban § force).
Numero em blacklist da instanciaBlacklistO número foi marcado via POST /instance/:name/report-block ou blacklist manual — force não contorna este gate.
Numero nao existe no WhatsAppExistência no WhatsAppConfirme o número com POST /chat/whatsappNumbers antes de reenviar.
Instancia em estado vermelho: envios proativos pausadosCircuit breakerVer a seção Instância presa em vermelho acima.

Um envio recusado nunca chega à fila — não confunda com queued (enfileirado, envio real ainda não aconteceu) ou scheduled (adiado para mais tarde/próxima janela, sem ser recusado). Nenhum dos três status, inclusive queued, significa "sent" — a API nunca devolve isso, porque o envio de fato acontece depois, no worker (ver Arquitetura § Fluxo de um envio proativo). Envios reativos (proactive: false) nunca retornam rejected, porque não passam por nenhum dos seis gates.