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
$ // 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