Tutorial 2 of 5 in series
Deployment Strategies (Heroku, Docker, VPS)
Deploy your Express app to production using Heroku, Docker, or a VPS with PM2
Abhishek Sharma 2026-06-13 Web Development / Backend / Express / Production
Deployment Options
| Platform | Best For | Effort | Scaling | Cost |
|---|---|---|---|---|
| Heroku | Quick deployment, startups | Low | Manual/Auto | Pay-per-dyno |
| Docker | Containerized apps, consistency | Medium | Orchestration | Depends on host |
| VPS (DigitalOcean, AWS EC2) | Full control, custom setup | High | Manual | Fixed monthly |
Pre-Deployment Checklist
// Before deploying, ensure:
// 1. Use environment variables for config
// 2. Set NODE_ENV=production
// 3. Use a process manager (PM2)
// 4. Enable logging (Winston)
// 5. Set up error handling
// 6. Use a reverse proxy (Nginx)
// 7. Enable compression
// 8. Set security headers (Helmet)
// 9. Add rate limiting
// 10. Configure CORS properly
Production-Ready Server Setup
const express = require('express');
const helmet = require('helmet');
const compression = require('compression');
const cors = require('cors');
const app = express();
// Production middleware
app.use(helmet());
app.use(compression());
app.use(cors({ origin: process.env.CLIENT_URL }));
app.use(express.json({ limit: '100kb' }));
// Trust proxy (important behind Nginx/Heroku)
app.set('trust proxy', 1);
// Routes
app.use('/api', require('./routes'));
// 404 & error handlers
app.use((req, res) => res.status(404).json({ error: 'Not found' }));
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Internal server error' });
});
module.exports = app;
1. Deploying to Heroku
Requirements
# Install Heroku CLI
# Create a Heroku account
# Login
heroku login
# Create app
heroku create my-express-app
Procfile
web: npm start
package.json Scripts
{
"scripts": {
"start": "node index.js",
"heroku-postbuild": "echo 'Build complete'"
},
"engines": {
"node": "18.x"
}
}
Deploy
# Set environment variables
heroku config:set NODE_ENV=production
heroku config:set JWT_SECRET=$(openssl rand -base64 32)
heroku config:set MONGO_URI=mongodb+srv://...
# Add MongoDB addon (or use external)
heroku addons:create mongolab:sandbox
# Deploy
git push heroku main
# Open app
heroku open
# View logs
heroku logs --tail
# Scale
heroku ps:scale web=1
Zero-Downtime Deployment
// Use multiple dynos for rolling updates
heroku ps:scale web=2
// Heroku will start new dynos before stopping old ones
2. Deploying with Docker
Dockerfile
FROM node:18-alpine
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci --only=production
# Copy source
COPY . .
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001 && \
chown -R nodejs:nodejs /app
USER nodejs
EXPOSE 3000
CMD ["node", "index.js"]
.dockerignore
node_modules
npm-debug.log
.env
.git
.gitignore
logs
README.md
Docker Compose (Multi-Service)
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- MONGO_URI=mongodb://mongo:27017/myapp
- REDIS_URL=redis://redis:6379
- JWT_SECRET=${JWT_SECRET}
depends_on:
- mongo
- redis
restart: always
mongo:
image: mongo:7
volumes:
- mongo_data:/data/db
restart: always
redis:
image: redis:7-alpine
restart: always
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- app
restart: always
volumes:
mongo_data:
Build & Run
# Build and start
docker-compose up -d --build
# View logs
docker-compose logs -f app
# Stop
docker-compose down
# Deploy to server
docker save myapp:latest | ssh user@server 'docker load'
Multi-Stage Build (Smaller Image)
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:18-alpine
WORKDIR /app
COPY /app/dist ./dist
COPY /app/node_modules ./node_modules
COPY /app/package.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]
3. Deploying to a VPS (DigitalOcean, AWS EC2, Linode)
Server Setup
# SSH into server
ssh root@your-server-ip
# Update system
apt update && apt upgrade -y
# Install Node.js
curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
apt install -y nodejs git nginx
# Install PM2 (process manager)
npm install -g pm2
# Clone your app
git clone https://github.com/your/repo.git /app
cd /app
npm ci --only=production
# Set environment variables
cat > /app/.env << EOF
NODE_ENV=production
PORT=3000
MONGO_URI=...
JWT_SECRET=...
EOF
# Start with PM2
pm2 start index.js --name "myapp"
# Save PM2 process list
pm2 save
pm2 startup
PM2 Configuration (ecosystem.config.js)
module.exports = {
apps: [{
name: 'myapp',
script: 'index.js',
instances: 'max', // Use all CPU cores
exec_mode: 'cluster', // Cluster mode for load balancing
env: {
NODE_ENV: 'production',
PORT: 3000
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss',
max_memory_restart: '500M',
autorestart: true,
watch: false,
max_restarts: 10,
restart_delay: 5000
}]
};
# Start with config
pm2 start ecosystem.config.js
# Monitor
pm2 monit
# List processes
pm2 list
# Graceful reload (zero downtime)
pm2 reload ecosystem.config.js
Nginx as Reverse Proxy (on VPS)
# /etc/nginx/sites-available/myapp
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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;
proxy_cache_bypass $http_upgrade;
}
# Enable gzip
gzip on;
gzip_types text/plain application/json application/javascript text/css;
}
# Enable site
ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
nginx -t
systemctl restart nginx
# Set up SSL with Certbot
apt install certbot python3-certbot-nginx
certbot --nginx -d yourdomain.com
Deployment Comparison
const deployments = {
heroku: {
setup: 'Minutes',
scaling: 'Easy (horizontal)',
cost: 'Pay per dyno',
control: 'Limited',
bestFor: 'Quick MVPs, startups'
},
docker: {
setup: 'Hours',
scaling: 'Via orchestrator',
cost: 'Server cost + orchestration',
control: 'Full',
bestFor: 'Microservices, consistent environments'
},
vps: {
setup: 'Half day',
scaling: 'Manual (vertical/horizontal)',
cost: 'Fixed monthly',
control: 'Complete',
bestFor: 'Full control, custom requirements'
}
};
Key Takeaways
- Heroku: Fastest setup,
git pushdeployment, good for MVPs - Docker: Consistent environments across dev/prod, container orchestration
- VPS: Full control, PM2 for process management, Nginx for reverse proxy
- Always use environment variables for config
- Use PM2 with cluster mode for multi-core CPU utilization
- Set up a reverse proxy (Nginx) for SSL, load balancing, static file serving
- Enable compression, Helmet, and rate limiting in production
- Set
app.set('trust proxy', 1)when behind a reverse proxy
Keep learning
Related tutorials from across the library.
Tutorial
Reverse Proxies (Nginx)
Set up Nginx as a reverse proxy for Express - SSL, load balancing, static file serving, and security
Web Development / Backend / Express / Production
ExpressNginx
Tutorial
CI/CD Pipelines for Express Apps
Set up continuous integration and deployment for Express apps using GitHub Actions, testing, and automated deployment
Web Development / Backend / Express / Production
ExpressCI/CD
Tutorial
Environment Variables & Config Management
Manage environment-specific configuration in Express using dotenv, config files, and validation
Web Development / Backend / Express / Production
ExpressEnvironment Variables