Deploying a Node.js App to Production: The Complete 2026 Guide
From "it works on my machine" to "it works in production" — every step documented.
Before You Deploy: The Checklist
- [ ] All
console.logremoved or using a proper logger - [ ] Environment variables in
.env(never committed) - [ ]
NODE_ENV=productionset - [ ] Error handling with global catchers
- [ ] Graceful shutdown handlers
- [ ] Health check endpoint (
/health) - [ ] Rate limiting on public APIs
- [ ] Security headers configured
- [ ] Dependencies audited (
npm audit) - [ ] Git repo clean with no sensitive files
Option 1: VPS + Nginx + PM2 (My Setup, $5/month)
Server Setup
# On a fresh Ubuntu server:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs nginx
# Install PM2 globally
sudo npm install -g pm2
# Clone your app
git clone https://github.com/you/your-app.git
cd your-app
npm ci --production
Nginx Configuration
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://127.0.0.1:3000;
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;
# Timeouts for long-running requests
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
PM2 Process Manager
# Start your app
pm2 start server.js --name "myapp"
# Useful commands
pm2 list # List all processes
pm2 logs myapp # View logs
pm2 monit # Monitoring dashboard
pm2 restart myapp # Restart
pm2 stop myapp # Stop
pm2 delete myapp # Remove
# Auto-restart on boot
pm2 startup # Generates the startup command (run it!)
pm2 save # Save current process list
Ecosystem File (For Multiple Apps)
// ecosystem.config.js
module.exports = {
apps: [
{
name: 'api',
script: 'server.js',
instances: 'max', // Use all CPU cores
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000,
},
error_file: '/var/log/myapp/error.log',
out_file: '/var/log/myapp/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
max_memory_restart: '500M', // Restart if memory exceeds 500MB
max_restarts: 10, // Give up after 10 crashes in rapid succession
},
],
};
pm2 start ecosystem.config.js
Option 2: Docker Deployment
Dockerfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY . .
FROM node:22-alpine AS runner
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder --chown=appuser:appuser /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appuser /app/server.js .
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "server.js"]
docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
restart: unless-stopped
healthcheck:
test: ['CMD', 'wget', '--spider', '-q', 'http://localhost:3000/health']
interval: 30s
timeout: 3s
retries: 3
deploy:
resources:
limits:
memory: 512M
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
redis_data:
Deploy Commands
# Build and run
docker compose up -d --build
# View logs
docker compose logs -f app
# Update (zero downtime if you use a reverse proxy)
docker compose up -d --no-deps --build app
Option 3: Free/Cheap Platforms
| Platform | Free Tier | Cost After | Best For |
|---|---|---|---|
| Render | Yes (1 instance) | $7/mo | Simple Node.js apps |
| Railway | $5 credit | Pay per use | Small projects |
| Fly.io | 3 shared VMs | ~$5/mo | Docker-based |
| Hetzner | No | €3.29/mo | Full VPS control |
SSL Certificate (Let's Encrypt)
# Install Certbot
sudo apt install certbot python3-certbot-nginx
# Get certificate (auto-configures Nginx!)
sudo certbot --nginx -d your-domain.com -d www.your-domain.com
# Auto-renewal is set up automatically
sudo certbot renew --dry-run # Test renewal
Monitoring Basics
# PM2 monitoring
pm2 monit # Interactive dashboard
pm2 plus # Web dashboard (free tier)
# Basic health check script
#!/bin/bash
# health-check.sh — run via cron every 5 minutes
if ! curl -sf http://localhost:3000/health > /dev/null; then
echo "$(date): App DOWN! Restarting..." >> /var/log/app-health.log
pm2 restart myapp
fi
Post-Deploy Verification
# Your deployment checklist:
curl -I https://your-domain.com # HTTPS works?
curl https://your-domain.com/health # Health endpoint?
curl -I https://www.your-domain.com # www redirect?
npm audit # No known vulnerabilities?
pm2 list # Process running?
pm2 logs myapp --lines 20 # Any errors on startup?
Follow @armorbreak for more DevOps guides.
Top comments (0)