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/chromiumestá definido (já é o padrão da imagem, setado comoENVnoDockerfilee lido emsrc/index.ts→puppeteerPath) — se você buildou uma imagem customizada sem o Chromium doapt-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.
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.ymllocal, 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/sessionsno 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:
JWT_SECRETausente. É 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çaConfiguração inválida: ...e a API nunca chega a darlisten. Gere um valor longo e aleatório — nunca reaproveite entre ambientes.DATABASE_URLouREDIS_URLincorretas. Ambas também são obrigatórias emsrc/infra/config.ts(sem elas oloadConfig()já falha do mesmo jeito que oJWT_SECRET). Mesmo com o formato correto, oCMDda imagem rodanpx prisma migrate deployantes de iniciar o processo Node — se o PostgreSQL apontado porDATABASE_URLnã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):
- 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_PASSWORDcontinuem definidas; e - ambas
ADMIN_EMAILeADMIN_PASSWORDestã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/reportsolham 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/sentProactiveWindowusam 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:
reason | Gate | O que fazer |
|---|---|---|
Numero sem opt-in registrado | Opt-in | Registre 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 instancia | Blacklist | O número foi marcado via POST /instance/:name/report-block ou blacklist manual — force não contorna este gate. |
Numero nao existe no WhatsApp | Existência no WhatsApp | Confirme o número com POST /chat/whatsappNumbers antes de reenviar. |
Instancia em estado vermelho: envios proativos pausados | Circuit breaker | Ver 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.