Deployment
How to run your bot in production, always connected.
ESM-only, Node >= 22
@botmesh/core and the transport packages are pure ESM — no CommonJS build. Make sure your production package.json has "type": "module" (or your entrypoint uses .mjs), and that the runtime is Node >= 22.
Option 1: VPS (recommended)
1. Prepare the server
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
npm install -g pnpm
git clone https://github.com/your-user/my-bot.git
cd my-bot
pnpm install2. First connection (scan QR)
node bot.js
# Scan the QR, then Ctrl+CThe session is saved in ./sessions/. You don't need to scan again.
3. Run with PM2 (always on)
npm install -g pm2
pm2 start bot.js --name my-bot
pm2 save
pm2 startup # follow instructions for auto-start on bootPM2 commands
pm2 status # status
pm2 logs my-bot # logs
pm2 restart my-bot # restart
pm2 stop my-bot # stopOption 2: Docker
Dockerfile
FROM node:22-slim
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN mkdir -p /app/sessions
CMD ["node", "bot.js"]docker-compose.yml
version: '3.8'
services:
bot:
build: .
restart: unless-stopped
volumes:
- ./sessions:/app/sessions
- ./store.json:/app/store.json
environment:
- NODE_ENV=productiondocker compose up -d
docker compose logs -f # scan QR first timeOption 3: Screen / Tmux
screen -S my-bot
node bot.js
# Detach: Ctrl+A, then D
# Reattach: screen -r my-botConsiderations
Session persistence (auth)
The WhatsApp login credentials are saved under sessionsDir/session (defaults to ./sessions/<session>) by the Baileys provider. Don't delete it or you'll need to scan the QR again:
const bot = new Bot({
provider: new BaileysProvider({ session: 'my-bot', sessionsDir: './sessions' }),
})Store persistence (conversations & history)
The default MemoryStore lives in memory only. For production, use SQLiteStore — a real database that persists messages, chats, contacts, flow state, checkpoints and AI conversations:
import { Bot } from '@botmesh/core'
import { BaileysProvider } from '@botmesh/baileys'
import { SQLiteStore } from '@botmesh/sqlite-store'
const bot = new Bot({
provider: new BaileysProvider({ session: 'my-bot' }),
store: new SQLiteStore({ file: './data/bot.db' }),
})With SQLiteStore, flow(...).persistent() checkpoints survive restarts automatically. See the Persistent Store guide for details.
Back up both directories/files together — they're what let a redeployed bot resume without re-scanning the QR or losing in-flight conversations:
tar -czf backup.tar.gz sessions/ data/
tar -xzf backup.tar.gzGraceful shutdown
bot.stop() tears down the provider and aborts active conversations. Use { drain: true } to wait for in-flight sends before closing (prevents lost messages during deploys):
process.on('SIGINT', async () => {
console.log('Shutting down gracefully...')
await bot.stop({ drain: true, timeout: '15s' }) // wait up to 15s for pending sends (ms also work)
process.exit(0)
})
process.on('SIGTERM', async () => {
await bot.stop({ drain: true, timeout: 15_000 })
process.exit(0)
})| Option | Type | Default | Description |
|---|---|---|---|
drain | boolean | false | Wait for in-flight sends to complete |
timeout | number | 10000 | Max ms to wait for drain before forcing stop |
Replies still being sent when stop() begins, and those the provider already accepted, are still recorded in the message log (after-send hooks and the store write, up to 5 s) before the store is closed, with or without drain.
After the drain, stop() leaves nothing running behind it: messages still queued for a chat are dropped instead of being handled, no new conversation starts until start(), paced replies still waiting are dropped ({ skipped: true }) and running ctx.stream()s are cancelled without touching the transport again (no final flush, edit or deletion; they resolve with cancelled: true). No timer of the bot keeps the process alive afterwards; pnpm bench:soak checks this under load (see CONTRIBUTING.md).
Logging
const bot = new Bot({
provider: new BaileysProvider({ session: 'my-bot', debug: false }), // true for verbose logs
debug: false, // Bot logger: `warn` by default, `debug` with true
})Without logger or debug, the Bot's built-in pino logger runs at warn, so warnings from plugins and handlers (e.g. a failed AI autoreply) are visible. Pass debug: true for debug logs, or inject your own logger.
Next steps
- Running N replicas — several processes of one bot (failover, webhook throughput)
- FAQ — common problems
- API Reference — technical docs