Skip to content

Production Deployment Guide

Learn how to deploy your WhatsApp applications to production with proper security, monitoring, scaling, and best practices.

Overview

Deploying a WhatsApp application requires careful consideration of security, reliability, performance, and monitoring. This guide covers deployment strategies for various platforms and environments.

Pre-Deployment Checklist

Before deploying to production, ensure you have:

  • Environment Variables: All secrets configured securely
  • Error Handling: Proper error handling and logging
  • Testing: Comprehensive test coverage
  • Monitoring: Logging and monitoring setup
  • Rate Limiting: API rate limiting configured
  • Database: Production database configured
  • Backup Strategy: Database backup plan
  • SSL/TLS: HTTPS enabled for webhooks
  • Documentation: API documentation up to date
  • Scaling Strategy: Auto-scaling configured if needed

Environment Configuration

Environment Variables

Create separate environment files for each environment:

.env.production
NODE_ENV=production
# WhatsApp API
CLOUD_API_ACCESS_TOKEN=your_production_token
WA_PHONE_NUMBER_ID=your_production_phone_id
WA_BUSINESS_ACCOUNT_ID=your_production_business_id
WHATSAPP_WEBHOOK_VERIFICATION_TOKEN=your_webhook_token
# Database
DATABASE_URL=postgresql://user:password@host:5432/prod_db
# Security
API_SECRET=your_strong_api_secret
SESSION_SECRET=your_session_secret
# Monitoring
SENTRY_DSN=your_sentry_dsn
LOG_LEVEL=info
# Performance
MAX_RETRY_ATTEMPTS=3
REQUEST_TIMEOUT=30000
RATE_LIMIT_WINDOW=60000
RATE_LIMIT_MAX=100

Secret Management

Using Environment Variables (Vercel, Railway)

Terminal window
# Set via CLI
vercel env add CLOUD_API_ACCESS_TOKEN
# Or via dashboard
# Navigate to Project Settings → Environment Variables

Using AWS Secrets Manager

lib/config/secrets.ts
import { SecretsManager } from '@aws-sdk/client-secrets-manager';
const secretsManager = new SecretsManager({
region: process.env.AWS_REGION || 'us-east-1',
});
export async function getSecret(secretName: string): Promise<any> {
try {
const response = await secretsManager.getSecretValue({
SecretId: secretName,
});
if (response.SecretString) {
return JSON.parse(response.SecretString);
}
throw new Error('Secret not found');
} catch (error) {
console.error('Failed to retrieve secret:', error);
throw error;
}
}
// Usage
export async function loadSecrets() {
const secrets = await getSecret('whatsapp-api-secrets');
process.env.CLOUD_API_ACCESS_TOKEN = secrets.CLOUD_API_ACCESS_TOKEN;
process.env.WA_PHONE_NUMBER_ID = secrets.WA_PHONE_NUMBER_ID;
// ... other secrets
}

Using HashiCorp Vault

lib/config/vault.ts
import vault from 'node-vault';
const vaultClient = vault({
apiVersion: 'v1',
endpoint: process.env.VAULT_ADDR,
token: process.env.VAULT_TOKEN,
});
export async function getVaultSecrets() {
const response = await vaultClient.read('secret/data/whatsapp-api');
return response.data.data;
}

Docker Deployment

Dockerfile

Create an optimized production Dockerfile:

# Multi-stage build for smaller image size
FROM node:20-alpine AS builder
# Install pnpm
RUN npm install -g pnpm
# Set working directory
WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy source code
COPY . .
# Build application
RUN pnpm build
# Prune dev dependencies
RUN pnpm prune --prod
# Production stage
FROM node:20-alpine AS runner
# Install pnpm
RUN npm install -g pnpm
# Create app user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 whatsapp
WORKDIR /app
# Copy built application from builder
COPY --from=builder --chown=whatsapp:nodejs /app/dist ./dist
COPY --from=builder --chown=whatsapp:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=whatsapp:nodejs /app/package.json ./
# Switch to non-root user
USER whatsapp
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Start application
CMD ["node", "dist/server.js"]

Docker Compose

Production-ready docker-compose.yml:

version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: whatsapp-api
restart: unless-stopped
ports:
- "3000:3000"
environment:
NODE_ENV: production
DATABASE_URL: postgresql://postgres:password@postgres:5432/whatsapp
env_file:
- .env.production
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- whatsapp-network
volumes:
- ./logs:/app/logs
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
postgres:
image: postgres:16-alpine
container_name: whatsapp-postgres
restart: unless-stopped
environment:
POSTGRES_DB: whatsapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./backups:/backups
networks:
- whatsapp-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: whatsapp-redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- whatsapp-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
nginx:
image: nginx:alpine
container_name: whatsapp-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- app
networks:
- whatsapp-network
volumes:
postgres_data:
redis_data:
networks:
whatsapp-network:
driver: bridge

Nginx Configuration

nginx.conf
events {
worker_connections 1024;
}
http {
upstream whatsapp_api {
server app:3000;
}
# Rate limiting
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
listen 80;
server_name api.example.com;
# Redirect to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name api.example.com;
# SSL configuration
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Webhook endpoint (no rate limit)
location /webhook {
proxy_pass http://whatsapp_api;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
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;
}
# API endpoints (with rate limit)
location /api {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://whatsapp_api;
proxy_http_version 1.1;
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;
}
# Health check
location /health {
proxy_pass http://whatsapp_api;
access_log off;
}
}
}

Cloud Platform Deployments

Vercel (Next.js)

1. Install Vercel CLI

Terminal window
pnpm add -g vercel

2. Configure vercel.json

{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/next"
}
],
"env": {
"NODE_ENV": "production"
},
"regions": ["iad1"],
"functions": {
"app/api/**/*": {
"maxDuration": 30
}
}
}

3. Deploy

Terminal window
# Deploy to production
vercel --prod
# Set environment variables
vercel env add CLOUD_API_ACCESS_TOKEN production
vercel env add WA_PHONE_NUMBER_ID production
vercel env add DATABASE_URL production

4. Custom Domain

Terminal window
# Add custom domain
vercel domains add api.yourdomain.com
# Verify DNS
vercel domains verify api.yourdomain.com

Railway

1. Install Railway CLI

Terminal window
npm i -g @railway/cli
railway login

2. Create railway.json

{
"$schema": "https://railway.app/railway.schema.json",
"build": {
"builder": "NIXPACKS",
"buildCommand": "pnpm build"
},
"deploy": {
"startCommand": "pnpm start",
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 10
}
}

3. Deploy

Terminal window
# Initialize project
railway init
# Add environment variables
railway variables set CLOUD_API_ACCESS_TOKEN=your_token
railway variables set WA_PHONE_NUMBER_ID=your_id
# Deploy
railway up

4. Add PostgreSQL

Terminal window
railway add postgresql

Render

1. Create render.yaml

services:
- type: web
name: whatsapp-api
env: node
buildCommand: pnpm install && pnpm build
startCommand: pnpm start
healthCheckPath: /health
envVars:
- key: NODE_ENV
value: production
- key: CLOUD_API_ACCESS_TOKEN
sync: false
- key: WA_PHONE_NUMBER_ID
sync: false
- key: DATABASE_URL
fromDatabase:
name: whatsapp-db
property: connectionString
databases:
- name: whatsapp-db
databaseName: whatsapp
user: whatsapp

2. Deploy

Terminal window
# Connect GitHub repo and deploy automatically
# Or use Render CLI
render up

AWS (EC2 with Docker)

1. Launch EC2 Instance

Terminal window
# Create security group
aws ec2 create-security-group \
--group-name whatsapp-api-sg \
--description "WhatsApp API security group"
# Allow SSH, HTTP, HTTPS
aws ec2 authorize-security-group-ingress \
--group-id sg-xxx \
--protocol tcp --port 22 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress \
--group-id sg-xxx \
--protocol tcp --port 80 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress \
--group-id sg-xxx \
--protocol tcp --port 443 --cidr 0.0.0.0/0

2. Deploy with Docker

Terminal window
# SSH into instance
ssh -i your-key.pem ec2-user@your-instance-ip
# Install Docker
sudo yum update -y
sudo yum install docker -y
sudo service docker start
sudo usermod -a -G docker ec2-user
# Install Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# Clone your repository
git clone your-repo-url
cd your-repo
# Copy environment file
cp .env.production .env
# Deploy
docker-compose up -d

3. Setup Automatic Deployment

# Create deployment script
cat > deploy.sh << 'EOF'
#!/bin/bash
cd /home/ec2-user/whatsapp-api
git pull origin main
docker-compose down
docker-compose up -d --build
docker system prune -f
EOF
chmod +x deploy.sh
# Setup cron for auto-updates (optional)
crontab -e
# Add: 0 2 * * * /home/ec2-user/whatsapp-api/deploy.sh

Monitoring and Logging

Application Logging

lib/logging/logger.ts
import winston from 'winston';
import { format } from 'winston';
const { combine, timestamp, json, errors, printf } = format;
// Custom format for development
const devFormat = printf(({ level, message, timestamp, ...metadata }) => {
let msg = `${timestamp} [${level}] ${message}`;
if (Object.keys(metadata).length > 0) {
msg += ` ${JSON.stringify(metadata)}`;
}
return msg;
});
// Create logger
export const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: combine(
timestamp(),
errors({ stack: true }),
process.env.NODE_ENV === 'production' ? json() : devFormat
),
defaultMeta: {
service: 'whatsapp-api',
environment: process.env.NODE_ENV,
},
transports: [
// Console logging
new winston.transports.Console(),
// File logging
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
maxsize: 10485760, // 10MB
maxFiles: 5,
}),
new winston.transports.File({
filename: 'logs/combined.log',
maxsize: 10485760,
maxFiles: 5,
}),
],
});
// Add CloudWatch transport for AWS
if (process.env.AWS_CLOUDWATCH_GROUP) {
const CloudWatchTransport = require('winston-cloudwatch');
logger.add(new CloudWatchTransport({
logGroupName: process.env.AWS_CLOUDWATCH_GROUP,
logStreamName: `${process.env.NODE_ENV}-${new Date().toISOString().split('T')[0]}`,
awsRegion: process.env.AWS_REGION || 'us-east-1',
}));
}

Error Tracking with Sentry

lib/monitoring/sentry.ts
import * as Sentry from '@sentry/node';
import { nodeProfilingIntegration } from '@sentry/profiling-node';
export function initializeSentry() {
if (!process.env.SENTRY_DSN) {
console.warn('Sentry DSN not configured');
return;
}
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
integrations: [
nodeProfilingIntegration(),
],
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
profilesSampleRate: 1.0,
});
console.log('✅ Sentry initialized');
}
export function captureError(error: Error, context?: Record<string, any>) {
logger.error('Error occurred', { error: error.message, ...context });
Sentry.captureException(error, {
extra: context,
});
}

Performance Monitoring

lib/monitoring/metrics.ts
import { Counter, Histogram, register } from 'prom-client';
// Message counters
export const messagesSent = new Counter({
name: 'whatsapp_messages_sent_total',
help: 'Total number of messages sent',
labelNames: ['type', 'status'],
});
export const messagesReceived = new Counter({
name: 'whatsapp_messages_received_total',
help: 'Total number of messages received',
labelNames: ['type'],
});
// Response time histogram
export const apiResponseTime = new Histogram({
name: 'whatsapp_api_response_time_seconds',
help: 'API response time in seconds',
labelNames: ['method', 'route', 'status'],
buckets: [0.1, 0.5, 1, 2, 5],
});
// Error counter
export const errors = new Counter({
name: 'whatsapp_errors_total',
help: 'Total number of errors',
labelNames: ['type', 'category'],
});
// Metrics endpoint
export function getMetrics() {
return register.metrics();
}

Health Check Endpoint

routes/health.ts
import { Router } from 'express';
import { whatsappClient } from '@/lib/whatsapp/client';
import { prisma } from '@/lib/db/prisma';
const router = Router();
router.get('/health', async (req, res) => {
const health = {
uptime: process.uptime(),
timestamp: Date.now(),
status: 'ok',
checks: {
database: 'unknown',
whatsapp: 'unknown',
memory: 'unknown',
},
};
// Check database
try {
await prisma.$queryRaw`SELECT 1`;
health.checks.database = 'ok';
} catch (error) {
health.checks.database = 'error';
health.status = 'degraded';
}
// Check WhatsApp API
try {
await whatsappClient.phone.get();
health.checks.whatsapp = 'ok';
} catch (error) {
health.checks.whatsapp = 'error';
health.status = 'degraded';
}
// Check memory usage
const memUsage = process.memoryUsage();
const memThreshold = 500 * 1024 * 1024; // 500MB
if (memUsage.heapUsed > memThreshold) {
health.checks.memory = 'warning';
health.status = 'degraded';
} else {
health.checks.memory = 'ok';
}
const statusCode = health.status === 'ok' ? 200 : 503;
res.status(statusCode).json(health);
});
router.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
export default router;

Scaling Considerations

Horizontal Scaling

// Use Redis for session storage
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
export class MessageQueue {
async enqueue(message: any) {
await redis.lpush('message_queue', JSON.stringify(message));
}
async dequeue() {
const message = await redis.rpop('message_queue');
return message ? JSON.parse(message) : null;
}
}

Load Balancing

kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: whatsapp-api
spec:
replicas: 3
selector:
matchLabels:
app: whatsapp-api
template:
metadata:
labels:
app: whatsapp-api
spec:
containers:
- name: whatsapp-api
image: your-registry/whatsapp-api:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: whatsapp-api-service
spec:
type: LoadBalancer
selector:
app: whatsapp-api
ports:
- port: 80
targetPort: 3000

Database Backup

backup-db.sh
#!/bin/bash
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_DIR="/backups"
DB_NAME="whatsapp"
# PostgreSQL backup
pg_dump -U postgres -d $DB_NAME | gzip > "$BACKUP_DIR/backup_$TIMESTAMP.sql.gz"
# Upload to S3
aws s3 cp "$BACKUP_DIR/backup_$TIMESTAMP.sql.gz" s3://your-bucket/backups/
# Keep only last 7 days of backups
find $BACKUP_DIR -name "backup_*.sql.gz" -mtime +7 -delete
echo "Backup completed: backup_$TIMESTAMP.sql.gz"

Security Best Practices

  1. Use HTTPS: Always use SSL/TLS for webhook endpoints
  2. Validate Webhook Signatures: Verify Meta webhook signatures
  3. Rate Limiting: Implement API rate limiting
  4. Input Validation: Validate all user inputs
  5. Secret Management: Never commit secrets to git
  6. Regular Updates: Keep dependencies updated
  7. Access Control: Implement proper authentication
  8. Audit Logging: Log all sensitive operations
  9. DDoS Protection: Use CloudFlare or AWS Shield
  10. Regular Backups: Automate database backups

Troubleshooting

High Memory Usage

// Monitor memory and restart if threshold exceeded
setInterval(() => {
const memUsage = process.memoryUsage();
const threshold = 500 * 1024 * 1024; // 500MB
if (memUsage.heapUsed > threshold) {
logger.error('Memory threshold exceeded, restarting...');
process.exit(1); // PM2/Kubernetes will restart
}
}, 60000); // Check every minute

Webhook Timeouts

// Process webhooks asynchronously
app.post('/webhook', async (req, res) => {
// Return 200 immediately
res.sendStatus(200);
// Process in background
setImmediate(async () => {
try {
await processWebhook(req.body);
} catch (error) {
logger.error('Webhook processing failed', error);
}
});
});