---
title: Runbook — WAHA em produção (VPS Hostgator)
status: canônico
last_review: 2026-09-17
owner: Rafael Melgaço
---

# Runbook — WAHA em produção (VPS Hostgator)

> Guia passo-a-passo pra subir, operar e recuperar a instância WAHA Plus em produção sobre VPS Hostgator. Hostgator é parceiro comercial; este runbook substitui qualquer doc histórico que mencionasse Hetzner.

---

## 1. Especificações alvo

| Item | Valor recomendado | Notas |
|---|---|---|
| Plano Hostgator | **VPS Turing** (ou superior) | Cartesius (1 vCPU/2GB) é insuficiente — puppeteer/baileys + 5+ sessões saturam. |
| OS | Ubuntu 22.04 LTS ou 24.04 LTS | NOWEB engine testado em ambos. CentOS funciona mas docs do compose pressupõem Debian-family. |
| CPU/RAM | mín. 2 vCPU / 4 GB RAM | NOWEB usa ~150 MB por sessão; +overhead Node ~300 MB. |
| Disco | mín. 80 GB SSD | Mídia inline mínima (vai pro Supabase Storage), mas `.sessions` cresce com histórico WhatsApp Web. |
| Datacenter | São Paulo (default Hostgator BR) | Latência <30ms pro Meta SP — relevante pra anti-banimento e UX de QR. |
| IP público | Estático (incluso no plano) | Necessário pra DNS A record + egress allowlist. |

> **Sem parceria com Hostgator?** Substitua por Hetzner CX22 (~$5/mês, datacenter EU) ou DigitalOcean Droplet 4GB (~$24/mês). Tudo neste runbook funciona idêntico — só não terá a vantagem de latência BR.

---

## 2. Pré-requisitos

1. Acesso SSH ao VPS (Hostgator entrega via cPanel ou root SSH; preferir SSH-only).
2. Domínio com DNS gerenciado em Cloudflare (ou outro provider) — ex.: `waha.deskcomm.com.br`.
3. Conta Backblaze B2 com bucket `deskcomm-waha-backup` (R$0,06/GB/mês ≈ $0.005/GB).
4. Licença ativa **WAHA Plus** (`https://waha.devlike.pro` — ~$30/mês).
5. Uma instalação do CRM já de pé, com `WAHA_API_BASE_URL`, `WAHA_API_KEY`, `WAHA_WEBHOOK_BASE_URL` e `WAHA_HMAC_SECRET` no `.env` dela (ainda apontando pra dev — atualizamos no fim).

---

## 3. Bootstrap inicial do VPS

### 3.1 Acessar e endurecer SSH

```bash
ssh -p 22022 root@<IP_DO_VPS>

# usuário não-root
adduser deskcomm
usermod -aG sudo deskcomm
mkdir -p /home/deskcomm/.ssh
cp ~/.ssh/authorized_keys /home/deskcomm/.ssh/
chown -R deskcomm:deskcomm /home/deskcomm/.ssh
chmod 700 /home/deskcomm/.ssh && chmod 600 /home/deskcomm/.ssh/authorized_keys

# desabilitar password auth + root login
sed -i 's/#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
systemctl reload sshd
```

A partir daqui: `ssh -p 22022 deskcomm@<IP>` + `sudo` para tudo.

### 3.2 Instalar dependências

```bash
sudo apt-get update && sudo apt-get upgrade -y
sudo apt-get install -y \
  docker.io docker-compose-plugin \
  nginx certbot python3-certbot-nginx \
  ufw fail2ban restic curl jq

sudo usermod -aG docker deskcomm
# logout + login pra grupo aplicar
```

### 3.3 Firewall

```bash
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22022/tcp  # SSH da HostGator escuta na 22022
sudo ufw allow 80/tcp     # certbot challenge
sudo ufw allow 443/tcp
sudo ufw enable
```

### 3.4 Fail2ban (SSH + Nginx 401)

```bash
sudo tee /etc/fail2ban/jail.d/deskcomm.conf > /dev/null <<'EOF'
[sshd]
enabled = true
maxretry = 3
bantime = 1h

[nginx-http-auth]
enabled = true
maxretry = 5
bantime = 1h
EOF
sudo systemctl restart fail2ban
```

---

## 4. DNS + TLS

No Cloudflare (ou seu DNS provider):

- `waha.deskcomm.com.br` → A record → IP do VPS Hostgator
- Proxy = **DNS only** (cinza). Cloudflare Proxy (laranja) interfere em SSE/WebSocket que o WAHA usa.

```bash
sudo certbot --nginx -d waha.deskcomm.com.br \
  --non-interactive --agree-tos -m ops@deskcomm.com.br
```

Certbot já injeta SSL no `/etc/nginx/sites-available/default`.

---

## 5. Deploy do WAHA

### 5.1 Estrutura

```bash
sudo mkdir -p /opt/deskcomm-waha
sudo chown deskcomm:deskcomm /opt/deskcomm-waha
cd /opt/deskcomm-waha
```

Copiar (via `scp` ou `git clone`) o `docker-compose.yml` do repo (raiz do DeskcommCRM). Ajustes obrigatórios pra prod:

```yaml
services:
  waha:
    image: devlikeapro/waha-plus@sha256:<DIGEST_PINADO>   # NÃO usar :latest
    ports:
      - "127.0.0.1:3000:3000"                             # bind localhost; Nginx termina TLS
    environment:
      WAHA_API_KEY: ${WAHA_API_KEY}                       # plaintext rotacionado
      WHATSAPP_HOOK_URL: ${WAHA_WEBHOOK_URL}              # rota COM token — ver 5.2
      WHATSAPP_HOOK_HMAC_KEY: ${WAHA_HMAC_SECRET}   # nome EXATO da doc do WAHA; sem o _KEY ele nunca assina
      WAHA_DEFAULT_ENGINE: NOWEB
      WAHA_DASHBOARD_ENABLED: "false"                     # sem dashboard em prod
```

> **Pin de digest**: rode `docker pull devlikeapro/waha-plus:latest && docker inspect ... | jq -r '.[0].RepoDigests[0]'` e cole. Cada upgrade vira PR explícito (a gente já comeu uma migração silenciosa de env var nesse repo).

### 5.2 .env de produção

`/opt/deskcomm-waha/.env` (chmod 600):

```bash
WAHA_API_KEY=<plaintext gerado novo, 64 chars hex>
WAHA_WEBHOOK_URL=https://<dominio-da-sua-instalacao-do-crm>/api/v1/webhooks/waha/<token-do-canal>
WAHA_HMAC_SECRET=<32 bytes random distinto da api key>
```

> **Rota com token, não a global.** A rota global (`/api/v1/webhooks/waha`, sem token)
> só atende a rede interna da stack do CRM e responde 404 a quem chega pelo domínio.
> WAHA em outro servidor entrega pela rota do canal: o `<token-do-canal>` é a coluna
> `webhook_path_token` da linha do canal em `channel_sessions`. É uma URL por canal —
> com mais de um número no mesmo WAHA, cada sessão precisa do seu webhook.

```bash
chmod 600 /opt/deskcomm-waha/.env
```

### 5.3 Subir

```bash
cd /opt/deskcomm-waha
docker compose up -d
docker compose logs -f waha       # confere "Nest application successfully started"
```

Healthcheck:

```bash
PLAIN=$(grep WAHA_API_KEY .env | cut -d= -f2-)
curl -s -H "X-Api-Key: $PLAIN" http://127.0.0.1:3000/api/sessions
# → [] (array vazio inicial)
```

---

## 6. Nginx reverse proxy

`/etc/nginx/sites-available/waha`:

```nginx
server {
    listen 443 ssl http2;
    server_name waha.deskcomm.com.br;

    ssl_certificate     /etc/letsencrypt/live/waha.deskcomm.com.br/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/waha.deskcomm.com.br/privkey.pem;

    # Egress allowlist — só o servidor onde o CRM roda pode chamar.
    include /etc/nginx/conf.d/crm-egress-allowlist.conf;
    deny all;

    proxy_buffering off;        # SSE / streaming WAHA
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;
    client_max_body_size 50M;   # uploads de mídia

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server {
    listen 80;
    server_name waha.deskcomm.com.br;
    return 301 https://$host$request_uri;
}
```

`/etc/nginx/conf.d/crm-egress-allowlist.conf` — o IP público do servidor onde o CRM roda:

```bash
echo "allow <IP_DO_SERVIDOR_DO_CRM>;" | sudo tee /etc/nginx/conf.d/crm-egress-allowlist.conf
sudo nginx -t && sudo systemctl reload nginx
```

> É um endereço só, escrito à mão: não há lista para sincronizar nem cron para manter. Quem o muda é você, ao mover o CRM de servidor — quando isso acontecer, reescreva este arquivo e recarregue o Nginx. Para descobrir o IP de saída, rode de dentro do próprio servidor do CRM: `curl -s https://ifconfig.me`.

```bash
sudo ln -s /etc/nginx/sites-available/waha /etc/nginx/sites-enabled/waha
sudo nginx -t && sudo systemctl reload nginx
curl -I https://waha.deskcomm.com.br/api/health    # 401 esperado sem header
```

---

## 7. Backup com restic + Backblaze B2

### 7.1 Setup

```bash
sudo tee /opt/deskcomm-waha/backup.env > /dev/null <<EOF
B2_ACCOUNT_ID=<key id>
B2_ACCOUNT_KEY=<key>
RESTIC_REPOSITORY=b2:deskcomm-waha-backup:/waha
RESTIC_PASSWORD=<senha forte armazenada no 1Password>
EOF
sudo chmod 600 /opt/deskcomm-waha/backup.env

source /opt/deskcomm-waha/backup.env
restic init    # uma vez só
```

### 7.2 Cron diário (3am)

`/etc/cron.d/waha-backup`:

```cron
0 3 * * * deskcomm . /opt/deskcomm-waha/backup.env && restic backup /var/lib/docker/volumes/deskcomm-waha_waha-data --tag daily && restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
```

### 7.3 Restore drill (rodar mensal)

```bash
# em VPS de teste:
docker compose down
sudo rm -rf /var/lib/docker/volumes/deskcomm-waha_waha-data/*
. /opt/deskcomm-waha/backup.env
restic restore latest --target /
docker compose up -d
# verificar que sessões voltaram sem precisar re-parear
```

Nunca confie em backup que você não testou restaurando.

---

## 8. Monitoramento

### 8.1 Healthcheck externo

UptimeRobot (free tier, 5min) apontando pra `https://waha.deskcomm.com.br/ping` (rota pública sem auth). Alerta → email + WhatsApp via webhook do próprio app.

### 8.2 Watchdog local

```bash
sudo tee /usr/local/bin/waha-watchdog.sh > /dev/null <<'EOF'
#!/bin/bash
FAILS=$(cat /tmp/waha-fails 2>/dev/null || echo 0)
if curl -s -f http://127.0.0.1:3000/ping > /dev/null; then
  echo 0 > /tmp/waha-fails
else
  FAILS=$((FAILS+1))
  echo $FAILS > /tmp/waha-fails
  if [ "$FAILS" -ge 3 ]; then
    cd /opt/deskcomm-waha && docker compose restart waha
    curl -s -X POST "https://hooks.sentry.io/..." -d "WAHA restarted após $FAILS falhas"
    echo 0 > /tmp/waha-fails
  fi
fi
EOF
sudo chmod +x /usr/local/bin/waha-watchdog.sh
echo "*/1 * * * * deskcomm /usr/local/bin/waha-watchdog.sh" | sudo tee /etc/cron.d/waha-watchdog
```

### 8.3 Log shipping (opcional Fase 2)

Better Stack ou Datadog Agent → `docker logs deskcomm-waha`. Sem isso, logs ficam só locais e somem em rotação.

---

## 9. Apontar o CRM para este WAHA

As variáveis do CRM vivem no `.env` da instalação dele — não num painel. Edite lá:

```
WAHA_API_BASE_URL=https://waha.deskcomm.com.br
WAHA_API_KEY=<mesmo plaintext do .env do VPS>
WAHA_WEBHOOK_BASE_URL=http://app:3000
WAHA_HMAC_SECRET=<mesmo do VPS>
```

Env var é lida no boot: editar o `.env` não muda nada até os contêineres serem recriados.

Não tente descobrir quais são eles com um `grep env_file`: esse grep não enxerga o
serviço `waha` do compose, que recebe `WAHA_HMAC_SECRET` por
interpolação (`${...}` dentro do bloco `environment:`), sem `env_file` nenhum — a
resposta sai incompleta com cara de completa.

Numa instalação real, quem recria tudo é `bash hostgator-setup-kit/update.sh`: ele roda
o `up -d` sem nomear serviço. Subindo à mão, o comando está em [`deploy.md`](deploy.md) —
numa VPS com proxy reverso próprio o `up -d` precisa dos **dois** arquivos de compose,
senão o domínio passa a responder 404.

---

## 10. Checklist final pré-go-live

- [ ] Digest pinado em `docker-compose.yml` (zero `:latest`)
- [ ] `WAHA_DASHBOARD_ENABLED=false`
- [ ] UFW ativo, só 22/80/443
- [ ] SSH password disabled, root login disabled
- [ ] fail2ban com jails de SSH + nginx-http-auth
- [ ] Egress allowlist Nginx com o IP do servidor do CRM
- [ ] TLS válido (testar `https://www.ssllabs.com/ssltest/` ≥ A)
- [ ] Backup `restic` rodando + restore drill executado uma vez
- [ ] UptimeRobot configurado
- [ ] Watchdog cron de 1min ativo
- [ ] Sentry release tagging do app capturando erros do `lib/waha/client`
- [ ] `.env` do CRM apontando pro domínio público do WAHA
- [ ] Webhook entrante funcionando (mensagem de teste WhatsApp → aparece na Inbox)
- [ ] API key WAHA documentada em 1Password com data de rotação +90d

---

## 11. Diferenças operacionais Hostgator vs cloud-native (Hetzner/DO)

| Aspecto | Hostgator | Hetzner / DO |
|---|---|---|
| Volume snapshots nativos | ❌ não tem | ✅ tem |
| Latência pro Meta BR | ✅ <30ms (SP) | ⚠️ 150-200ms (EU) ou 80ms (NYC) |
| Custo (mín. 2 vCPU/4GB) | R$140/mês (~$28) | $5-10/mês |
| Painel | cPanel/WHM (web GUI) | Console + API |
| Provisionamento via API | ❌ limitado | ✅ Terraform-friendly |
| Suporte 24x7 PT-BR | ✅ incluso | ⚠️ EN-only, ticket lento |

**Conclusão**: Hostgator paga prêmio pela parceria + suporte BR + datacenter SP. Pra MVP/scale-out até ~50 tenants é OK; acima disso, considerar diversificação (instância secundária Hetzner como DR cross-region).

---

## 12. Troubleshooting rápido

| Sintoma | Diagnóstico | Fix |
|---|---|---|
| App recebe 401 do WAHA | Env var `WAHA_API_KEY` desalinhada (`.env` do CRM vs `.env` do WAHA) | Confirmar plaintext idêntico nos dois lados |
| WAHA cria session mas não inicia | `start: true` ignorado em algumas versões | Chamar `POST /api/sessions/:name/start` explicitamente |
| Webhook não chega | Nginx allowlist bloqueando, ou Cloudflare Proxy ON | `tail -f /var/log/nginx/access.log` + desligar proxy CF |
| QR expira sempre | RTT alto, ou clock drift no VPS | `timedatectl set-ntp true`, conferir RTT pro `web.whatsapp.com` |
| Container OOMKilled | Sessões demais pro plano | Upgrade Hostgator pra plano superior, ou particionar tenants em VPS secundário |
| Session WORKING mas mensagens não saem | Daily limit atingido, ou janela horária | Conferir `channel_sessions.daily_message_limit` + `lib/waha/throttle` |

---

## 13. Histórico de decisão

- **2026-09-17** — O passo 9 deixou de mandar editar env var em painel de hospedagem: o CRM é self-host, e as variáveis dele vivem no `.env` da instalação. A allowlist de egress do Nginx passou a liberar o IP do servidor onde o CRM roda, no lugar dos ranges da plataforma — que eram atualizados por cron diário e sobrescreveriam qualquer ajuste manual.
- **2026-05-04** — Trocamos referências Hetzner→Hostgator nos docs por parceria comercial existente. Custo subiu (~$5 → ~$28) mas latência BR melhora pareamento e suporte ticketing fica em PT-BR. Hetzner mantido como plano B documentado em §1.
