Step-by-step
-
1
Understand why a reverse proxy
Running
node server.jsdirectly on port 80 requires root — a bad idea. A reverse proxy solves four things at once:- TLS termination — Nginx handles HTTPS; your app only sees plain HTTP internally
- Static asset caching — Nginx serves
/publicfiles without touching Node - Multiple apps on one host — route
api.example.comto port 3001,app.example.comto port 3000 - No port numbers in URLs — users see
https://example.com, nothttp://example.com:3000
-
2
Install Nginx
Install Nginx from the official package repo and verify it starts clean.
bashsudo apt update sudo apt install -y nginx sudo systemctl enable nginx sudo systemctl start nginx # Confirm it's running sudo systemctl status nginx -
3
Create a server block config file
Drop a new config file in
sites-available. Replaceexample.comwith your actual domain (or the server's IP if you're testing without DNS).bashsudo nano /etc/nginx/sites-available/myapp.conf -
4
Write the proxy_pass block
This is the minimal working config. The four
proxy_set_headerlines are not optional — without them your app sees127.0.0.1as every visitor's IP and can't detect the original protocol.nginxserver { listen 80; server_name example.com www.example.com; location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; # Critical headers 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; } } -
5
Add WebSocket upgrade headers
If your app uses WebSockets (Socket.IO, ws, etc.), add the
UpgradeandConnectionheaders inside thelocation /block. Without these, the WebSocket handshake fails silently.nginx# Inside location / — add these two lines alongside the others proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; -
6
Enable the site and test the config
Symlink the config into
sites-enabled, then always runnginx -tbefore reloading. A typo in the config can take down every site on the machine.bashsudo ln -s /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/ # Test — must say "syntax is ok" sudo nginx -t # Reload (zero downtime — no restart needed) sudo systemctl reload nginx -
7
Keep Node running with pm2
Nginx is now a system service that survives reboots. Your Node app needs the same treatment.
pm2is the simplest option.bashnpm install -g pm2 # Start the app pm2 start server.js --name myapp # Generate and enable the startup script so pm2 restarts on reboot pm2 startup # Run the printed command, then: pm2 save # Useful commands pm2 status pm2 logs myapp pm2 restart myapp -
8
Verify the proxy is working
With your Node app running and Nginx reloaded, hit the server. The response should come from Node even though you're connecting on port 80.
bash# From your local machine (replace with your server IP or domain) curl -I http://example.com # Expect: HTTP/1.1 200 OK (or whatever your app returns) # The "Server: nginx" header confirms Nginx is in the chain -
9
Next step — add free HTTPS with Certbot
Once port 80 is working, adding SSL takes one command. Certbot will edit your Nginx config automatically and set up auto-renewal. See the follow-up guide: How to Get a Free SSL Certificate with Let's Encrypt.
bash# Preview only — full steps in the next guide sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d example.com -d www.example.com
Tips & gotchas
- Always run <code>sudo nginx -t</code> before reloading. A bad config file takes down every site on the server.
- Use <code>upstream</code> blocks when load-balancing across multiple Node processes: <code>upstream myapp { server 127.0.0.1:3000; server 127.0.0.1:3001; }</code>
- Set <code>proxy_read_timeout 300;</code> for long-running requests (file uploads, AI endpoints). The default 60 s will timeout them.
- Add <code>client_max_body_size 20M;</code> if your app accepts file uploads — Nginx's default 1 MB limit will reject them silently.
- Remove the default Nginx site: <code>sudo rm /etc/nginx/sites-enabled/default</code> — it can shadow your config.
Wrapping up
Nginx is now intercepting all traffic on port 80 and forwarding it to your Node app. Your app is isolated from the internet, protected behind a process manager, and ready for HTTPS. The next logical step is running Certbot to get a free TLS certificate — that guide picks up exactly where this one ends.