Deploy production-ready Node.js applications using PM2 process manager and Nginx reverse proxy with SSL certificates, automatic startup, and comprehensive monitoring for high-availability web hosting.
Prerequisites
- Root or sudo access
- Domain name with DNS configured
- Basic understanding of Node.js and npm
What this solves
This tutorial shows you how to deploy Node.js applications in production with proper process management, load balancing, and SSL termination. You'll use PM2 to manage Node.js processes with automatic restarts and clustering, while Nginx handles reverse proxying, SSL certificates, and static file serving for optimal performance.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you get the latest versions of all dependencies.
sudo apt update && sudo apt upgrade -y
Install Node.js and npm
Install Node.js using the NodeSource repository to get the latest LTS version with better security and performance.
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt install -y nodejs build-essential
Install PM2 globally
PM2 is a production process manager for Node.js applications that provides clustering, monitoring, and automatic restarts.
sudo npm install -g pm2
Create application user
Create a dedicated user for running Node.js applications to improve security by avoiding root execution.
sudo useradd -m -s /bin/bash nodeapp
sudo mkdir -p /var/www/nodeapp
sudo chown nodeapp:nodeapp /var/www/nodeapp
Create sample Node.js application
Switch to the nodeapp user and create a simple Express.js application for testing the deployment setup.
sudo -u nodeapp bash
cd /var/www/nodeapp
npm init -y
npm install express helmet morgan
Create application code
Create a production-ready Express application with security headers and request logging.
const express = require('express');
const helmet = require('helmet');
const morgan = require('morgan');
const app = express();
const port = process.env.PORT || 3000;
// Security middleware
app.use(helmet());
// Request logging
app.use(morgan('combined'));
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({
status: 'healthy',
timestamp: new Date().toISOString(),
process: process.pid
});
});
// Main route
app.get('/', (req, res) => {
res.json({
message: 'Node.js application running with PM2',
environment: process.env.NODE_ENV || 'development',
version: process.env.npm_package_version || '1.0.0'
});
});
// Graceful shutdown handling
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
process.exit(0);
});
app.listen(port, () => {
console.log(Server running on port ${port});
});
Configure PM2 ecosystem file
Create a PM2 configuration file to define application settings, clustering, and environment variables.
module.exports = {
apps: [{
name: 'nodeapp',
script: './app.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000
},
error_file: '/var/log/pm2/nodeapp-error.log',
out_file: '/var/log/pm2/nodeapp-out.log',
log_file: '/var/log/pm2/nodeapp.log',
time: true,
max_restarts: 10,
min_uptime: '10s',
max_memory_restart: '512M',
watch: false,
ignore_watch: ['node_modules', 'logs'],
node_args: '--max-old-space-size=512'
}]
};
Create PM2 log directory
Set up log directory with proper permissions for PM2 to write application logs.
exit
sudo mkdir -p /var/log/pm2
sudo chown nodeapp:nodeapp /var/log/pm2
sudo chmod 755 /var/log/pm2
Start application with PM2
Launch the Node.js application using PM2 with the ecosystem configuration file.
sudo -u nodeapp bash
cd /var/www/nodeapp
pm2 start ecosystem.config.js
pm2 save
exit
Configure PM2 startup script
Generate and install a systemd service to automatically start PM2 applications on server boot.
sudo -u nodeapp pm2 startup systemd -u nodeapp --hp /home/nodeapp
sudo systemctl enable pm2-nodeapp
Install and configure Nginx
Install Nginx to serve as a reverse proxy and handle SSL termination for the Node.js application.
sudo apt install -y nginx certbot python3-certbot-nginx
Configure Nginx virtual host
Create an Nginx server block to proxy requests to the PM2-managed Node.js application with security headers.
upstream nodeapp {
server 127.0.0.1:3000;
keepalive 32;
}
server {
listen 80;
server_name example.com www.example.com;
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin";
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req zone=api burst=20 nodelay;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1000;
gzip_types text/plain application/json application/javascript text/css;
# Health check endpoint
location /health {
access_log off;
proxy_pass http://nodeapp;
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;
}
# Main application
location / {
proxy_pass http://nodeapp;
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 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Static file serving (if needed)
location /static/ {
alias /var/www/nodeapp/public/;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Enable Nginx site
Enable the virtual host and configure Nginx to start on boot.
sudo ln -s /etc/nginx/sites-available/nodeapp /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl enable --now nginx
Configure SSL certificate
Use Certbot to obtain and configure SSL certificates for secure HTTPS connections.
sudo certbot --nginx -d example.com -d www.example.com --non-interactive --agree-tos --email admin@example.com
Configure firewall
Open the necessary ports for HTTP and HTTPS traffic while maintaining security.
sudo ufw allow 'Nginx Full'
sudo ufw --force enable
Configure log rotation
Set up log rotation for PM2 application logs to prevent disk space issues.
/var/log/pm2/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
copytruncate
su nodeapp nodeapp
}
Verify your setup
Test that all components are working correctly and the application is accessible.
# Check PM2 status
sudo -u nodeapp pm2 status
sudo -u nodeapp pm2 logs nodeapp --lines 10
Check Nginx status
sudo nginx -t
sudo systemctl status nginx
Test application locally
curl http://localhost:3000/health
Test through Nginx proxy
curl -H "Host: example.com" http://localhost/health
Test SSL certificate (if configured)
curl -I https://example.com/health
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| PM2 app won't start | File permissions or missing dependencies | sudo -u nodeapp pm2 logs nodeapp and check ownership with ls -la /var/www/nodeapp |
| 502 Bad Gateway | Node.js app not running or port mismatch | Verify PM2 status and check port 3000 with netstat -tlnp | grep 3000 |
| SSL certificate error | Domain not pointing to server | Check DNS with dig example.com and retry certbot after DNS propagation |
| High memory usage | Memory leaks or too many cluster instances | Adjust instances and max_memory_restart in ecosystem.config.js |
| PM2 not starting on boot | Startup script not properly configured | Re-run sudo -u nodeapp pm2 startup systemd and check systemd status |
Next steps
- Optimize Node.js application performance with PM2 clustering and memory management
- Setup nginx reverse proxy with SSL certificates and security hardening
- Configure Node.js application monitoring with PM2 and Grafana
- Implement Node.js application deployment with Git hooks and PM2
- Configure Node.js application clustering with PM2 and load balancing
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Node.js with PM2 and Nginx reverse proxy installer
# Usage: ./install.sh [domain_name] [app_port]
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Configuration
DOMAIN=${1:-"localhost"}
APP_PORT=${2:-"3000"}
APP_USER="nodeapp"
APP_DIR="/var/www/nodeapp"
NGINX_AVAILABLE="/etc/nginx/sites-available"
NGINX_ENABLED="/etc/nginx/sites-enabled"
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
cleanup() {
log_error "Installation failed. Cleaning up..."
systemctl stop nginx 2>/dev/null || true
userdel -r $APP_USER 2>/dev/null || true
rm -rf $APP_DIR 2>/dev/null || true
}
trap cleanup ERR
usage() {
echo "Usage: $0 [domain_name] [app_port]"
echo "Example: $0 example.com 3000"
exit 1
}
check_prerequisites() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root"
exit 1
fi
}
detect_distro() {
if [ ! -f /etc/os-release ]; then
log_error "Cannot detect OS distribution"
exit 1
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
UPDATE_CMD="apt update && apt upgrade -y"
NGINX_AVAILABLE="/etc/nginx/sites-available"
NGINX_ENABLED="/etc/nginx/sites-enabled"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
UPDATE_CMD="dnf update -y"
NGINX_AVAILABLE="/etc/nginx/conf.d"
NGINX_ENABLED="/etc/nginx/conf.d"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
UPDATE_CMD="yum update -y"
NGINX_AVAILABLE="/etc/nginx/conf.d"
NGINX_ENABLED="/etc/nginx/conf.d"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
log_info "Detected distribution: $ID using $PKG_MGR"
}
install_nodejs() {
log_info "[2/10] Installing Node.js LTS..."
if command -v node >/dev/null 2>&1; then
log_warn "Node.js already installed: $(node --version)"
return 0
fi
case "$ID" in
ubuntu|debian)
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
$PKG_INSTALL nodejs build-essential
;;
*)
curl -fsSL https://rpm.nodesource.com/setup_lts.x | bash -
$PKG_INSTALL nodejs gcc-c++ make
;;
esac
log_info "Node.js version: $(node --version)"
log_info "npm version: $(npm --version)"
}
install_pm2() {
log_info "[3/10] Installing PM2..."
npm install -g pm2
log_info "PM2 version: $(pm2 --version)"
}
create_app_user() {
log_info "[4/10] Creating application user..."
if id "$APP_USER" >/dev/null 2>&1; then
log_warn "User $APP_USER already exists"
else
useradd -m -s /bin/bash $APP_USER
fi
mkdir -p $APP_DIR
chown $APP_USER:$APP_USER $APP_DIR
chmod 755 $APP_DIR
}
create_app() {
log_info "[5/10] Creating Node.js application..."
sudo -u $APP_USER bash -c "cd $APP_DIR && npm init -y && npm install express helmet morgan"
cat > $APP_DIR/app.js << 'EOF'
const express = require('express');
const helmet = require('helmet');
const morgan = require('morgan');
const app = express();
const port = process.env.PORT || 3000;
app.use(helmet());
app.use(morgan('combined'));
app.get('/health', (req, res) => {
res.status(200).json({
status: 'healthy',
timestamp: new Date().toISOString(),
process: process.pid
});
});
app.get('/', (req, res) => {
res.json({
message: 'Node.js application running with PM2',
environment: process.env.NODE_ENV || 'development',
version: process.env.npm_package_version || '1.0.0'
});
});
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
process.exit(0);
});
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
EOF
chown $APP_USER:$APP_USER $APP_DIR/app.js
chmod 644 $APP_DIR/app.js
}
configure_pm2() {
log_info "[6/10] Configuring PM2..."
cat > $APP_DIR/ecosystem.config.js << EOF
module.exports = {
apps: [{
name: 'nodeapp',
script: './app.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: $APP_PORT
},
error_file: '/var/log/pm2/nodeapp-error.log',
out_file: '/var/log/pm2/nodeapp-out.log',
log_file: '/var/log/pm2/nodeapp.log',
time: true,
max_restarts: 10,
min_uptime: '10s',
max_memory_restart: '512M',
watch: false,
ignore_watch: ['node_modules', 'logs'],
node_args: '--max-old-space-size=512'
}]
};
EOF
chown $APP_USER:$APP_USER $APP_DIR/ecosystem.config.js
chmod 644 $APP_DIR/ecosystem.config.js
mkdir -p /var/log/pm2
chown $APP_USER:$APP_USER /var/log/pm2
chmod 755 /var/log/pm2
}
start_pm2() {
log_info "[7/10] Starting PM2 application..."
sudo -u $APP_USER bash -c "cd $APP_DIR && pm2 start ecosystem.config.js && pm2 save"
sudo -u $APP_USER pm2 startup systemd -u $APP_USER --hp /home/$APP_USER | grep -E '^sudo' | sh
systemctl enable pm2-$APP_USER
}
install_nginx() {
log_info "[8/10] Installing and configuring Nginx..."
$PKG_INSTALL nginx
case "$ID" in
ubuntu|debian)
CONFIG_FILE="$NGINX_AVAILABLE/$DOMAIN"
;;
*)
CONFIG_FILE="$NGINX_AVAILABLE/$DOMAIN.conf"
;;
esac
cat > $CONFIG_FILE << EOF
server {
listen 80;
server_name $DOMAIN;
location / {
proxy_pass http://127.0.0.1:$APP_PORT;
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;
}
location /health {
access_log off;
proxy_pass http://127.0.0.1:$APP_PORT/health;
}
}
EOF
chmod 644 $CONFIG_FILE
if [[ "$ID" == "ubuntu" || "$ID" == "debian" ]]; then
ln -sf $CONFIG_FILE $NGINX_ENABLED/
rm -f $NGINX_ENABLED/default
fi
}
configure_firewall() {
log_info "[9/10] Configuring firewall..."
if command -v ufw >/dev/null 2>&1; then
ufw allow 80/tcp
ufw allow 22/tcp
ufw --force enable
elif command -v firewall-cmd >/dev/null 2>&1; then
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=ssh
firewall-cmd --reload
fi
}
start_services() {
log_info "[10/10] Starting services..."
nginx -t
systemctl enable nginx
systemctl restart nginx
systemctl restart pm2-$APP_USER
}
verify_installation() {
log_info "Verifying installation..."
sleep 5
if curl -sf http://localhost/health > /dev/null; then
log_info "✓ Health check passed"
else
log_error "✗ Health check failed"
fi
if systemctl is-active --quiet nginx; then
log_info "✓ Nginx is running"
else
log_error "✗ Nginx is not running"
fi
if systemctl is-active --quiet pm2-$APP_USER; then
log_info "✓ PM2 service is running"
else
log_error "✗ PM2 service is not running"
fi
log_info "Installation completed successfully!"
log_info "Application URL: http://$DOMAIN"
log_info "Health check: http://$DOMAIN/health"
log_info "PM2 status: sudo -u $APP_USER pm2 status"
}
main() {
check_prerequisites
log_info "[1/10] Updating system packages..."
detect_distro
eval $UPDATE_CMD
install_nodejs
install_pm2
create_app_user
create_app
configure_pm2
start_pm2
install_nginx
configure_firewall
start_services
verify_installation
}
main "$@"
Review the script before running. Execute with: bash install.sh