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:
NODE_ENV=production
# WhatsApp APICLOUD_API_ACCESS_TOKEN=your_production_tokenWA_PHONE_NUMBER_ID=your_production_phone_idWA_BUSINESS_ACCOUNT_ID=your_production_business_idWHATSAPP_WEBHOOK_VERIFICATION_TOKEN=your_webhook_token
# DatabaseDATABASE_URL=postgresql://user:password@host:5432/prod_db
# SecurityAPI_SECRET=your_strong_api_secretSESSION_SECRET=your_session_secret
# MonitoringSENTRY_DSN=your_sentry_dsnLOG_LEVEL=info
# PerformanceMAX_RETRY_ATTEMPTS=3REQUEST_TIMEOUT=30000RATE_LIMIT_WINDOW=60000RATE_LIMIT_MAX=100Secret Management
Using Environment Variables (Vercel, Railway)
# Set via CLIvercel env add CLOUD_API_ACCESS_TOKEN
# Or via dashboard# Navigate to Project Settings → Environment VariablesUsing AWS Secrets Manager
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; }}
// Usageexport 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
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 sizeFROM node:20-alpine AS builder
# Install pnpmRUN npm install -g pnpm
# Set working directoryWORKDIR /app
# Copy package filesCOPY package.json pnpm-lock.yaml ./
# Install dependenciesRUN pnpm install --frozen-lockfile
# Copy source codeCOPY . .
# Build applicationRUN pnpm build
# Prune dev dependenciesRUN pnpm prune --prod
# Production stageFROM node:20-alpine AS runner
# Install pnpmRUN npm install -g pnpm
# Create app userRUN addgroup --system --gid 1001 nodejsRUN adduser --system --uid 1001 whatsapp
WORKDIR /app
# Copy built application from builderCOPY --from=builder --chown=whatsapp:nodejs /app/dist ./distCOPY --from=builder --chown=whatsapp:nodejs /app/node_modules ./node_modulesCOPY --from=builder --chown=whatsapp:nodejs /app/package.json ./
# Switch to non-root userUSER whatsapp
# Expose portEXPOSE 3000
# Health checkHEALTHCHECK --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 applicationCMD ["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: bridgeNginx Configuration
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
pnpm add -g vercel2. 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
# Deploy to productionvercel --prod
# Set environment variablesvercel env add CLOUD_API_ACCESS_TOKEN productionvercel env add WA_PHONE_NUMBER_ID productionvercel env add DATABASE_URL production4. Custom Domain
# Add custom domainvercel domains add api.yourdomain.com
# Verify DNSvercel domains verify api.yourdomain.comRailway
1. Install Railway CLI
npm i -g @railway/clirailway login2. 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
# Initialize projectrailway init
# Add environment variablesrailway variables set CLOUD_API_ACCESS_TOKEN=your_tokenrailway variables set WA_PHONE_NUMBER_ID=your_id
# Deployrailway up4. Add PostgreSQL
railway add postgresqlRender
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: whatsapp2. Deploy
# Connect GitHub repo and deploy automatically# Or use Render CLIrender upAWS (EC2 with Docker)
1. Launch EC2 Instance
# Create security groupaws ec2 create-security-group \ --group-name whatsapp-api-sg \ --description "WhatsApp API security group"
# Allow SSH, HTTP, HTTPSaws 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/02. Deploy with Docker
# SSH into instancessh -i your-key.pem ec2-user@your-instance-ip
# Install Dockersudo yum update -ysudo yum install docker -ysudo service docker startsudo usermod -a -G docker ec2-user
# Install Docker Composesudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-composesudo chmod +x /usr/local/bin/docker-compose
# Clone your repositorygit clone your-repo-urlcd your-repo
# Copy environment filecp .env.production .env
# Deploydocker-compose up -d3. Setup Automatic Deployment
# Create deployment scriptcat > deploy.sh << 'EOF'#!/bin/bashcd /home/ec2-user/whatsapp-apigit pull origin maindocker-compose downdocker-compose up -d --builddocker system prune -fEOF
chmod +x deploy.sh
# Setup cron for auto-updates (optional)crontab -e# Add: 0 2 * * * /home/ec2-user/whatsapp-api/deploy.shMonitoring and Logging
Application Logging
import winston from 'winston';import { format } from 'winston';
const { combine, timestamp, json, errors, printf } = format;
// Custom format for developmentconst devFormat = printf(({ level, message, timestamp, ...metadata }) => { let msg = `${timestamp} [${level}] ${message}`; if (Object.keys(metadata).length > 0) { msg += ` ${JSON.stringify(metadata)}`; } return msg;});
// Create loggerexport 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 AWSif (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
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
import { Counter, Histogram, register } from 'prom-client';
// Message countersexport 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 histogramexport 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 counterexport const errors = new Counter({ name: 'whatsapp_errors_total', help: 'Total number of errors', labelNames: ['type', 'category'],});
// Metrics endpointexport function getMetrics() { return register.metrics();}Health Check Endpoint
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 storageimport 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
apiVersion: apps/v1kind: Deploymentmetadata: name: whatsapp-apispec: 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: v1kind: Servicemetadata: name: whatsapp-api-servicespec: type: LoadBalancer selector: app: whatsapp-api ports: - port: 80 targetPort: 3000Database Backup
#!/bin/bash
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")BACKUP_DIR="/backups"DB_NAME="whatsapp"
# PostgreSQL backuppg_dump -U postgres -d $DB_NAME | gzip > "$BACKUP_DIR/backup_$TIMESTAMP.sql.gz"
# Upload to S3aws s3 cp "$BACKUP_DIR/backup_$TIMESTAMP.sql.gz" s3://your-bucket/backups/
# Keep only last 7 days of backupsfind $BACKUP_DIR -name "backup_*.sql.gz" -mtime +7 -delete
echo "Backup completed: backup_$TIMESTAMP.sql.gz"Security Best Practices
- Use HTTPS: Always use SSL/TLS for webhook endpoints
- Validate Webhook Signatures: Verify Meta webhook signatures
- Rate Limiting: Implement API rate limiting
- Input Validation: Validate all user inputs
- Secret Management: Never commit secrets to git
- Regular Updates: Keep dependencies updated
- Access Control: Implement proper authentication
- Audit Logging: Log all sensitive operations
- DDoS Protection: Use CloudFlare or AWS Shield
- Regular Backups: Automate database backups
Troubleshooting
High Memory Usage
// Monitor memory and restart if threshold exceededsetInterval(() => { 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 minuteWebhook Timeouts
// Process webhooks asynchronouslyapp.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); } });});