DEV Community

Alex Chen
Alex Chen

Posted on

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

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

From local development to live server. The complete deployment checklist.

Pre-Deployment Checklist

// 1. Environment variables
const requiredEnvs = ['DATABASE_URL', 'JWT_SECRET', 'PORT'];
for (const env of requiredEnvs) {
  if (!process.env[env]) {
    console.error(`Missing: ${env}`);
    process.exit(1);
  }
}

// 2. Health check endpoint
app.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    uptime: process.uptime(),
    memory: process.memoryUsage(),
    timestamp: new Date().toISOString(),
    environment: process.env.NODE_ENV,
  });
});

// 3. Graceful shutdown
process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);

let isShuttingDown = false;

function gracefulShutdown(signal) {
  if (isShuttingDown) return;
  isShuttingDown = true;

  console.log(`\n${signal} received. Shutting down gracefully...`);

  // Stop accepting new connections
  server.close(() => {
    console.log('HTTP server closed');
    process.exit(0);
  });

  // Force exit after 10 seconds
  setTimeout(() => {
    console.error('Forced shutdown');
    process.exit(1);
  }, 10000);
}
Enter fullscreen mode Exit fullscreen mode

PM2 Process Manager

# Install globally
npm install -g pm2

# Start your app
pm2 start server.js --name "my-app"

# Useful commands
pm2 list              # List all apps
pm2 logs my-app       # View logs
pm2 monit             # Monitor dashboard
pm2 restart my-app    # Restart
pm2 stop my-app       # Stop
pm2 delete my-app     # Remove

# Auto-restart on crash
pm2 start server.js --name "my-app" --max-memory-restart 500M

# Cluster mode (utilize all CPU cores)
pm2 start server.js -i max --name "my-app-cluster"

# Startup script (survives reboot)
pm2 startup          # Generates command to run as sudo
pm2 save             # Save current process list
Enter fullscreen mode Exit fullscreen mode

Nginx Reverse Proxy

# /etc/nginx/sites-available/my-app
upstream my_app {
    server 127.0.0.1:3000;
    keepalive 64;
}

server {
    listen 80;
    server_name example.com www.example.com;

    # Redirect HTTP → HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    # SSL certificates (Let's Encrypt)
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    # Proxy to Node.js app
    location / {
        proxy_pass http://my_app;
        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
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;

        # Buffer settings for large responses
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 4k;
    }

    # Static files (serve directly, don't pass to Node)
    location /static/ {
        alias /var/www/my-app/public/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml image/svg+xml;
}
Enter fullscreen mode Exit fullscreen mode

Systemd Service (Alternative to PM2)

# /etc/systemd/system/my-app.service
[Unit]
Description=My Node.js Application
After=network.target

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/my-app
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=10
Environment=NODE_ENV=production
EnvironmentFile=/var/www/my-app/.env.production

# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadOnlyPaths=/usr /bin

[Install]
WantedBy=multi-user.target
Enter fullscreen mode Exit fullscreen mode
# Enable and start
sudo systemctl daemon-reload
sudo systemctl enable my-app
sudo systemctl start my-app

# Commands
sudo systemctl status my-app
sudo systemctl restart my-app
sudo journalctl -u my-app -f   # Follow logs
Enter fullscreen mode Exit fullscreen mode

Let's Encrypt SSL

# Install certbot
sudo apt install certbot python3-certbot-nginx

# Get certificate (auto-configures Nginx!)
sudo certbot --nginx -d example.com -d www.example.com

# Auto-renewal (certbot sets this up automatically)
sudo certbot renew --dry-run    # Test renewal
sudo systemctl status certbot.timer  # Check auto-renew timer
Enter fullscreen mode Exit fullscreen mode

Monitoring & Logging

// Structured logging with pino (fast!)
import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  transport: {
    target: 'pino-pretty', // Pretty-print in dev
    options: { colorize: true }
  },
});

logger.info({ userId: 123 }, 'User logged in');
logger.error({ err: error }, 'Database query failed');

// Request logging middleware
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    logger.info(
      { method: req.method, url: req.url, statusCode: res.statusCode, duration: Date.now() - start },
      'Request completed'
    );
  });
  next();
});
Enter fullscreen mode Exit fullscreen mode

Deployment Script

#!/bin/bash
# deploy.sh — One-command deployment

set -e  # Exit on any error

echo "🚀 Starting deployment..."

APP_DIR="/var/www/my-app"
GIT_REPO="git@github.com:user/my-app.git"
BRANCH="main"

# 1. Pull latest code
cd $APP_DIR
git fetch origin
git reset --hard origin/$BRANCH

# 2. Install dependencies
npm ci --production  # Faster than npm install, uses package-lock.json

# 3. Build assets (if needed)
npm run build

# 4. Run migrations (if applicable)
npx prisma migrate deploy

# 5. Restart application
if command -v pm2 &> /dev/null; then
  pm2 restart my-app
else
  sudo systemctl restart my-app
fi

# 6. Verify health
sleep 3
if curl -sf http://localhost:3000/health > /dev/null; then
  echo "✅ Deployment successful!"
else
  echo "❌ Health check failed! Check logs."
  exit 1
fi
Enter fullscreen mode Exit fullscreen mode

Quick Reference

Task Tool Command
Process manager PM2 pm2 start server.js
Or use systemd systemctl start my-app
Reverse proxy Nginx Config in sites-available
SSL Certbot certbot --nginx
Logs PM2/journalctl pm2 logs / journalctl -u my-app
Restart PM2/systemd pm2 restart / systemctl restart
Zero-downtime PM2 reload pm2 reload my-app

What's your deployment setup? Do you use PM2, Docker, or something else?

Follow @armorbreak for more DevOps content.

Top comments (0)