Most of us learn this the hard way: running your app server on port 8000 and pointing the world at it is a recipe for pain. You need a reverse proxy in front—something to handle SSL termination, load balancing, and hiding your application's real topology.
Nginx is the obvious choice. It's lightweight, battle-tested, and doesn't require a JVM or runtime overhead. This post walks through a production-ready nginx reverse proxy setup on Ubuntu 22.04, with the specific gotchas I've hit.
Install and Enable Nginx
Start with the Ubuntu package, not a third-party PPA (unless you need a newer version for a specific feature).
sudo apt update
sudo apt install nginx
sudo systemctl enable nginx
sudo systemctl start nginx
Verify it's running:
sudo systemctl status nginx
You should see active (running). If not, check /var/log/nginx/error.log.
Gotcha: Ubuntu 22.04 ships nginx 1.18 (released 2019). If you need HTTP/2 or HTTP/3, you'll want the official nginx PPA. For most reverse proxy work, 1.18 is fine.
Basic Reverse Proxy Configuration
Nginx config lives in /etc/nginx/sites-available/. Create a new site file:
sudo nano /etc/nginx/sites-available/myapp
Here's a minimal working config that proxies requests to a backend on localhost:8000:
server {
listen 80;
server_name example.com www.example.com;
location / {
proxy_pass http://localhost:8000;
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;
}
}
Break down what's happening:
proxy_passsends traffic to your backend.Hostheader tells the backend what domain was requested (important if you have multiple domains).X-Real-IPandX-Forwarded-Forpreserve the client's actual IP (your app sees nginx's IP otherwise).X-Forwarded-Prototells the backend whether the client used HTTP or HTTPS (crucial for apps that check protocol).
Enable the site:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
Test the config for syntax errors:
sudo nginx -t
You should see syntax is ok and test is successful. If not, fix the error and test again.
Reload nginx:
sudo systemctl reload nginx
Gotcha: Don't use restart; use reload. Restart closes all connections. Reload keeps existing connections alive while picking up new config.
Add SSL with Let's Encrypt
HTTP-only proxies are fine for internal use. For anything public, you need HTTPS. Certbot makes this trivial.
sudo apt install certbot python3-certbot-nginx
sudo certbot certonly --nginx -d example.com -d www.example.com
Certbot will prompt you for email and terms. Answer the questions and it'll fetch a certificate to /etc/letsencrypt/live/example.com/.
Now update your nginx config to use SSL:
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://localhost:8000;
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 first block redirects all HTTP traffic to HTTPS. The second block handles HTTPS with modern TLS settings.
Test and reload:
sudo nginx -t
sudo systemctl reload nginx
Certbot auto-renews certificates via a systemd timer. Verify it's active:
sudo systemctl status certbot.timer
Gotcha: Certbot's renewal runs twice daily, but renewal only happens if the cert is within 30 days of expiry. You won't see activity most days. Check the timer logs if you're paranoid.
Handle Websockets and Long-Lived Connections
If your backend uses WebSockets, the default proxy settings will time out idle connections. Add these directives inside the location block:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
The first line tells nginx to use HTTP/1.1 (required for WebSocket upgrades). The next two headers pass the upgrade request through. The timeout is set to 24 hours; adjust if needed.
Monitor and Troubleshoot
Watch the access log in real time:
sudo tail -f /var/log/nginx/access.log
You'll see requests as they come through. Look for 502 (bad gateway) errors—that usually means your backend is down or not listening.
For errors, check:
sudo tail -f /var/log/nginx/error.log
Common issues:
- 502 Bad Gateway: Backend not running or listening on the wrong port. Verify with
netstat -tulpn | grep 8000. If you're running a WordPress stack behind nginx, the writeup on wpcamp.web.id covers overlapping gateway and 500-class errors worth knowing about. - Connection refused: Firewall blocking localhost:8000. If your backend is on a different machine, make sure the firewall allows it.
- SSL handshake failures: Check certificate paths and permissions. Nginx runs as
www-data; it needs to read the cert files.
Performance Tuning
For high-traffic setups, tune these in the main nginx config (/etc/nginx/nginx.conf):
worker_processes auto;
worker_connections 2048;
keepalive_timeout 65;
worker_processes autospawns one worker per CPU core.worker_connectionsis per-worker; 2048 is reasonable for most sites.keepalive_timeoutcontrols how long idle connections stay open.
Also add connection pooling to your upstream block (place this outside the server block):
upstream backend {
server localhost:8000 max_fails=3 fail_timeout=30s;
keepalive 32;
}
server {
# ...
location / {
proxy_pass http://backend;
# ... headers ...
}
}
This reuses connections to the backend and marks it down if it fails 3 times in 30 seconds.
Your Checklist
- Install nginx on Ubuntu 22.04
- Write a basic reverse proxy config pointing to your backend
- Test the config with
nginx -t - Reload nginx and verify traffic flows
- Install certbot and obtain an SSL certificate
- Update config to redirect HTTP → HTTPS
- Add WebSocket headers if needed
- Monitor error and access logs for issues
- Set up certbot renewal (automatic on Ubuntu)
- Tune worker processes and connections for your load
That's it. You now have a reverse proxy sitting in front of your application, handling SSL termination, preserving client IPs, and hiding your backend's real address. This is the foundation for everything else—load balancing, caching, rate limiting—but get this part right first.
Next step: log into your server, run through the checklist, and point your domain at it. Test with curl -I https://example.com and watch the access log light up.