DeveloperBootstrap

Node.js

Node.js Production Deployment Complete Guide

Deploy Node.js applications to production with PM2, Nginx reverse proxy, SSL, structured logging, monitoring, and zero-downtime deployments.

What you'll learn

  • Install Node.js 22 LTS via nvm on Ubuntu 24.04
  • Configure PM2 for cluster mode and auto-restart
  • Set up Nginx as a reverse proxy with SSL
  • Implement zero-downtime deployments with PM2 reload
  • Configure environment variables securely
  • Monitor application health and set up log rotation
  • Automate deployments with GitHub Actions

Node.js Installation

Bash
1# Install nvm
2curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash
3source ~/.bashrc
4
5# Install Node.js 22 LTS
6nvm install 22
7nvm use 22
8nvm alias default 22
9
10# Install pnpm
11npm install -g pnpm pm2
12
13node --version   # v22.x.x
14pm2 --version

PM2 Process Manager

JavaScript
1# ecosystem.config.js
2module.exports = {
3  apps: [{
4    name: 'myapp',
5    script: 'dist/server.js',
6    instances: 'max',          // one per CPU core
7    exec_mode: 'cluster',
8    max_memory_restart: '512M',
9    env_production: {
10      NODE_ENV: 'production',
11      PORT: 3000,
12    },
13    log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
14    error_file: '/var/log/myapp/error.log',
15    out_file:   '/var/log/myapp/out.log',
16    merge_logs: true,
17  }],
18};
19
20# Start
21pm2 start ecosystem.config.js --env production
22
23# Save and enable on boot
24pm2 save
25pm2 startup   # follow the printed command
26
27# Monitor
28pm2 monit
29pm2 status
30pm2 logs myapp --lines 100

Nginx Reverse Proxy

NGINX
1# /etc/nginx/sites-available/myapp
2server {
3    listen 80;
4    server_name myapp.example.com;
5    return 301 https://$host$request_uri;
6}
7
8server {
9    listen 443 ssl;
10    http2 on;
11    server_name myapp.example.com;
12
13    ssl_certificate     /etc/letsencrypt/live/myapp.example.com/fullchain.pem;
14    ssl_certificate_key /etc/letsencrypt/live/myapp.example.com/privkey.pem;
15
16    client_max_body_size 10M;
17
18    location / {
19        proxy_pass         http://127.0.0.1:3000;
20        proxy_http_version 1.1;
21        proxy_set_header   Upgrade $http_upgrade;
22        proxy_set_header   Connection 'upgrade';
23        proxy_set_header   Host $host;
24        proxy_set_header   X-Real-IP $remote_addr;
25        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
26        proxy_set_header   X-Forwarded-Proto $scheme;
27        proxy_read_timeout 30s;
28    }
29}
30
31sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
32sudo nginx -t && sudo systemctl reload nginx

Environment Variables

Bash
1# /etc/myapp.env — owner root, mode 600
2DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
3JWT_SECRET=your-256-bit-secret
4NODE_ENV=production
5PORT=3000
6
7# Load in ecosystem.config.js
8require('dotenv').config({ path: '/etc/myapp.env' });
9
10# Or pass via PM2
11pm2 start ecosystem.config.js --env production   --node-args "--env-file=/etc/myapp.env"

Zero-downtime Deployment

Bash
1# deploy.sh — run on server after pulling new code
2#!/bin/bash
3set -e
4
5cd /opt/myapp
6
7# Pull latest code
8git pull origin main
9
10# Install dependencies
11pnpm install --frozen-lockfile
12
13# Build
14pnpm build
15
16# Run migrations
17pnpm prisma migrate deploy
18
19# Reload — PM2 replaces workers one at a time (zero downtime)
20pm2 reload ecosystem.config.js --env production
21
22echo "Deployment complete"

Monitoring & Logs

Bash
1# PM2 log rotation
2pm2 install pm2-logrotate
3pm2 set pm2-logrotate:max_size 100M
4pm2 set pm2-logrotate:retain 7
5pm2 set pm2-logrotate:compress true
6
7# Health check endpoint in your app
8app.get('/health', (req, res) => {
9  res.json({ status: 'ok', uptime: process.uptime(), timestamp: new Date().toISOString() });
10});
11
12# Monitor with Nginx
13location = /health {
14    proxy_pass http://127.0.0.1:3000/health;
15    access_log off;
16}
17
18# Sentry error tracking
19import * as Sentry from '@sentry/node';
20Sentry.init({ dsn: process.env['SENTRY_DSN'], tracesSampleRate: 0.1 });