
Table of Contents
Reverse Proxy with Nginx and Apache2 for Node.js
SSL, WebSocket support, security headers, and a side-by-side config comparison — Part 3 of 3
In Part 1 we set up NVM and PM2. In Part 2 we started the app. Now let's expose it to the internet with a proper reverse proxy.
Architecture
Internet → [Port 80/443] → Nginx or Apache2 → [localhost:3000] → PM2 → Node.js App The reverse proxy handles: SSL termination, security headers, static file serving, WebSocket upgrades.
Prerequisites
- Node.js app running on
localhost:3000via PM2 (Part 2) - A domain name pointing to your server
- Nginx or Apache2 installed
Option A: Nginx Configuration
Install Nginx
sudo apt update
sudo apt install nginx -y
sudo systemctl enable nginx
sudo systemctl start nginx Create a Server Block
# /etc/nginx/sites-available/myapp
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
# Redirect HTTP to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# Security headers
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 Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Static files (optional — serve directly from Nginx)
location /public {
alias /opt/myapp/public;
expires 30d;
add_header Cache-Control "public, immutable";
}
# Proxy to Node.js
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
# WebSocket support
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_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering off;
}
access_log /var/log/nginx/myapp.access.log;
error_log /var/log/nginx/myapp.error.log;
} Enable and Test
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx Option B: Apache2 Configuration
Install Apache2 and Enable Modules
sudo apt update
sudo apt install apache2 -y
sudo a2enmod proxy proxy_http proxy_wstunnel ssl headers rewrite
sudo systemctl restart apache2 Create a Virtual Host
# /etc/apache2/sites-available/myapp.conf
ServerName yourdomain.com
ServerAlias www.yourdomain.com
Redirect permanent / https://yourdomain.com/
ServerName yourdomain.com
ServerAlias www.yourdomain.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/yourdomain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/yourdomain.com/privkey.pem
# Security headers
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
# Static files
Alias /public /opt/myapp/public
Options -Indexes
AllowOverride None
Require all granted
ExpiresActive On
ExpiresDefault "access plus 30 days"
# Proxy to Node.js
ProxyPreserveHost On
ProxyRequests Off
# WebSocket support
RewriteEngine On
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "ws://localhost:3000/$1" [P,L]
ProxyPass / http://localhost:3000/
ProxyPassReverse / http://localhost:3000/
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Real-IP "%{REMOTE_ADDR}s"
ErrorLog /var/log/apache2/myapp.error.log
CustomLog /var/log/apache2/myapp.access.log combined
Enable and Test
sudo a2ensite myapp.conf
sudo apache2ctl configtest
sudo systemctl reload apache2 SSL with Let's Encrypt (Both Servers)
# Install Certbot
sudo apt install certbot -y
# For Nginx
sudo apt install python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
# For Apache2
sudo apt install python3-certbot-apache -y
sudo certbot --apache -d yourdomain.com -d www.yourdomain.com
# Auto-renewal test
sudo certbot renew --dry-run Nginx vs Apache2 — Quick Comparison
| Feature | Nginx | Apache2 |
|---|---|---|
| Performance | Excellent (event-driven) | Good (process/thread) |
| Configuration | Declarative blocks | Directive-based |
| WebSocket | proxy_set_header Upgrade | mod_proxy_wstunnel + RewriteRule |
| Static files | Faster, built-in | Good with mod_expires |
| .htaccess | Not supported | Full support |
| Memory | Lower | Higher |
| Let's Encrypt | certbot --nginx | certbot --apache |
Recommendation: Use Nginx for new setups. Use Apache2 if you're already running it or need .htaccess support.
Verify the Full Stack
# Check all services
pm2 status
sudo systemctl status nginx # or apache2
# Test SSL
curl -I https://yourdomain.com
# Check headers
curl -I https://yourdomain.com | grep -E "X-Frame|Strict|X-Content"
# Logs
sudo tail -f /var/log/nginx/myapp.error.log
pm2 logs myapp Full Stack Summary
- ✅ Part 1: NVM + Node.js version management + PM2 setup + log rotation
- ✅ Part 2: App running with PM2, cluster mode, zero-downtime reloads
- ✅ Part 3: Nginx/Apache2 reverse proxy + SSL + security headers + WebSocket
Your Node.js app is now running in production — managed by PM2, protected by SSL, served through a proper reverse proxy.
Check out more DevOps guides on khimananda.com.