Setup NGINX SSL certificates with PM2 clustering for Node.js applications

Intermediate 45 min Jun 02, 2026 123 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

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 -y
sudo dnf update -y

Install 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 htop
sudo dnf install -y nginx certbot python3-certbot-nginx curl htop
sudo dnf install -y epel-release

Install 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 pm2

Create 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 express

Create 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/pm2

Start 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 startup

Configure 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 nginx

Configure firewall

Open the necessary ports for HTTP and HTTPS traffic through the system firewall.

sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo firewall-cmd --permanent --zone=public --add-service=http
sudo firewall-cmd --permanent --zone=public --add-service=https
sudo firewall-cmd --reload

Obtain 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.com
Note: Replace example.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.timer

Optimize 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 0
sudo chmod +x /usr/local/bin/health-check.sh

Enable 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 nginx

Verify 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.log

Load 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 myapp

Performance 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 myapp
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',
    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 production

Add 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 nginx

Common issues

SymptomCauseFix
502 Bad GatewayPM2 processes not runningpm2 restart myapp and check logs with pm2 logs
SSL certificate errorCertbot failed or domain misconfiguredCheck DNS with nslookup example.com and retry sudo certbot --nginx
Load balancing not workingSingle PM2 process or keepalive issuesCheck pm2 list shows multiple processes and verify upstream config
High memory usagePM2 memory leaks or too many instancesReduce instances in ecosystem.config.js or lower max_memory_restart
NGINX permission deniedIncorrect file ownershipsudo chown -R www-data:www-data /var/www/myapp and chmod 755 directories
Health check failsApplication not binding to localhostEnsure 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 true

Monitoring 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 htop

Next steps

Running this in production?

Want this handled for you? Setting this up once is straightforward. Keeping it patched, monitored, backed up and performant across environments is the harder part. See how we run infrastructure like this for European SaaS and e-commerce teams.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle managed cloud infrastructure for businesses that depend on uptime. From initial setup to ongoing operations.