Production Deployment
In production WhatsMax runs as two supervised processes plus one cron entry:
| Piece | Command | Lifetime |
|---|---|---|
| Web app | next start -p 3000 | Long-lived, under PM2. |
| Queue worker | node scripts/worker | Long-lived, under PM2. |
| Scheduler | node scripts/scheduler | One tick then exits — called every minute by cron. |
| WhatsApp QR gateway (optional) | node scripts/gateway | Long-lived, under PM2, exactly one instance. |
npm run worker and npm run scheduler are development commands
Both run under tsx watch, which restarts on file changes. In production use the extensionless launchers — node scripts/worker and node scripts/scheduler — as shown below.
Do not install with --omit=dev
The background launchers load TypeScript through tsx, which is a dev dependency. A production install must be a plain npm install.
1. Install PM2
sudo npm install -g pm2
2. Create the ecosystem file
Create ecosystem.config.cjs in the project root (/var/www/whatsmax):
module.exports = {
apps: [
{
name: "whatsmax-web",
script: "node_modules/next/dist/bin/next",
args: "start -p 3000",
cwd: __dirname,
instances: 1,
autorestart: true,
max_memory_restart: "1G",
env: { NODE_ENV: "production" },
},
{
name: "whatsmax-worker",
script: "scripts/worker",
interpreter: "node",
cwd: __dirname,
autorestart: true,
max_memory_restart: "1G",
env: { NODE_ENV: "production" },
},
// Only if you enable the optional WhatsApp QR channel
// (WHATSAPP_QR_ENABLED=true). Read the warning below first.
{
name: "whatsmax-gateway",
script: "scripts/gateway",
interpreter: "node",
cwd: __dirname,
instances: 1, // MUST stay 1 — see below
autorestart: true,
max_memory_restart: "1G",
kill_timeout: 25000, // let sockets close and auth state flush
env: { NODE_ENV: "production" },
},
],
}
The scheduler does not belong here
scripts/scheduler runs one tick and exits by design. Put it under PM2 and PM2 will treat every normal exit as a crash and restart-loop it. It goes in cron — step 4.
The gateway process (WhatsApp QR only)
Skip it entirely unless you have set WHATSAPP_QR_ENABLED=true. The official WhatsApp Cloud API channel does not use it.
Unlike the web and worker processes, the gateway is stateful: it holds one live WhatsApp socket per linked number.
Run exactly one gateway
Never set instances above 1, and never run the gateway on more than one server. Two gateways racing for the same WhatsApp session kick each other off repeatedly — the inbox stops working, and the constant reconnects look like abuse to WhatsApp, which is how numbers get banned.
The process takes an ownership lock in Redis as a safety net for deploy overlap, but that is not a way to scale out.
The gateway also requires Redis. Every other part of WhatsMax degrades gracefully when Redis is unavailable; the gateway deliberately refuses to start, because without the ownership lock it cannot know whether another process already holds a session.
3. Start the supervised processes
cd /var/www/whatsmax
pm2 start ecosystem.config.cjs
pm2 status
┌────┬─────────────────────┬─────────┬─────────┐
│ id │ name │ status │ memory │
├────┼─────────────────────┼─────────┼─────────┤
│ 0 │ whatsmax-web │ online │ 180 MB │
│ 1 │ whatsmax-worker │ online │ 120 MB │
└────┴─────────────────────┴─────────┴─────────┘
4. Add the scheduler cron entry
crontab -e
Add one line — adjust the path to your installation:
* * * * * cd /var/www/whatsmax && node scripts/scheduler >> /dev/null 2>&1
Every minute this runs the heartbeat plus any task whose cron expression matches that minute: scheduled campaigns, scheduled social posts, subscription reconciliation, cleanups.
Admin → System → Cron Setup shows the exact line for your installation (with your real path), a copy button, and a live scheduler heartbeat. Wait up to a minute after adding the entry and the heartbeat should flip to active.
5. Survive server reboots
pm2 save # remember the current process list
pm2 startup # prints a command — run the printed command with sudo
After running the printed sudo env PATH=... pm2 startup ... command, PM2 resurrects the web and worker processes on every reboot. The cron entry survives reboots on its own.
6. Day-to-day commands
pm2 status # health overview
pm2 logs whatsmax-web # tail web logs
pm2 logs whatsmax-worker # tail worker logs (campaign/AI jobs live here)
pm2 restart whatsmax-web # restart one process
pm2 restart ecosystem.config.cjs # restart everything
pm2 monit # live CPU/memory dashboard
Log rotation
PM2 keeps logs forever by default. Install the rotation module once:
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 14
7. Verify the background system
- Admin → System → Queue — pending work draining, failed work not climbing, the queue list populated.
- Admin → System → Cron Setup — the scheduler heartbeat recent, not
Never run.
If either looks dead, check pm2 logs whatsmax-worker and confirm REDIS_URL is reachable (redis-cli ping should answer PONG). See System & Health.
Scaling notes
- Web — set
instances: 2(or"max") withexec_mode: "cluster"once traffic grows. Sessions are stateless JWTs, so clustering is safe. - Worker — add more by duplicating the worker block under a new name. BullMQ distributes jobs automatically.
- Scheduler — exactly one cron entry, on exactly one host. Two hosts running it means every scheduled campaign goes out twice.
- Multiple app servers — require S3 storage (
STORAGE_DRIVER=s3) plus shared Redis and MySQL.
Next: put Nginx and SSL in front — Nginx & SSL.