Most teams I've worked with run their application servers on private IPs and let nginx face the internet. It's simpler, safer, and gives you a single point to handle SSL, caching, and failover. If you're still pointing DNS at your app server directly, this post is for you.
We'll walk through a working nginx reverse proxy setup on Ubuntu 22.04 — the long-term support release you'll see in production for the next five years. I'm assuming you have root access and a basic understanding of Linux networking.
Install nginx and verify the version
First, update your package lists and install nginx from the Ubuntu repos.
sudo apt update
sudo apt install -y nginx
nginx -v
You'll get nginx/1.18.0 on Ubuntu 22.04. That's old enough to feel stale, but it's stable and gets security patches. If you need a newer version, use the official nginx PPA, but I'd skip that unless you have a specific feature requirement.
Start the service and enable it on boot:
sudo systemctl start nginx
sudo systemctl enable nginx
Create the upstream block
The upstream block tells nginx where your actual application servers live. Create a new file under /etc/nginx/sites-available/:
sudo nano /etc/nginx/sites-available/myapp
Here's a working config for a single backend:
upstream myapp_backend {
server 10.0.1.50:8080;
}
server {
listen 80;
server_name example.com www.example.com;
location / {
proxy_pass http://myapp_backend;
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;
}
}
The proxy_set_header lines are not optional — they tell your backend what the original client IP was. Without X-Forwarded-For, your app logs will show nginx's IP as the source for every request. That's useless for debugging.
Gotcha: If your backend is listening on localhost only, change 10.0.1.50:8080 to 127.0.0.1:8080. Make sure the port isn't already in use.
Enable the site:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp
Test the config before reloading:
sudo nginx -t
You should see syntax is ok and test is successful. If not, nginx will tell you the line number and problem.
Add SSL with Let's Encrypt
HTTP-only proxies are a liability. Install certbot and the nginx plugin:
sudo apt install -y certbot python3-certbot-nginx
Let certbot modify your nginx config automatically:
sudo certbot --nginx -d example.com -d www.example.com
It will ask for your email and whether to redirect HTTP to HTTPS. Choose redirect (option 2). Certbot will update your config file and add a renewal cron job.
Verify the renewal will work:
sudo certbot renew --dry-run
No output is good. If you see errors, check that port 80 is open to the internet and that your DNS is resolving correctly.
Handle multiple backends (load balancing)
If you have several app servers, add them all to the upstream block:
upstream myapp_backend {
server 10.0.1.50:8080;
server 10.0.1.51:8080;
server 10.0.1.52:8080;
}
By default, nginx uses round-robin — each request goes to the next server in the list. If you want to weight them (e.g., one server is beefier), add a weight:
upstream myapp_backend {
server 10.0.1.50:8080 weight=3;
server 10.0.1.51:8080;
server 10.0.1.52:8080;
}
Now the first server gets 3 requests for every 1 to the others.
Gotcha: Round-robin doesn't account for server load. If one backend is slow, nginx will still send it traffic. For smarter distribution, use a load balancer (HAProxy, AWS ALB) in front, or pay for nginx Plus. If you're evaluating whether a more complex orchestration layer makes sense for your team, this guide on Kubernetes complexity is worth a read before committing.
Add connection timeouts and buffering
By default, nginx will wait 60 seconds for your backend to respond. If your app is slow, increase it:
location / {
proxy_pass http://myapp_backend;
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 10s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
These are in seconds. proxy_connect_timeout is how long to wait for the TCP handshake. proxy_read_timeout is how long to wait for the backend to send data. If your app streams large files, increase proxy_read_timeout.
Also enable buffering so nginx doesn't hold client connections open while waiting for the backend:
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
These defaults work for most APIs and web apps. If you're proxying large uploads, you may need to tune them. If you run WooCommerce behind a proxy like this, the WooCommerce hosting requirements benchmark from wpcompass.io has real data on how buffer and timeout settings affect store performance under load.
Monitor the proxy
Watch the nginx error log for backend connection issues:
sudo tail -f /var/log/nginx/error.log
If you see connect() failed (111: Connection refused), your backend isn't listening or the IP/port is wrong. Check it:
sudo netstat -tlnp | grep 8080
Or on Ubuntu 22.04 with ss:
sudo ss -tlnp | grep 8080
For request metrics, check the access log:
sudo tail -f /var/log/nginx/access.log
You should see requests coming in. If the backend is returning 5xx errors, the problem is in your app, not nginx.
Reload and test
Once you're happy with the config, reload nginx:
sudo systemctl reload nginx
Reload is safer than restart — it doesn't drop existing connections.
Test from your local machine:
curl -I https://example.com
You should get a 200 or 302 (redirect) response. If you get 502 Bad Gateway, nginx couldn't reach the backend. Check the error log.
One more thing: keep nginx updated
Ubuntu's unattended-upgrades will patch nginx automatically, but only for security fixes. That's fine. Don't chase minor version bumps unless you need a specific feature.
Check for updates manually:
apt list --upgradable | grep nginx
If there's a new version, test it in a staging environment first. nginx configs are usually backward-compatible, but it's worth checking.
What to do tomorrow
Set up your reverse proxy exactly as shown here. Test it with real traffic. Then spend 15 minutes reading the nginx docs on proxy_pass to understand what each header does. You'll run into edge cases — WebSocket proxying, custom headers, request rewriting — and that knowledge will save you hours of debugging.
If your backend is slow or you're seeing timeouts, the proxy isn't the problem. Profile your app. If you're seeing high CPU on the nginx box, you might need to tune worker processes, but that's rare on modern hardware.