Running a Node.js app behind Nginx is a battle-tested pattern: Nginx handles TLS, static files, gzip, and connection upgrades (for WebSockets), while your Node app focuses on business logic. This guide gives you a clean, production-ready Nginx config and shows how to test it with curl.
What you’ll set up
- A Node.js app listening on 127.0.0.1:3000
- Nginx reverse proxy on port 80/443
- WebSocket support
- Gzip compression & basic hardening
- Simple tests with curl
Works on Ubuntu/Debian and CentOS/RHEL. Adjust paths if your distro uses different Nginx locations.
1) Minimal Node.js app (for testing)
Create a tiny server to verify the proxy:
mkdir -p /var/www/node-app && cd /var/www/node-app
cat > server.js <<'EOF'
const http = require('http');
const server = http.createServer((req, res) => {
// Simple health route
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ ok: true, via: 'node', time: new Date().toISOString() }));
}
// Echo headers (useful for proxy tests)
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
url: req.url,
headers: req.headers,
message: 'Hello from Node behind Nginx!'
}));
});
server.listen(3000, '127.0.0.1', () => {
console.log('Node app listening on http://127.0.0.1:3000');
});
EOF
node server.js
You should see: Node app listening on http://127.0.0.1:3000
2) (Optional) Keep Node running with systemd
sudo tee /etc/systemd/system/node-app.service >/dev/null <<'EOF'
[Unit]
Description=Node.js Example App
After=network.target
[Service]
WorkingDirectory=/var/www/node-app
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=3
Environment=NODE_ENV=production
User=www-data
Group=www-data
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now node-app
sudo systemctl status node-app --no-pager
On CentOS/RHEL, the Node path is usually /usr/bin/node too; user/group may be nginx instead of www-data.
3) Nginx reverse proxy config (HTTP)
Create a site config (Debian/Ubuntu style):
sudo mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
sudo tee /etc/nginx/sites-available/node-proxy.conf >/dev/null <<'EOF'
# Replace example.com with your domain
server {
listen 80;
server_name example.com www.example.com;
# Gzip for JSON/JS/CSS
gzip on;
gzip_types text/plain application/json application/javascript text/css;
gzip_min_length 1024;
# Proxy timeouts (tune as needed)
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
send_timeout 60s;
# Forward real client IPs
set $upstream http://127.0.0.1:3000;
location / {
proxy_pass $upstream;
# Preserve Host and IP info
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;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Don't buffer SSE/WebSockets too aggressively
proxy_buffering off;
}
# Health path can be cached off for clarity
location = /health {
proxy_pass $upstream;
proxy_set_header Host $host;
proxy_buffering off;
}
# Basic hardening
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
# Map for Connection upgrade (place in http{} block if in main nginx.conf)
}
EOF
Create a small map to handle Connection header for WS upgrades. Put this once in your global http {} block (e.g., /etc/nginx/nginx.conf), outside the server block:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
Enable and test:
sudo ln -s /etc/nginx/sites-available/node-proxy.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
CentOS/RHEL path: often a single /etc/nginx/nginx.conf with server {} blocks in /etc/nginx/conf.d/*.conf. If so, save as /etc/nginx/conf.d/node-proxy.conf and skip the symlink step.
4) Add HTTPS (Let’s Encrypt)
If your DNS points to the server, you can set up TLS quickly:
# Install Certbot
# Ubuntu/Debian:
sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
# CentOS/RHEL (Stream 8/9; may be 'dnf' and package names differ slightly):
# sudo dnf install -y certbot python3-certbot-nginx
# Obtain and auto-configure SSL for the server block with server_name example.com
sudo certbot --nginx -d example.com -d www.example.com
# Auto renewal (usually installed by default)
sudo systemctl status certbot.timer --no-pager
Certbot will add listen 443 ssl http2; and ssl_certificate lines for you.
5) Test with
curl
A. Test that Nginx reaches Node
# Replace example.com with your domain or server IP (if HTTP only for now)
curl -i http://example.com/health
Expected:
- HTTP/1.1 200 OK (or HTTP/2 200 if HTTPS)
- JSON body like: {“ok”:true,”via”:”node”,”time”:”…”}
- Response header server: nginx (from Nginx) and body says it came from Node
B. Check headers forwarded by Nginx
curl -i http://example.com/ -H "X-Demo: test123"
Look for:
- Host: example.com
- x-demo: test123
- x-forwarded-proto: http (or https if TLS enabled)
- x-real-ip set to your client IP
C. Test HTTPS (after Certbot)
curl -I https://example.com/health
You should see:
- HTTP/2 200
- TLS certificate fields if you add -v
D. (Optional) WebSocket sanity test
If your Node app upgrades connections at /ws, you can verify the handshake:
curl -i -N \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Host: example.com" \
-H "Origin: https://example.com" \
http://example.com/ws
You should get 101 Switching Protocols if your backend handles WS.
6) Common pitfalls & fixes
- 502 Bad Gateway
- Node app not running or wrong upstream port/host.
- SELinux (CentOS) may block Nginx from proxying: setsebool -P httpd_can_network_connect 1
- WebSockets not upgrading
- Missing proxy_http_version 1.1, Upgrade, and Connection headers.
- The map $http_upgrade $connection_upgrade must be in http{}.
- Wrong client IP
- Ensure proxy_set_header X-Real-IP $remote_addr; and X-Forwarded-For.
- TLS issues
- DNS not pointing to server before running Certbot.
- Port 80/443 blocked by firewall.
7) Full example (drop-in) for
/etc/nginx/conf.d/node-proxy.conf
# Place this map ONCE in /etc/nginx/nginx.conf inside http { }:
# map $http_upgrade $connection_upgrade { default upgrade; '' close; }
server {
listen 80;
server_name example.com www.example.com;
gzip on;
gzip_types text/plain application/json application/javascript text/css;
gzip_min_length 1024;
set $upstream http://127.0.0.1:3000;
location / {
proxy_pass $upstream;
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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
location = /health {
proxy_pass $upstream;
proxy_set_header Host $host;
proxy_buffering off;
}
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
}
Reload Nginx after changes:
sudo nginx -t && sudo systemctl reload nginx
Final checklist
- Node app runs on 127.0.0.1:3000 (or your chosen port)
- Nginx config enabled and tested (nginx -t)
- Health endpoint returns 200 via Nginx
- TLS installed with Certbot (optional but recommended)
- curl tests show correct headers and status codes
Tags: Nginx, Node.js, Reverse Proxy, WebSockets, Linux, CentOS, Ubuntu, DevOps
Meta Description: Learn how to put Node.js behind Nginx with a production-ready reverse proxy config, WebSocket support, and quick tests using curl.
Leave a Reply