DEV Community

Alex Chen
Alex Chen

Posted on

Deploying a Node.js App to Production: The Complete 2026 Guide

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.log removed or using a proper logger
  • [ ] Environment variables in .env (never committed)
  • [ ] NODE_ENV=production set
  • [ ] 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
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    },
  ],
};
Enter fullscreen mode Exit fullscreen mode
pm2 start ecosystem.config.js
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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:
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

Follow @armorbreak for more DevOps guides.

Top comments (0)