Deployment Guide
This guide will help you deploy your CalmAPI application to production using either PM2 or Docker. Both methods are beginner-friendly and widely used in the industry.
Table of Contents#
Prerequisites#
Before deploying your application, make sure you have:
- Node.js installed (version 18 or higher recommended)
- Your application tested and working locally
- Environment variables configured properly
- A server or hosting platform (VPS, EC2, DigitalOcean, etc.)
Deploying with PM2#
PM2 is a powerful process manager for Node.js applications. It keeps your app running, restarts it automatically if it crashes, and provides useful monitoring tools.
Why Use PM2?#
- ✅ Automatic application restarts on crashes
- ✅ Built-in load balancing
- ✅ Log management
- ✅ Zero-downtime deployments
- ✅ Process monitoring
Installing PM2#
First, install PM2 globally on your server:
npm install -g pm2📚 Official PM2 Documentation: https://pm2.keymetrics.io/docs/usage/quick-start/
Method 1: Direct PM2 Start (Quick & Simple)#
This is the fastest way to get your app running with PM2:
pm2 start index.js --name "MY_AWESOME_API"Explanation:
pm2 start- Starts your application with PM2index.js- Your main entry file (replace with your actual entry point)--name "MY_AWESOME_API"- Gives your app a friendly name for easy identification
Useful PM2 Commands#
# View all running applications
pm2 list
# View logs
pm2 logs MY_AWESOME_API
# View detailed info
pm2 show MY_AWESOME_API
# Restart your app
pm2 restart MY_AWESOME_API
# Stop your app
pm2 stop MY_AWESOME_API
# Delete from PM2
pm2 delete MY_AWESOME_API
# Monitor CPU and Memory
pm2 monitStarting with Environment Variables#
pm2 start index.js --name "MY_AWESOME_API" --env productionRunning in Cluster Mode (Multiple Instances)#
PM2 can run multiple instances of your app to handle more traffic:
pm2 start index.js --name "MY_AWESOME_API" -i maxThe -i max flag starts as many instances as you have CPU cores.
Method 2: Using PM2 Ecosystem File (Recommended for Production)#
An ecosystem file gives you more control and makes deployments reproducible. It's a configuration file that defines how PM2 should run your app.
Step 1: Generate Ecosystem File#
pm2 ecosystemThis creates a file named ecosystem.config.js in your project root.
Step 2: Configure Your Ecosystem File#
Edit ecosystem.config.js:
module.exports = {
apps: [
{
name: 'calmapi-app',
script: './index.js',
instances: 'max', // or a specific number like 2, 4, etc.
exec_mode: 'cluster', // enables load balancing
env: {
NODE_ENV: 'development',
PORT: 3000,
MONGODB_URI: 'mongodb://localhost:27017/calmapi',
JWT_SECRET: 'dev-secret-key',
JWT_EXPIRES_IN: '7d',
},
env_production: {
NODE_ENV: 'production',
PORT: 3000,
MONGODB_URI: 'mongodb://admin:your-password@localhost:27017/calmapi?authSource=admin',
JWT_SECRET: 'your-super-secret-production-key',
JWT_EXPIRES_IN: '7d',
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
merge_logs: true,
autorestart: true,
max_memory_restart: '1G',
watch: false, // set to true in development if you want auto-reload
},
],
};Configuration Explained:
name- Your application namescript- Entry point fileinstances- Number of instances to run ('max' uses all CPU cores)exec_mode- 'cluster' enables load balancing across instancesenv- Environment variables for developmentenv_production- Environment variables for productionerror_file/out_file- Log file locationsautorestart- Automatically restart if app crashesmax_memory_restart- Restart if memory exceeds this limit
Step 3: Start with Ecosystem File#
# Start in development mode
pm2 start ecosystem.config.js
# Start in production mode
pm2 start ecosystem.config.js --env productionStep 4: Save PM2 Configuration#
Save the current PM2 process list so it restarts after server reboot:
pm2 saveStep 5: Setup Auto-Start on Server Reboot#
Generate and configure startup script:
pm2 startupFollow the instructions shown in the terminal (usually you need to run a command with sudo).
PM2 with TypeScript#
If you're using TypeScript, you have two options:
Option 1: Compile first, then run
npm run build
pm2 start dist/index.js --name "MY_AWESOME_API"Option 2: Use ts-node
pm2 start index.ts --interpreter ./node_modules/.bin/ts-node --name "MY_AWESOME_API"Monitoring and Logs#
PM2 provides excellent monitoring capabilities:
# Real-time monitoring
pm2 monit
# View logs (all apps)
pm2 logs
# View logs for specific app
pm2 logs MY_AWESOME_API
# Clear logs
pm2 flush
# View only errors
pm2 logs --errPM2 Plus (Optional)#
For advanced monitoring, PM2 offers a cloud-based dashboard called PM2 Plus:
📚 PM2 Plus: https://pm2.io/
pm2 plusDeploying with Docker#
Docker packages your application with all its dependencies into a container, ensuring it runs consistently across different environments.
Why Use Docker?#
- ✅ Consistent environments (dev, staging, production)
- ✅ Easy to scale
- ✅ Isolated from host system
- ✅ Simple deployment and rollback
- ✅ Works anywhere Docker is supported
Installing Docker#
📚 Official Docker Installation Guide: https://docs.docker.com/get-docker/
For Different Platforms:
- Windows: Docker Desktop for Windows
- Mac: Docker Desktop for Mac
- Linux: Docker Engine for Linux
Installing Docker Compose#
Docker Compose is included with Docker Desktop. For Linux:
sudo apt-get update
sudo apt-get install docker-compose-plugin📚 Docker Compose Documentation: https://docs.docker.com/compose/
Method 1: Basic Docker Setup#
Step 1: Create Dockerfile#
Create a file named Dockerfile in your project root:
# Use official Node.js image
FROM node:18-alpine
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy application code
COPY . .
# Expose port (CalmAPI default port)
EXPOSE 3000
# Start the application
CMD ["node", "index.js"]Dockerfile Explained:
FROM node:18-alpine- Uses lightweight Node.js 18 imageWORKDIR /app- Sets the working directory inside containerCOPY package*.json ./- Copies package files first (for better caching)RUN npm ci- Installs dependencies (production only)COPY . .- Copies your application codeEXPOSE 3000- Documents which port your app usesCMD- Command to run when container starts
Step 2: Create .dockerignore#
Create .dockerignore file to exclude unnecessary files:
node_modules
npm-debug.log
.git
.gitignore
.env
.DS_Store
dist
coverage
*.mdStep 3: Build Docker Image#
docker build -t my-awesome-api .Explanation:
docker build- Builds an image from Dockerfile-t my-awesome-api- Tags (names) your image.- Uses current directory as build context
For CalmAPI, you might name it:
docker build -t my-calmapi-app .Step 4: Run Docker Container#
docker run -d -p 8080:3000 --name my-api my-awesome-apiExplanation:
-d- Runs container in detached mode (background)-p 8080:3000- Maps port 3000 in container to port 8080 on host--name my-api- Gives container a friendly namemy-awesome-api- The image name we built
With Environment Variables (For CalmAPI)#
docker run -d -p 3000:3000 \
--name calmapi-app \
-e NODE_ENV=production \
-e MONGODB_URI=your_mongodb_uri \
-e JWT_SECRET=your_jwt_secret \
my-calmapi-appOr use an env file:
docker run -d -p 3000:3000 \
--name calmapi-app \
--env-file .env.production \
my-calmapi-appMethod 2: Docker Compose (Recommended)#
Docker Compose makes it easy to manage multi-container applications (e.g., your API + database).
Step 1: Create docker-compose.yml#
Create docker-compose.yml in your project root:
version: '3.8'
services:
# Your CalmAPI Service
api:
build: .
container_name: calmapi-app
restart: unless-stopped
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- PORT=3000
- MONGODB_URI=mongodb://mongodb:27017/calmapi
- JWT_SECRET=your-super-secret-jwt-key-change-this
- JWT_EXPIRES_IN=7d
depends_on:
- mongodb
volumes:
- ./logs:/app/logs
networks:
- calmapi-network
# MongoDB Database
mongodb:
image: mongo:7
container_name: calmapi-mongodb
restart: unless-stopped
environment:
- MONGO_INITDB_ROOT_USERNAME=admin
- MONGO_INITDB_ROOT_PASSWORD=your-secure-password
- MONGO_INITDB_DATABASE=calmapi
volumes:
- mongodb-data:/data/db
- mongodb-config:/data/configdb
networks:
- calmapi-network
ports:
- "27017:27017"
# Redis Cache (Optional - for sessions, caching, etc.)
redis:
image: redis:7-alpine
container_name: calmapi-redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis-data:/data
networks:
- calmapi-network
ports:
- "6379:6379"
volumes:
mongodb-data:
mongodb-config:
redis-data:
networks:
calmapi-network:
driver: bridgeDocker Compose Explained:
services- Defines all containers to runbuild: .- Builds image from current directory's Dockerfilerestart: unless-stopped- Automatically restart container unless you stop it manuallyports- Maps container ports to host ports (format:"host:container")environment- Sets environment variablesdepends_on- Ensures MongoDB starts before CalmAPIvolumes- Persists MongoDB data and application logsnetworks- Allows containers to communicate with each otherMONGODB_URI- Connection string pointing to the MongoDB containermongodb-data- Persists MongoDB database filesredis-data- Persists Redis data for cache and sessions
Step 2: Run with Docker Compose#
# Start all services (CalmAPI + MongoDB + Redis)
docker-compose up -d
# View logs
docker-compose logs -f
# View logs for specific service
docker-compose logs -f api
# Stop all services
docker-compose down
# Stop and remove volumes (⚠️ deletes MongoDB data)
docker-compose down -v
# Rebuild and restart
docker-compose up -d --buildProduction-Ready Dockerfile (Multi-Stage Build)#
For better optimization, use a multi-stage build for CalmAPI:
# Stage 1: Build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# If using TypeScript, uncomment:
# RUN npm run build
# Stage 2: Production
FROM node:18-alpine
WORKDIR /app
# Create non-root user for security
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
COPY package*.json ./
RUN npm ci --only=production
# Copy built files from builder stage
COPY --from=builder --chown=nodejs:nodejs /app .
# Create logs directory
RUN mkdir -p logs && chown -R nodejs:nodejs logs
USER nodejs
EXPOSE 3000
# Health check for CalmAPI
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
CMD ["node", "index.js"]Benefits:
- Smaller final image size
- Only production dependencies included
- Runs as non-root user (more secure)
Docker Compose with Environment File#
Create .env.production:
NODE_ENV=production
PORT=3000
MONGODB_URI=mongodb://admin:your-secure-password@mongodb:27017/calmapi?authSource=admin
JWT_SECRET=your-super-secret-jwt-key-change-this
JWT_EXPIRES_IN=7d
REDIS_URL=redis://redis:6379
# MongoDB credentials (used by MongoDB container)
MONGO_INITDB_ROOT_USERNAME=admin
MONGO_INITDB_ROOT_PASSWORD=your-secure-password
MONGO_INITDB_DATABASE=calmapiUpdate docker-compose.yml:
services:
api:
build: .
env_file:
- .env.production
# ... rest of configurationUseful Docker Commands#
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a
# View logs
docker logs my-api
# Follow logs in real-time
docker logs -f my-api
# Execute command in running container
docker exec -it my-api sh
# Stop container
docker stop my-api
# Start stopped container
docker start my-api
# Remove container
docker rm my-api
# Remove image
docker rmi my-awesome-api
# View resource usage
docker stats
# Clean up unused resources
docker system prune -aDocker Compose Commands#
# Start services
docker-compose up -d
# Stop services
docker-compose down
# View running services
docker-compose ps
# View logs
docker-compose logs -f
# Restart specific service
docker-compose restart api
# Rebuild specific service
docker-compose build api
# Scale service (run multiple instances)
docker-compose up -d --scale api=3
# Execute command in service
docker-compose exec api shEnvironment Variables#
Never commit sensitive information to your repository. Use environment variables for configuration.
For PM2#
Create .env file:
NODE_ENV=production
PORT=8080
DATABASE_URL=your_database_url
JWT_SECRET=your_secret_keyLoad with dotenv package:
require('dotenv').config();For Docker#
Use --env-file flag:
docker run --env-file .env.production my-awesome-apiOr in docker-compose.yml:
services:
api:
env_file:
- .env.productionBest Practices#
General#
- Use Environment Variables - Never hardcode sensitive data
- Set up Logging - Monitor your application's health
- Health Checks - Implement
/healthendpoint - Graceful Shutdown - Handle SIGTERM/SIGINT signals properly
- Use HTTPS - Always use SSL/TLS in production
- Set up Monitoring - Use tools like PM2 Plus, DataDog, or New Relic
PM2 Best Practices#
- Use Ecosystem File - Version control your PM2 configuration
- Enable Cluster Mode - Utilize all CPU cores
- Set Memory Limits - Prevent memory leaks from crashing server
- Configure Log Rotation - Prevent log files from growing too large
- Save PM2 State - Always run
pm2 saveafter changes
Docker Best Practices#
- Use Multi-Stage Builds - Keep images small
- Don't Run as Root - Create and use non-root user
- Use .dockerignore - Exclude unnecessary files
- Tag Images Properly - Use semantic versioning
- Persist Data - Use volumes for databases and important data
- Health Checks - Add HEALTHCHECK to Dockerfile
- Resource Limits - Set memory and CPU limits
Sample Dockerfile with Health Check#
FROM node:18-alpine
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
COPY package*.json ./
RUN npm ci --only=production
COPY --chown=nodejs:nodejs . .
USER nodejs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
CMD ["node", "index.js"]Sample Health Check Endpoint#
Add this to your CalmAPI application:
// In your system/configs/routing.js
// After the `app.use(sanitizerMiddleware);` add the bellow block, (also don't forget to import mongoose at the top)
// Health check endpoint
app.get('/health', async (req, res) => {
try {
// Check MongoDB connection
const dbStatus = mongoose.connection.readyState === 1 ? 'connected' : 'disconnected';
res.status(200).json({
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
database: dbStatus,
environment: process.env.NODE_ENV,
});
} catch (error) {
res.status(503).json({
status: 'error',
message: error.message,
});
}
});
Troubleshooting#
PM2 Issues#
App keeps crashing:
# Check logs for errors
pm2 logs MY_AWESOME_API --err
# Check if port is already in use
lsof -i :3000Can't connect after server reboot:
# Check if PM2 startup is configured
pm2 startup
# List saved processes
pm2 listDocker Issues#
Container exits immediately:
# Check logs
docker logs my-api
# Run in foreground to see errors
docker run --rm my-awesome-apiCan't connect to container:
# Check if container is running
docker ps
# Check port mappings
docker port my-api
# Test from inside container
docker exec -it my-api sh
wget -O- http://localhost:3000/healthBuild fails:
# Clear build cache
docker builder prune
# Build with no cache
docker build --no-cache -t my-awesome-api .Next Steps#
After deploying your application:
- Set up a reverse proxy (Nginx or Traefik)
- Configure SSL/TLS (Let's Encrypt)
- Set up CI/CD (GitHub Actions, GitLab CI)
- Monitor performance (PM2 Plus, Prometheus, Grafana)
- Set up backups (for databases and important data)
- Configure firewall (UFW, iptables)
- Set up log aggregation (ELK Stack, Loki)
Additional Resources#
PM2#
- Official Documentation: https://pm2.keymetrics.io/
- PM2 Plus: https://pm2.io/
- PM2 GitHub: https://github.com/Unitech/pm2
Docker#
- Official Documentation: https://docs.docker.com/
- Docker Hub: https://hub.docker.com/
- Docker Compose Docs: https://docs.docker.com/compose/
- Best Practices: https://docs.docker.com/develop/dev-best-practices/
General Deployment#
- Node.js Deployment Best Practices: https://nodejs.org/en/docs/guides/nodejs-docker-webapp/
- Digital Ocean Tutorials: https://www.digitalocean.com/community/tutorials
Happy Deploying! 🚀