Nginx Reverse Proxy Tutorial: Complete Setup Guide

by David Park
Nginx Reverse Proxy Tutorial: Complete Setup Guide

What You'll End Up With

This nginx reverse proxy tutorial walks you through configuring Nginx on Ubuntu 24.04 to sit in front of one or more backend applications — Node.js, Python, Go, whatever — and forward requests to them over HTTP. By the end you'll have a working reverse proxy with proper headers, optional SSL termination via Certbot, and a systemd-managed Nginx service you can trust in production.

Prerequisites

  • Ubuntu 24.04 LTS server (fresh or existing)
  • A non-root user with sudo privileges
  • A backend process already listening on a local port (e.g., 127.0.0.1:3000)
  • A domain name pointed at the server's IP (required for the SSL section)
  • Ports 80 and 443 open in your firewall

Step 1: Install Nginx

Install Nginx from the Ubuntu package repository.

sudo apt update && sudo apt install -y nginx

Enable and start the service so it survives reboots.

sudo systemctl enable --now nginx

Verify the service is running.

sudo systemctl status nginx

Expected output (abbreviated):

● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled)
     Active: active (running)

Step 2: Create a Site Configuration File

Nginx on Ubuntu 24.04 reads per-site configs from /etc/nginx/sites-available/ and activates them via symlinks in /etc/nginx/sites-enabled/. Create a new config for your domain.

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

Paste the following block, replacing app.example.com with your domain and 3000 with your backend port.

server {
    listen 80;
    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;
        proxy_set_header   Upgrade           $http_upgrade;
        proxy_set_header   Connection        "upgrade";

        proxy_read_timeout    60s;
        proxy_connect_timeout 10s;
        proxy_send_timeout    60s;

        proxy_buffering    on;
        proxy_buffer_size  8k;
        proxy_buffers      8 8k;
    }
}

What each directive does:

  • proxy_pass — forwards requests to the backend address.
  • proxy_http_version 1.1 — required for WebSocket keepalive connections.
  • X-Real-IP / X-Forwarded-For — passes the real client IP to your app.
  • X-Forwarded-Proto — tells the app whether the original request was HTTP or HTTPS.
  • Upgrade / Connection — enables WebSocket proxying.
  • Timeout values — sane defaults that prevent Nginx from hanging indefinitely.

Step 3: Enable the Site and Test the Configuration

Create a symlink to activate the site.

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

Remove the default site if it's still enabled, to avoid a conflicting server_name _ catch-all.

sudo rm -f /etc/nginx/sites-enabled/default

Test the Nginx configuration for syntax errors.

sudo nginx -t

Expected output:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Reload Nginx to apply the new configuration without dropping active connections.

sudo systemctl reload nginx

Step 4: Configure SSL with Certbot

Skip this step if you're proxying internal traffic only. For any public-facing service, terminate TLS at Nginx.

Install Certbot and the Nginx plugin.

sudo apt install -y certbot python3-certbot-nginx

Obtain and install a certificate for your domain. Certbot will automatically edit your Nginx config to add the listen 443 ssl block and redirect HTTP to HTTPS.

sudo certbot --nginx -d app.example.com

Follow the prompts. When asked whether to redirect HTTP to HTTPS, choose option 2 (redirect).

Certbot installs a systemd timer that auto-renews certificates. Confirm it's active.

sudo systemctl status certbot.timer

Expected output:

● certbot.timer - Run certbot twice daily
     Loaded: loaded (/usr/lib/systemd/system/certbot.timer; enabled)
     Active: active (waiting)

After Certbot finishes, your config at /etc/nginx/sites-available/myapp will contain the SSL directives. Test and reload again.

sudo nginx -t && sudo systemctl reload nginx

Step 5: Proxy Multiple Backend Services

A single Nginx instance can proxy several apps on different subdomains or paths. Create a separate config file for each service, or use upstream blocks to define named backend pools.

Create a second site config for an API backend running on port 4000.

sudo nano /etc/nginx/sites-available/myapi
upstream api_backend {
    server 127.0.0.1:4000;
    keepalive 32;
}

server {
    listen 80;
    listen [::]:80;
    server_name api.example.com;

    location / {
        proxy_pass         http://api_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_read_timeout    60s;
        proxy_connect_timeout 10s;
        proxy_send_timeout    60s;
    }
}

Why upstream with keepalive 32? It instructs Nginx to maintain a pool of persistent connections to the backend, reducing TCP handshake overhead on high-traffic APIs.

Enable and reload.

sudo ln -s /etc/nginx/sites-available/myapi /etc/nginx/sites-enabled/myapi
sudo nginx -t && sudo systemctl reload nginx

Verify It Works

Run each check in order.

1. Confirm Nginx is listening on the expected ports.

sudo ss -tlnp | grep nginx

Expected output:

LISTEN  0  511  0.0.0.0:80   0.0.0.0:*  users:(("nginx",...)
LISTEN  0  511  0.0.0.0:443  0.0.0.0:*  users:(("nginx",...)

2. Send a test request through the proxy.

curl -I http://app.example.com

If SSL is configured, test HTTPS.

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

Expected output includes HTTP/2 200 (or HTTP/1.1 200) and the response headers from your backend.

3. Confirm the forwarded IP header reaches your app.

On your backend, log the X-Forwarded-For header and compare it against your client's public IP.

4. Check the Nginx access log in real time.

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

Make a request from a browser and watch the log line appear with your IP, status code, and upstream response time.


Troubleshooting

502 Bad Gateway Nginx reached the backend address but got no response. Confirm your backend process is running and listening on the correct port.

sudo ss -tlnp | grep 3000

If the port is empty, your backend crashed. Check its logs.

504 Gateway Timeout The backend is running but not responding within the timeout window. Increase proxy_read_timeout in the location block, or investigate why the backend is slow.

nginx -t fails with "conflicting server name" Two config files declare the same server_name. Check /etc/nginx/sites-enabled/ for duplicates and remove the conflicting file.

ls -la /etc/nginx/sites-enabled/

Certificate renewal fails Run a dry-run to diagnose.

sudo certbot renew --dry-run

Common causes: port 80 blocked by a firewall rule, or the server_name in the Nginx config doesn't match the certificate domain.

"connect() failed (111: Connection refused)" in error log Nginx can't reach 127.0.0.1:<port>. Verify the backend is bound to 127.0.0.1 and not 0.0.0.0 on a different interface, and that no iptables rule is blocking loopback traffic.

sudo tail -50 /var/log/nginx/error.log

Client IP shows 127.0.0.1 in app logs instead of real IP Your backend is reading the wrong header. Configure it to trust the X-Forwarded-For header. For Express.js: set app.set('trust proxy', 1). For Django: add SECURE_PROXY_SSL_HEADER and use django-ipware.


Next Steps

With the nginx reverse proxy tutorial complete, consider these additions for a production-hardened setup:

  • Rate limiting — Add limit_req_zone and limit_req directives to protect against abusive clients.
  • Gzip compression — Enable gzip on in /etc/nginx/nginx.conf to reduce bandwidth on text responses.
  • Security headers — Add X-Content-Type-Options, X-Frame-Options, and Strict-Transport-Security in a add_header block.
  • Log rotation — Ubuntu's default logrotate config at /etc/logrotate.d/nginx rotates logs weekly; adjust to daily if traffic is high.
  • Monitoring — Enable the ngx_http_stub_status_module and scrape metrics with Prometheus and Grafana setup tutorial for visibility into active connections and request rates.

All of the above can run comfortably on a €4/month Hetzner CAX11 instance — no need to over-provision.