Configure NGINX as a reverse proxy with SSL certificates and load balancing for PM2 clustered Node.js applications. Set up automatic SSL certificate management with Let's Encrypt, implement health checks, and optimize performance for production environments.
Prerequisites
- Root or sudo access
- Domain name pointing to server
- Node.js application to deploy
- Basic Linux command line knowledge
What this solves
This tutorial shows you how to set up NGINX as a reverse proxy with SSL certificates for Node.js applications running in PM2 clusters. You'll configure automatic SSL certificate management with Let's Encrypt, implement load balancing across multiple Node.js processes, and set up health checks for production reliability.
This setup is essential when you need to scale Node.js applications beyond a single process, secure them with HTTPS, and handle high traffic loads with proper load balancing and failover capabilities.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you get the latest versions of all packages.
sudo apt update && sudo apt upgrade -yInstall NGINX and required packages
Install NGINX web server, Certbot for SSL certificate management, and essential tools for the setup.
sudo apt install -y nginx certbot python3-certbot-nginx curl htopInstall Node.js and PM2
Install Node.js LTS version and PM2 process manager for clustering Node.js applications.
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt install -y nodejs
sudo npm install -g pm2Create a sample Node.js application
Create a basic Express.js application to demonstrate the setup with clustering capabilities.
sudo mkdir -p /var/www/myapp
sudo chown $USER:$USER /var/www/myapp
cd /var/www/myapp
npm init -y
npm install expressCreate the application file
Create a simple Express.js server that shows the process ID to demonstrate load balancing across multiple processes.
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.json({
message: 'Hello from Node.js with PM2!',
process: process.pid,
uptime: process.uptime()
});
});
app.get('/health', (req, res) => {
res.json({ status: 'healthy', process: process.pid });
});
app.listen(port, '127.0.0.1', () => {
console.log(Server running on port ${port}, process ${process.pid});
});Create PM2 ecosystem configuration
Configure PM2 with an ecosystem file to manage clustering, environment variables, and application settings.
module.exports = {
apps: [{
name: 'myapp',
script: 'app.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'development',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 3000
},
max_memory_restart: '1G',
error_file: '/var/log/pm2/myapp-error.log',
out_file: '/var/log/pm2/myapp-out.log',
log_file: '/var/log/pm2/myapp-combined.log',
time: true
}]
};Create PM2 log directory
Create the log directory for PM2 and set appropriate permissions for the application user.
sudo mkdir -p /var/log/pm2
sudo chown $USER:$USER /var/log/pm2Start the application with PM2
Start the Node.js application in cluster mode using PM2, which will spawn multiple processes based on CPU cores.
cd /var/www/myapp
pm2 start ecosystem.config.js --env production
pm2 save
pm2 startupConfigure NGINX reverse proxy
Create an NGINX configuration that acts as a reverse proxy and load balancer for the PM2 clustered Node.js application.
upstream nodejs_backend {
least_conn;
server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
keepalive 32;
}
server {
listen 80;
server_name example.com www.example.com;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
location / {
proxy_pass http://nodejs_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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;
proxy_cache_bypass $http_upgrade;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffer settings
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
location /health {
proxy_pass http://nodejs_backend/health;
access_log off;
}
# Static files (if any)
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri @proxy;
}
location @proxy {
proxy_pass http://nodejs_backend;
}
}Enable the NGINX site
Enable the new site configuration and test the NGINX configuration for syntax errors.
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginxConfigure firewall
Open the necessary ports for HTTP and HTTPS traffic through the system firewall.
sudo ufw allow 'Nginx Full'
sudo ufw enableObtain SSL certificate with Let's Encrypt
Use Certbot to automatically obtain and configure SSL certificates for your domain with automatic NGINX configuration.
sudo certbot --nginx -d example.com -d www.example.comexample.com with your actual domain name. Make sure your domain's DNS records point to your server's IP address before running this command.Configure SSL renewal
Set up automatic SSL certificate renewal to ensure certificates don't expire.
sudo certbot renew --dry-run
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timerOptimize NGINX for production
Update the main NGINX configuration for better performance with the Node.js application.
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_requests 100;
types_hash_max_size 2048;
server_tokens off;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
# Buffer sizes
client_body_buffer_size 128k;
client_max_body_size 10m;
client_header_buffer_size 1k;
large_client_header_buffers 4 4k;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}Add health check monitoring
Create a simple health check script to monitor the application and PM2 processes.
#!/bin/bash
Check if PM2 processes are running
PM2_STATUS=$(pm2 list | grep -c "online")
if [ $PM2_STATUS -eq 0 ]; then
echo "ERROR: No PM2 processes running"
exit 1
fi
Check if application responds to health endpoint
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/health)
if [ "$HTTP_STATUS" != "200" ]; then
echo "ERROR: Application health check failed (HTTP $HTTP_STATUS)"
exit 1
fi
Check NGINX status
if ! sudo systemctl is-active --quiet nginx; then
echo "ERROR: NGINX is not running"
exit 1
fi
echo "OK: All services healthy"
exit 0sudo chmod +x /usr/local/bin/health-check.shEnable and start services
Ensure all services are enabled to start on boot and are currently running.
sudo systemctl enable nginx
sudo systemctl restart nginx
sudo systemctl status nginxVerify your setup
Test that your NGINX reverse proxy, SSL certificates, and PM2 clustering are working correctly.
# Check PM2 processes
pm2 list
pm2 monit
Test application endpoint
curl -k https://example.com/
curl -k https://example.com/health
Check SSL certificate
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | openssl x509 -noout -dates
Run health check
/usr/local/bin/health-check.sh
Check NGINX status
sudo systemctl status nginx
sudo nginx -t
View application logs
pm2 logs myapp
tail -f /var/log/nginx/access.logLoad balancing verification
Test that load balancing is working correctly across multiple PM2 processes by making several requests and observing different process IDs.
# Make multiple requests to see different process IDs
for i in {1..10}; do
curl -s https://example.com/ | grep -o '"process":[0-9]*'
sleep 1
done
Check PM2 process distribution
pm2 list
pm2 show myappPerformance optimization
Configure PM2 monitoring
Set up PM2 monitoring to track application performance and automatically restart failed processes.
# Update PM2 ecosystem for production optimization
pm2 stop myappmodule.exports = {
apps: [{
name: 'myapp',
script: 'app.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'development',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 3000
},
max_memory_restart: '1G',
min_uptime: '10s',
max_restarts: 10,
restart_delay: 4000,
error_file: '/var/log/pm2/myapp-error.log',
out_file: '/var/log/pm2/myapp-out.log',
log_file: '/var/log/pm2/myapp-combined.log',
time: true,
autorestart: true,
watch: false,
ignore_watch: ['node_modules', 'logs']
}]
};pm2 reload ecosystem.config.js --env productionAdd rate limiting to NGINX
Configure rate limiting to protect your application from abuse and ensure fair usage.
# Add this inside the server block, before existing location blocks
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://nodejs_backend;
# ... other proxy settings
}
location /login {
limit_req zone=login burst=5 nodelay;
proxy_pass http://nodejs_backend;
# ... other proxy settings
}sudo nginx -t
sudo systemctl reload nginxCommon issues
| Symptom | Cause | Fix |
|---|---|---|
| 502 Bad Gateway | PM2 processes not running | pm2 restart myapp and check logs with pm2 logs |
| SSL certificate error | Certbot failed or domain misconfigured | Check DNS with nslookup example.com and retry sudo certbot --nginx |
| Load balancing not working | Single PM2 process or keepalive issues | Check pm2 list shows multiple processes and verify upstream config |
| High memory usage | PM2 memory leaks or too many instances | Reduce instances in ecosystem.config.js or lower max_memory_restart |
| NGINX permission denied | Incorrect file ownership | sudo chown -R www-data:www-data /var/www/myapp and chmod 755 directories |
| Health check fails | Application not binding to localhost | Ensure app.js binds to 127.0.0.1 and port 3000 is not blocked |
Security hardening
Configure additional security headers
Add enhanced security headers to protect against common web vulnerabilities. For more comprehensive security, see our NGINX security headers tutorial.
# Add these headers in the server block
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;Configure PM2 log rotation
Set up log rotation to prevent log files from consuming too much disk space.
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 30
pm2 set pm2-logrotate:compress trueMonitoring and maintenance
For comprehensive monitoring of your Node.js application with PM2, consider implementing Node.js monitoring with Prometheus for production environments.
# Monitor PM2 processes
pm2 monit
Check application performance
pm2 show myapp
View real-time logs
pm2 logs myapp --lines 100
Check SSL certificate expiry
sudo certbot certificates
Monitor NGINX access patterns
sudo tail -f /var/log/nginx/access.log | grep -E '(POST|PUT|DELETE)'
Check system resource usage
sudo htopNext steps
- Monitor Node.js applications with Prometheus and Grafana for comprehensive observability
- Configure NGINX SSL termination with Redis session storage for session management
- Set up Node.js application security with Helmet and rate limiting for enhanced protection
- Configure NGINX Redis cluster caching for improved performance
- Set up centralized logging with Winston and Elasticsearch for better log management
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Configuration
DOMAIN="${1:-}"
APP_USER="${2:-appuser}"
APP_DIR="/var/www/myapp"
LOG_DIR="/var/log/pm2"
usage() {
echo "Usage: $0 <domain> [app_user]"
echo "Example: $0 example.com myappuser"
exit 1
}
log() {
echo -e "${GREEN}[INFO]${NC} $1"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1"
exit 1
}
cleanup() {
warn "Installation failed. Cleaning up..."
systemctl stop nginx 2>/dev/null || true
pm2 kill 2>/dev/null || true
userdel -r "$APP_USER" 2>/dev/null || true
}
trap cleanup ERR
# Check prerequisites
[[ $EUID -eq 0 ]] || error "This script must be run as root"
[[ -n "$DOMAIN" ]] || usage
# Detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update && apt upgrade -y"
PKG_INSTALL="apt install -y"
NGINX_CONF_DIR="/etc/nginx/sites-available"
NGINX_ENABLED_DIR="/etc/nginx/sites-enabled"
FIREWALL_CMD="ufw"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
NGINX_CONF_DIR="/etc/nginx/conf.d"
NGINX_ENABLED_DIR="/etc/nginx/conf.d"
FIREWALL_CMD="firewall-cmd"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
NGINX_CONF_DIR="/etc/nginx/conf.d"
NGINX_ENABLED_DIR="/etc/nginx/conf.d"
FIREWALL_CMD="firewall-cmd"
;;
*)
error "Unsupported distribution: $ID"
;;
esac
else
error "Cannot detect distribution"
fi
echo "[1/12] Updating system packages..."
$PKG_UPDATE
echo "[2/12] Installing EPEL repository (RHEL-based)..."
if [[ "$PKG_MGR" == "dnf" || "$PKG_MGR" == "yum" ]]; then
$PKG_INSTALL epel-release
fi
echo "[3/12] Installing NGINX and SSL tools..."
$PKG_INSTALL nginx certbot python3-certbot-nginx curl htop
echo "[4/12] Installing Node.js and PM2..."
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - 2>/dev/null || {
if [[ "$PKG_MGR" == "dnf" || "$PKG_MGR" == "yum" ]]; then
curl -fsSL https://rpm.nodesource.com/setup_lts.x | bash -
fi
}
$PKG_INSTALL nodejs
npm install -g pm2
echo "[5/12] Creating application user..."
useradd -r -s /bin/bash -d "$APP_DIR" "$APP_USER" || true
mkdir -p "$APP_DIR" "$LOG_DIR"
chown "$APP_USER:$APP_USER" "$APP_DIR" "$LOG_DIR"
chmod 755 "$APP_DIR" "$LOG_DIR"
echo "[6/12] Creating Node.js application..."
sudo -u "$APP_USER" bash << EOF
cd "$APP_DIR"
npm init -y
npm install express
EOF
cat > "$APP_DIR/app.js" << 'EOF'
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.json({
message: 'Hello from Node.js with PM2!',
process: process.pid,
uptime: process.uptime()
});
});
app.get('/health', (req, res) => {
res.json({ status: 'healthy', process: process.pid });
});
app.listen(port, '127.0.0.1', () => {
console.log(`Server running on port ${port}, process ${process.pid}`);
});
EOF
cat > "$APP_DIR/ecosystem.config.js" << EOF
module.exports = {
apps: [{
name: 'myapp',
script: 'app.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'development',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 3000
},
max_memory_restart: '1G',
error_file: '${LOG_DIR}/myapp-error.log',
out_file: '${LOG_DIR}/myapp-out.log',
log_file: '${LOG_DIR}/myapp-combined.log',
time: true
}]
};
EOF
chown -R "$APP_USER:$APP_USER" "$APP_DIR"
chmod 644 "$APP_DIR"/{app.js,ecosystem.config.js,package.json}
echo "[7/12] Starting PM2 application..."
sudo -u "$APP_USER" bash << EOF
cd "$APP_DIR"
pm2 start ecosystem.config.js --env production
pm2 save
EOF
echo "[8/12] Configuring PM2 startup..."
PM2_STARTUP=$(sudo -u "$APP_USER" pm2 startup | tail -1)
eval "$PM2_STARTUP"
echo "[9/12] Configuring NGINX..."
NGINX_CONF_FILE="$NGINX_CONF_DIR/${DOMAIN}.conf"
if [[ "$NGINX_CONF_DIR" == "/etc/nginx/sites-available" ]]; then
NGINX_CONF_FILE="$NGINX_CONF_DIR/$DOMAIN"
fi
cat > "$NGINX_CONF_FILE" << EOF
upstream nodejs_backend {
least_conn;
server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
keepalive 32;
}
server {
listen 80;
server_name $DOMAIN www.$DOMAIN;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
location / {
proxy_pass http://nodejs_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection 'upgrade';
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;
proxy_cache_bypass \$http_upgrade;
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
location /health {
proxy_pass http://nodejs_backend/health;
access_log off;
}
}
EOF
chmod 644 "$NGINX_CONF_FILE"
if [[ "$NGINX_CONF_DIR" == "/etc/nginx/sites-available" ]]; then
ln -sf "$NGINX_CONF_FILE" "$NGINX_ENABLED_DIR/"
rm -f /etc/nginx/sites-enabled/default
fi
echo "[10/12] Starting services..."
systemctl enable nginx
systemctl restart nginx
echo "[11/12] Configuring firewall..."
if command -v ufw &> /dev/null; then
ufw allow 'Nginx Full'
ufw --force enable
elif command -v firewall-cmd &> /dev/null; then
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
systemctl enable firewalld
fi
echo "[12/12] Setting up SSL certificates..."
certbot --nginx -d "$DOMAIN" -d "www.$DOMAIN" --non-interactive --agree-tos --email "admin@$DOMAIN" || warn "SSL setup failed - run manually: certbot --nginx -d $DOMAIN"
echo -e "${GREEN}Installation completed successfully!${NC}"
echo "Application user: $APP_USER"
echo "Application directory: $APP_DIR"
echo "Domain: https://$DOMAIN"
echo "Health check: https://$DOMAIN/health"
echo ""
echo "Verification:"
curl -f "http://localhost:3000/health" && log "Node.js app is running"
systemctl is-active nginx && log "NGINX is running"
sudo -u "$APP_USER" pm2 status && log "PM2 is managing processes"
Review the script before running. Execute with: bash install.sh