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

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

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

Pre-Deployment Checklist

PM2 Process Manager

Nginx Reverse Proxy

Systemd Service (Alternative to PM2)

Let's Encrypt SSL

Monitoring & Logging

Deployment Script

Quick Reference From local development to live server. The complete deployment checklist. What's your deployment setup? Do you use PM2, Docker, or something else? Follow @armorbreak for more DevOps content. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

$ // 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({ -weight: 500;">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); } // 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({ -weight: 500;">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); } // 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({ -weight: 500;">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); } # Install globally -weight: 500;">npm -weight: 500;">install -g pm2 # Start your app pm2 -weight: 500;">start server.js --name "my-app" # Useful commands pm2 list # List all apps pm2 logs my-app # View logs pm2 monit # Monitor dashboard pm2 -weight: 500;">restart my-app # Restart pm2 -weight: 500;">stop my-app # Stop pm2 delete my-app # Remove # Auto--weight: 500;">restart on crash pm2 -weight: 500;">start server.js --name "my-app" --max-memory--weight: 500;">restart 500M # Cluster mode (utilize all CPU cores) pm2 -weight: 500;">start server.js -i max --name "my-app-cluster" # Startup script (survives reboot) pm2 startup # Generates command to run as -weight: 600;">sudo pm2 save # Save current process list # Install globally -weight: 500;">npm -weight: 500;">install -g pm2 # Start your app pm2 -weight: 500;">start server.js --name "my-app" # Useful commands pm2 list # List all apps pm2 logs my-app # View logs pm2 monit # Monitor dashboard pm2 -weight: 500;">restart my-app # Restart pm2 -weight: 500;">stop my-app # Stop pm2 delete my-app # Remove # Auto--weight: 500;">restart on crash pm2 -weight: 500;">start server.js --name "my-app" --max-memory--weight: 500;">restart 500M # Cluster mode (utilize all CPU cores) pm2 -weight: 500;">start server.js -i max --name "my-app-cluster" # Startup script (survives reboot) pm2 startup # Generates command to run as -weight: 600;">sudo pm2 save # Save current process list # Install globally -weight: 500;">npm -weight: 500;">install -g pm2 # Start your app pm2 -weight: 500;">start server.js --name "my-app" # Useful commands pm2 list # List all apps pm2 logs my-app # View logs pm2 monit # Monitor dashboard pm2 -weight: 500;">restart my-app # Restart pm2 -weight: 500;">stop my-app # Stop pm2 delete my-app # Remove # Auto--weight: 500;">restart on crash pm2 -weight: 500;">start server.js --name "my-app" --max-memory--weight: 500;">restart 500M # Cluster mode (utilize all CPU cores) pm2 -weight: 500;">start server.js -i max --name "my-app-cluster" # Startup script (survives reboot) pm2 startup # Generates command to run as -weight: 600;">sudo pm2 save # Save current process list # /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; } # /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; } # /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; } # /etc/systemd/system/my-app.-weight: 500;">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 # /etc/systemd/system/my-app.-weight: 500;">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 # /etc/systemd/system/my-app.-weight: 500;">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 # Enable and -weight: 500;">start -weight: 600;">sudo -weight: 500;">systemctl daemon-reload -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">enable my-app -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">start my-app # Commands -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">status my-app -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart my-app -weight: 600;">sudo journalctl -u my-app -f # Follow logs # Enable and -weight: 500;">start -weight: 600;">sudo -weight: 500;">systemctl daemon-reload -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">enable my-app -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">start my-app # Commands -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">status my-app -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart my-app -weight: 600;">sudo journalctl -u my-app -f # Follow logs # Enable and -weight: 500;">start -weight: 600;">sudo -weight: 500;">systemctl daemon-reload -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">enable my-app -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">start my-app # Commands -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">status my-app -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart my-app -weight: 600;">sudo journalctl -u my-app -f # Follow logs # Install certbot -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install certbot python3-certbot-nginx # Get certificate (auto-configures Nginx!) -weight: 600;">sudo certbot --nginx -d example.com -d www.example.com # Auto-renewal (certbot sets this up automatically) -weight: 600;">sudo certbot renew --dry-run # Test renewal -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">status certbot.timer # Check auto-renew timer # Install certbot -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install certbot python3-certbot-nginx # Get certificate (auto-configures Nginx!) -weight: 600;">sudo certbot --nginx -d example.com -d www.example.com # Auto-renewal (certbot sets this up automatically) -weight: 600;">sudo certbot renew --dry-run # Test renewal -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">status certbot.timer # Check auto-renew timer # Install certbot -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install certbot python3-certbot-nginx # Get certificate (auto-configures Nginx!) -weight: 600;">sudo certbot --nginx -d example.com -d www.example.com # Auto-renewal (certbot sets this up automatically) -weight: 600;">sudo certbot renew --dry-run # Test renewal -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">status certbot.timer # Check auto-renew timer // 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 -weight: 500;">start = Date.now(); res.on('finish', () => { logger.info( { method: req.method, url: req.url, statusCode: res.statusCode, duration: Date.now() - -weight: 500;">start }, 'Request completed' ); }); next(); }); // 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 -weight: 500;">start = Date.now(); res.on('finish', () => { logger.info( { method: req.method, url: req.url, statusCode: res.statusCode, duration: Date.now() - -weight: 500;">start }, 'Request completed' ); }); next(); }); // 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 -weight: 500;">start = Date.now(); res.on('finish', () => { logger.info( { method: req.method, url: req.url, statusCode: res.statusCode, duration: Date.now() - -weight: 500;">start }, 'Request completed' ); }); next(); }); #!/bin/bash # deploy.sh — One-command deployment set -e # Exit on any error echo "🚀 Starting deployment..." APP_DIR="/var/www/my-app" GIT_REPO="-weight: 500;">git@github.com:user/my-app.-weight: 500;">git" BRANCH="main" # 1. Pull latest code cd $APP_DIR -weight: 500;">git fetch origin -weight: 500;">git reset --hard origin/$BRANCH # 2. Install dependencies -weight: 500;">npm ci --production # Faster than -weight: 500;">npm -weight: 500;">install, uses package-lock.json # 3. Build assets (if needed) -weight: 500;">npm run build # 4. Run migrations (if applicable) npx prisma migrate deploy # 5. Restart application if command -v pm2 &> /dev/null; then pm2 -weight: 500;">restart my-app else -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart my-app fi # 6. Verify health sleep 3 if -weight: 500;">curl -sf http://localhost:3000/health > /dev/null; then echo "✅ Deployment successful!" else echo "❌ Health check failed! Check logs." exit 1 fi #!/bin/bash # deploy.sh — One-command deployment set -e # Exit on any error echo "🚀 Starting deployment..." APP_DIR="/var/www/my-app" GIT_REPO="-weight: 500;">git@github.com:user/my-app.-weight: 500;">git" BRANCH="main" # 1. Pull latest code cd $APP_DIR -weight: 500;">git fetch origin -weight: 500;">git reset --hard origin/$BRANCH # 2. Install dependencies -weight: 500;">npm ci --production # Faster than -weight: 500;">npm -weight: 500;">install, uses package-lock.json # 3. Build assets (if needed) -weight: 500;">npm run build # 4. Run migrations (if applicable) npx prisma migrate deploy # 5. Restart application if command -v pm2 &> /dev/null; then pm2 -weight: 500;">restart my-app else -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart my-app fi # 6. Verify health sleep 3 if -weight: 500;">curl -sf http://localhost:3000/health > /dev/null; then echo "✅ Deployment successful!" else echo "❌ Health check failed! Check logs." exit 1 fi #!/bin/bash # deploy.sh — One-command deployment set -e # Exit on any error echo "🚀 Starting deployment..." APP_DIR="/var/www/my-app" GIT_REPO="-weight: 500;">git@github.com:user/my-app.-weight: 500;">git" BRANCH="main" # 1. Pull latest code cd $APP_DIR -weight: 500;">git fetch origin -weight: 500;">git reset --hard origin/$BRANCH # 2. Install dependencies -weight: 500;">npm ci --production # Faster than -weight: 500;">npm -weight: 500;">install, uses package-lock.json # 3. Build assets (if needed) -weight: 500;">npm run build # 4. Run migrations (if applicable) npx prisma migrate deploy # 5. Restart application if command -v pm2 &> /dev/null; then pm2 -weight: 500;">restart my-app else -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart my-app fi # 6. Verify health sleep 3 if -weight: 500;">curl -sf http://localhost:3000/health > /dev/null; then echo "✅ Deployment successful!" else echo "❌ Health check failed! Check logs." exit 1 fi