Most sysadmins reach for nginx reverse proxy setup Ubuntu 22.04 when they've got backend services scattered across private IPs and need a single entry point. It's the right call. nginx is lightweight, fast, and won't surprise you at 3 a.m.
But there's a gap between "install nginx" and "production-ready proxy." This post fills it. I'll walk you through the setup, SSL termination, and the gotchas I've seen break deployments.
Install and verify nginx
Start with a clean Ubuntu 22.04 box. You'll need sudo access.
sudo apt update
sudo apt install -y nginx
sudo systemctl start nginx
sudo systemctl enable nginx
Verify it's running:
sudo systemctl status nginx
curl http://localhost
You should see the default nginx welcome page. Good. Now stop it—we're about to replace the config.
sudo systemctl stop nginx
Gotcha: Ubuntu 22.04 ships with nginx 1.18.0 (from Focal backports). It's stable but missing some modern features. If you need HTTP/2 push or the latest QUIC support, add the official nginx PPA. For most reverse proxy work, 1.18 is fine.
Configure the reverse proxy
Backup the default config first:
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
Create a new upstream block and server block. Edit /etc/nginx/nginx.conf and replace the http block with this:
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 20M;
# Define upstream backend servers
upstream backend {
least_conn;
server 10.0.1.10:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.12:8080 backup;
keepalive 32;
}
server {
listen 80 default_server;
server_name example.com www.example.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2 default_server;
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;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
access_log /var/log/nginx/reverse_proxy_access.log main;
error_log /var/log/nginx/reverse_proxy_error.log warn;
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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_buffering off;
proxy_request_buffering off;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Let me break down the key decisions here:
upstream block: I used least_conn load balancing—it sends new requests to whichever backend has the fewest active connections. Better than round-robin for long-lived requests. The max_fails=3 fail_timeout=30s tells nginx to mark a backend as down after 3 failures within 30 seconds. The backup server only gets traffic if the primary two are down.
proxy_set_header directives: These are non-negotiable. Without X-Forwarded-For and X-Forwarded-Proto, your backend app won't know the client's real IP or whether the connection was originally HTTPS. Your logging and security checks will be broken.
proxy_buffering off: This matters if you're proxying file downloads or streaming responses. Buffering on (the default) means nginx reads the entire response before sending it to the client—bad for large files.
Gotcha: If your backend uses cookies or session affinity, least_conn won't work. Switch to ip_hash (hash by client IP) or add a session store. If you're weighing whether a service-oriented split is even worth it for your scale, why monolithic architecture still wins is worth a read before committing to a multi-backend setup.
Set up SSL with Let's Encrypt
You'll need certbot:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot certonly --standalone -d example.com -d www.example.com
Answer the prompts. Certbot will drop certs in /etc/letsencrypt/live/example.com/.
Add auto-renewal:
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer
Verify the timer:
sudo systemctl list-timers certbot.timer
Gotcha: If you use --standalone, certbot needs port 80 free during renewal. Run sudo systemctl stop nginx before renewal, or use the --webroot plugin instead. I prefer --standalone on a dedicated proxy box with nothing else on port 80.
Test the configuration
Before reloading, validate the syntax:
sudo nginx -t
You should see:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
If not, nginx will tell you the line number. Fix it and try again.
Now start nginx:
sudo systemctl start nginx
sudo systemctl status nginx
Test from your local machine:
curl -I https://example.com
You should get a 200 OK and see your backend's response headers (minus any server headers your backend sent—nginx strips them by default, which is good for security).
Test the health endpoint:
curl http://example.com/health
Should return healthy.
Monitor and troubleshoot
Watch the logs in real time:
sudo tail -f /var/log/nginx/reverse_proxy_access.log
sudo tail -f /var/log/nginx/reverse_proxy_error.log
Common issues:
502 Bad Gateway: Backend is down or unreachable. Check upstream server IPs and ports. Verify firewall rules allow traffic from the proxy box to the backend.
telnet 10.0.1.10 8080
504 Gateway Timeout: Backend is slow. Increase proxy_read_timeout. Start with 120s if you have long-running requests.
Connection refused: nginx is listening but the upstream isn't. Check sudo netstat -tlnp | grep nginx to confirm nginx is bound to the right port.
Performance tuning
For high throughput, tune the worker processes:
worker_processes auto;
worker_connections 4096;
Add this at the top of /etc/nginx/nginx.conf, outside the http block. auto detects CPU cores; worker_connections is per worker.
Gotcha: Don't blindly max out worker_connections. Each connection uses memory and file descriptors. Monitor ulimit -n and adjust /etc/security/limits.conf if needed.
Enable gzip compression for text responses (add inside the http block):
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 1024;
What to do tomorrow
Set up this nginx reverse proxy setup Ubuntu 22.04 on a test box first. Point a backend app at it, run load tests, and watch the logs. Once you're confident, deploy to production with monitoring on upstream response times and error rates. Add a health check endpoint to each backend if you don't have one—it's the only way nginx knows a server is actually alive. For a broader look at what else changed in the Apple ecosystem that might affect your infrastructure decisions, the Apple event recap key takeaways over at techbulletin.net covers the highlights worth knowing.
Don't skip SSL. Let's Encrypt is free and certbot automation removes the excuse.