A reverse proxy receives requests from clients and forwards them to one or more backend applications. It’s how most production apps are served: the app (Node.js, Python, Go, Java, PHP-FPM) listens on a local port, and Nginx handles the public side — TLS, compression, static files, caching and load balancing.

Basic setup

apt install -y nginx         # Debian/Ubuntu
# dnf install -y nginx && systemctl enable --now nginx   # AlmaLinux/Rocky

Assume your app listens on 127.0.0.1:3000. Create /etc/nginx/sites-available/example.com (on RHEL-family systems, use /etc/nginx/conf.d/example.com.conf):

server {
    listen 80;
    server_name example.com www.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;
    }
}
ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

On AlmaLinux/Rocky with SELinux enforcing, allow Nginx to connect to backends: setsebool -P httpd_can_network_connect 1.

Add HTTPS

apt install -y certbot python3-certbot-nginx
certbot --nginx -d example.com -d www.example.com --redirect

Serve static files directly

Let Nginx serve assets and only proxy dynamic requests:

location /static/ {
    alias /var/www/example/static/;
    expires 30d;
    access_log off;
}
location / {
    proxy_pass http://127.0.0.1:3000;
    # headers as above
}

Route by path

location /api/ {
    proxy_pass http://127.0.0.1:4000/;   # trailing slash strips /api/ from the path
}
location / {
    proxy_pass http://127.0.0.1:3000;
}

The trailing slash matters: proxy_pass http://backend/; replaces the matched location prefix; proxy_pass http://backend; passes the original path unchanged. This is the most common reverse-proxy mistake.

Load balancing

upstream app {
    least_conn;
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
    server 10.10.0.2:3000 backup;
    keepalive 32;
}

server {
    # …
    location / {
        proxy_pass http://app;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

Methods: round-robin (default), least_conn, ip_hash (sticky by client IP). Nginx marks failed servers as unavailable automatically (max_fails, fail_timeout).

Caching responses

proxy_cache_path /var/cache/nginx/app levels=1:2 keys_zone=app:50m max_size=1g inactive=60m;

location / {
    proxy_cache app;
    proxy_cache_valid 200 10m;
    proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
    add_header X-Cache $upstream_cache_status;
    proxy_pass http://app;
}

proxy_cache_use_stale keeps serving cached pages if the backend is briefly down.

Timeouts and limits

proxy_connect_timeout 5s;
proxy_read_timeout 60s;      # raise for long requests or WebSockets
client_max_body_size 20m;    # uploads

WebSockets

Add the Upgrade and Connection headers — see secure WebSocket server.

Common errors

Error Meaning
502 Bad Gateway Backend not running / wrong port / SELinux blocking
504 Gateway Timeout Backend too slow — raise proxy_read_timeout or fix the app
413 Request Entity Too Large Raise client_max_body_size
App generates http:// links Pass X-Forwarded-Proto and enable proxy trust in the app

Check /var/log/nginx/error.log first — it names the exact cause.

Summary

Keep apps on localhost, let Nginx handle TLS, static files and routing, and pass the forwarding headers. Related: reverse proxy for multiple Docker containers, VPS as a reverse-proxy gateway and hosting multiple websites.

  • Nginx
  • Reverse Proxy
  • Linux