Set up Nginx as a reverse proxy on Ubuntu 24.04

Your Node.js app is listening on localhost:3000, your Rails server on localhost:9292, and you need both visible on port 80 and 443 without fighting over the same socket. The standard solution in 2026 is still an Nginx reverse proxy, and on an Ubuntu 24.04 VPS it takes about ten minutes to set up if you know the exact directives. This guide walks through installing Nginx, writing a clean proxy_pass configuration, terminating TLS, proxying WebSocket, and verifying every step actually works.
Prerequisites
- An Ubuntu 24.04 VPS with a non-root sudo user (
ubuntuin the examples below). If you have not created one yet, do that before anything else. - A domain name pointing an A record to your server's IPv4 (e.g.
app.example.com). For a quick test, a server IP alone is enough for the first half. - A backend application already listening on a local port, for example
127.0.0.1:3000. - Ports 80 and 443 reachable from the internet. If you use
ufw, allow them explicitly.
Why put Nginx in front of your application
A backend server like Node.js, Gunicorn or a Java app is written to handle application logic, not to defend against slow clients, malformed HTTP, or TLS renegotiation attacks. Nginx handles that layer efficiently with a single-threaded event loop and battle-tested C code. It terminates TLS once, then talks plain HTTP to your backend on localhost, which saves CPU cycles on encryption for every request.
A reverse proxy also gives you one public entry point for multiple services. In my setup I run an API on :3000, a docs site on :8080 and a monitoring dashboard on :9090, all behind Nginx on the same IPv4. That is far easier to manage than assigning a separate port or IP to each, and it lets you add TLS, caching, compression, and access control in one place. A VPS with a dedicated IPv4 is exactly what you want here, one address, one reverse proxy, many services.
Step 1 - Install and start Nginx
Ubuntu ships a recent stable Nginx in its repositories, no need for a third-party PPA in most cases.
sudo apt update
sudo apt install nginx -y
sudo systemctl enable --now nginx
The enable --now flag both starts the service and configures it to start on boot, which is what a server should do.
Verify:
systemctl status nginx --no-pager
# expect: active (running)
Check that the default page responds on your public IP:
curl -I http://your_server_ip
# expect: HTTP/1.1 200 OK
If you use ufw, open the ports before testing from a browser:
sudo ufw allow 'Nginx Full'
sudo ufw status
Nginx Full opens both 80 and 443. The default site from the package is enough to prove the server works; you will remove it later.
Step 2 - Create a minimal reverse proxy configuration
Nginx on Ubuntu reads site configurations from /etc/nginx/sites-available/, and you enable one by symlinking it into /etc/nginx/sites-enabled/. This layout lets you keep disabled configs around for reference.
Create a config file for your app:
sudo nano /etc/nginx/sites-available/app.example.com
Start with a plain HTTP proxy, no TLS yet:
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
}
}
Disable the default site, enable yours, and test the syntax:
sudo rm /etc/nginx/sites-enabled/default
sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t
Verify:
# expect: syntax is ok, test is successful
sudo systemctl reload nginx
curl -H "Host: app.example.com" http://your_server_ip
# expect: the HTTP response body of your backend app
The -H "Host:..." trick lets you test the vhost without touching DNS. When you see your app's HTML, the proxy is routing correctly.
Why the header lines matter
Without the proxy_set_header block, your backend sees the request coming from 127.0.0.1 with Host: your_server_ip. That breaks anything that depends on the real client IP, like rate limiting, audit logs, or geo-blocking. The four headers above preserve the original request information:
Host $hostkeeps the domain name the client used, so virtual hosting on the backend still works.X-Real-IP $remote_addrpasses the actual client address.X-Forwarded-Forappends to the chain of proxies, useful when you add more layers later.X-Forwarded-Proto $schemetells the backend whether the original request was HTTP or HTTPS.
Set proxy_http_version 1.1 so upstream keep-alive works; the default 1.0 closes the connection after every request, which costs you a TCP handshake each time.
Step 3 - Terminate TLS with Let's Encrypt
Running a reverse proxy without HTTPS in 2026 is not an option, browsers mark it insecure and HTTP/2 or HTTP/3 require it. The fastest path is Certbot with the Nginx plugin, which edits your config and reloads the server for you.
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d app.example.com
Certbot will ask for an email address and whether to redirect HTTP to HTTPS. Answer yes to the redirect. It then fetches a certificate, inserts the TLS block into your config, and enables automatic renewal.
Verify:
sudo certbot certificates
# expect: a Certificate Name matching app.example.com with an Expiry date
curl -I https://app.example.com
# expect: HTTP/2 200
Test the auto-renewal timer is active:
systemctl list-timers | grep certbot
# expect: a timer scheduled twice a day
If you prefer to buy a certificate from a commercial CA instead, the config structure is identical, you only swap the ssl_certificate and ssl_certificate_key paths. Certbot just automates the acquisition and renewal.
Step 4 - Proxy WebSocket connections
Applications like n8n, LiveKit, or any real-time dashboard use WebSocket, which needs two extra headers to survive the proxy. Add them inside the location block:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# ... other headers from before
}
The Upgrade header tells the backend the client wants to switch protocols, and Connection "upgrade" keeps the socket open after the handshake. Without these two lines, the WebSocket connection drops within a second or two, showing repeated reconnects in the browser console.
Verify: open your app's console in a browser and confirm the WebSocket shows status: connected with no reconnect loop. From the server, you can watch the access log for 101 responses:
sudo tail -f /var/log/nginx/access.log | grep " 101 "
Step 5 - Load balance across multiple backends
One backend is fine for moderate traffic, but a production Linux VPS with spare capacity can run several instances of the same app. Define an upstream block and point proxy_pass at it:
upstream app_cluster {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
server 127.0.0.1:3002;
}
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://app_cluster;
# headers as before
}
}
Nginx round-robins across the three ports by default, and if one fails a health check it is removed from rotation temporarily. Add least_conn as the first line of the upstream block to send each request to the backend with the fewest active connections, a better fit when request times vary. You can also set max_fails and fail_timeout per server to tune how aggressively Nginx skips a sick backend.
Common troubleshooting
502 Bad Gateway. Nginx cannot reach the backend, either it is not running or it listens on a different port than proxy_pass points to. Diagnose with:
ss -tlnp | grep :3000
sudo tail -f /var/log/nginx/error.log
Fix: start the app, or correct the port in the config.
404 on the backend root even though the app works directly. The backend often mounts routes under a path like /api while your location / forwards the full URI. If the backend expects to be served at its root, you can rewrite the path:
location /api/ {
proxy_pass http://127.0.0.1:3000/;
}
Note the trailing slash on proxy_pass, it strips the /api prefix before forwarding.
WebSocket reconnects constantly. Forgot the Upgrade and Connection "upgrade" headers, or a proxy layer in front (like Cloudflare) is buffering. Add the headers and, if you use Cloudflare, enable WebSocket in the network settings.
FAQ
What is the difference between a reverse proxy and a forward proxy?
A reverse proxy sits in front of your backend servers and routes client requests to them, hiding the backend topology. A forward proxy sits in front of clients and routes their requests to the internet, typically for filtering or anonymity. Nginx is usually configured as a reverse proxy.
Can Nginx reverse proxy HTTP/2 or gRPC traffic?
Yes for HTTP/2, via the http2 directive on the listen line and by keeping the negotiated protocol with grpc_pass for gRPC backends. HTTP/3 is supported experimentally in recent Nginx builds, but for a stable setup in production, terminate HTTP/2 at the proxy and let the backend speak plain HTTP/1.1.
Should I use Nginx or Caddy as a reverse proxy in 2026?
Nginx remains the performance leader and the default choice for complex routing and high concurrency. Caddy wins on simplicity with automatic HTTPS out of the box and a much simpler config language. If you want zero configuration friction, see our guide on reverse proxy with automatic HTTPS using Caddy.
How do I secure the backend if it still listens on a public port?
Bind the backend to 127.0.0.1 instead of 0.0.0.0, then only Nginx can reach it. If the backend must bind to 0.0.0.0 for other reasons, block the port with ufw or a cloud firewall rule.
Why does my backend see the wrong client IP in the logs?
Because the X-Real-IP and X-Forwarded-For headers are not set, or your backend framework ignores them. Add the header lines from Step 2, and configure the framework (e.g. Express with app.set('trust proxy', 1)) to read them.
Related articles
- Nginx tuning for high traffic worker gzip buffer cache
- Enable HTTP/3 and Brotli compression on Nginx
- Run multiple sites on one VPS with Docker and Nginx proxy
- Zero downtime deploy with Nginx and systemd on Ubuntu 24.04
Nginx 反向代理配置要点
在 Ubuntu 24.04 VPS 上配置 Nginx 反向代理,核心是写好 proxy_pass 和请求头转发,让后端拿到真实客户端 IP。用 Certbot 自动申请 TLS 证书,并添加 Upgrade 和 Connection 头以支持 WebSocket。生产环境建议用 upstream 块做负载均衡。测试时用 nginx -t 校验语法,用 curl 加 Host 头验证转发是否生效。


