Nginx Reverse Proxy Setup on Ubuntu 22.04

by Liam Foster
Nginx Reverse Proxy Setup on Ubuntu 22.04

Most teams expose their app servers to the internet and wonder why they're getting hammered with garbage traffic. A reverse proxy sits between clients and your backends — it filters, caches, and routes traffic intelligently. Nginx does this better than most, and it'll run on a $5 VPS without breaking a sweat.

I'm walking you through a nginx reverse proxy setup on Ubuntu 22.04 that handles real traffic, not a hello-world example. You'll get SSL termination, upstream health checks, and the specific gotchas I've hit in production.

Install Nginx and Verify It Works

Start fresh. Ubuntu 22.04 ships with nginx 1.18 in the default repos, but I'd rather have 1.24 or later for better HTTP/2 support and security patches.

curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/ubuntu `lsb_release -cs` nginx" | sudo tee /etc/apt/sources.list.d/nginx.list
sudo apt update && sudo apt install -y nginx
nginx -v

You should see nginx/1.24.x or newer. Start the service and enable it on boot:

sudo systemctl start nginx
sudo systemctl enable nginx

Hit http://your-server-ip in a browser. You'll see the default nginx welcome page. Good — nginx is running.

Configure the Upstream Servers

Your reverse proxy needs to know where to send traffic. Let's say you have two backend app servers running on ports 8080 and 8081 (maybe Node.js, Python, Go — doesn't matter).

Edit the main config:

sudo nano /etc/nginx/nginx.conf

Inside the http block, add an upstream group before the include for server configs:

http {
    upstream app_backend {
        least_conn;  # load balancing method
        server 10.0.1.10:8080 max_fails=3 fail_timeout=30s;
        server 10.0.1.11:8081 max_fails=3 fail_timeout=30s;
        keepalive 32;  # connection pooling
    }
    
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    # ... rest of http block
}

The least_conn algorithm sends traffic to whichever backend has fewer active connections — better than round-robin for variable request times. The max_fails and fail_timeout settings tell nginx to stop sending traffic to a backend for 30 seconds if it fails 3 times in a row. The keepalive pool reuses connections to backends, cutting latency.

Create a Server Block for Your Domain

Create a new config file for your site:

sudo nano /etc/nginx/sites-available/myapp.conf

Paste this:

server {
    listen 80;
    server_name myapp.example.com;
    
    # Redirect all HTTP to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name myapp.example.com;
    
    # SSL certificates (we'll get these next)
    ssl_certificate /etc/letsencrypt/live/myapp.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/myapp.example.com/privkey.pem;
    
    # SSL hardening
    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;
    
    # Logging
    access_log /var/log/nginx/myapp_access.log;
    error_log /var/log/nginx/myapp_error.log warn;
    
    # Reverse proxy settings
    location / {
        proxy_pass http://app_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        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 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

Enable the config:

sudo ln -s /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/

Get SSL Certificates with Certbot

You need HTTPS. Certbot handles Let's Encrypt renewal automatically.

sudo apt install -y certbot python3-certbot-nginx
sudo certbot certonly --standalone -d myapp.example.com

Certbot will prompt you for an email and ask you to agree to the terms. It'll drop certificates in /etc/letsencrypt/live/myapp.example.com/. The renewal runs via a systemd timer that checks daily and renews if the cert is within 30 days of expiry.

Gotcha: If you're running nginx on port 443 already, --standalone will fail because it can't bind port 80. Stop nginx first, run certbot, then restart nginx.

Test the Config and Reload

Before you reload, validate the syntax:

sudo nginx -t

You should see syntax is ok and test is successful. If there's an error, it'll tell you the line number. Fix it and test again.

Once it passes:

sudo systemctl reload nginx

Reload is graceful — it doesn't drop active connections. Test your domain:

curl -I https://myapp.example.com

You should get a 200 or 302 (depending on your backend). Check the SSL certificate:

openssl s_client -connect myapp.example.com:443 -servername myapp.example.com

Look for Verify return code: 0 (ok). If it says something else, your cert chain is broken.

Monitor and Troubleshoot

Watch the access log in real time:

sudo tail -f /var/log/nginx/myapp_access.log

If requests are slow or timing out, check the error log:

sudo tail -f /var/log/nginx/myapp_error.log

Common issues:

  • "upstream timed out": Your backends are slow or down. Check if they're responding to curl http://10.0.1.10:8080.
  • "connection refused": The upstream IP or port is wrong. Verify with netstat -tlnp on the backend server.
  • SSL handshake failures: Run certbot renew --dry-run to check if renewal works. If not, you might have a firewall blocking port 80.

Performance Tuning (Optional, But Worth It)

If you're seeing high CPU or connection limits, tweak the worker processes in /etc/nginx/nginx.conf:

worker_processes auto;
worker_connections 4096;

The auto setting tells nginx to spawn one worker per CPU core. worker_connections is the max concurrent connections per worker. On a 2-core machine with 4096 connections per worker, you can handle 8192 simultaneous connections.

Reload and check:

sudo systemctl reload nginx
nginx -s reload
ps aux | grep nginx

You should see one master process and N worker processes.

What to Do Tomorrow

You've got a nginx reverse proxy setup on Ubuntu 22.04 that terminates SSL, balances traffic across backends, and reuses connections. It's not fancy, but it's solid.

Next steps: Add monitoring. Point your monitoring tool at the access logs or set up nginx metrics in Prometheus. Watch for slow backends (check upstream_response_time in your logs). Set up log rotation with logrotate so /var/log/nginx/ doesn't fill your disk.

Don't skip the SSL hardening — the config above uses modern TLS versions and strong ciphers. Test it with testssl.sh if you're paranoid. You should get an A or A+ rating. If you're choosing a host for this kind of setup, the comparison on wpcompass.io is worth a look for understanding how uptime guarantees actually hold up in practice.

If your backends start failing, the max_fails and fail_timeout settings will take them out of rotation automatically. But you still need monitoring to know they failed. Don't rely on nginx to be your canary.