A reverse proxy sits in front of one or more backend application servers, forwarding client requests to them and returning their responses to the client. From the client’s perspective, the reverse proxy is the web server. This architecture provides centralized SSL termination, load balancing, caching, rate limiting, and DDoS mitigation without exposing backend application ports to the internet. Nginx is an excellent reverse proxy: its event-driven architecture handles thousands of concurrent connections while forwarding requests to Node.js, Python (Gunicorn/uWSGI), Java, Go, PHP-FPM, or any other HTTP backend. This guide covers basic HTTP and HTTPS reverse proxying with Nginx on RHEL 9, including correct header passing, WebSocket proxying, upstream health checks, and buffer tuning.

Prerequisites

  • Nginx installed on RHEL 9
  • A backend application listening on a local port (e.g., a Node.js app on port 3000)

Step 1 — Basic Reverse Proxy Configuration

vi /etc/nginx/conf.d/myapp.conf
server {
    listen       80;
    server_name  app.example.com;

    location / {
        proxy_pass         http://127.0.0.1:3000;
        proxy_http_version 1.1;

        # Pass original client headers to backend
        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;

        # Disable buffering for SSE or streaming responses
        proxy_buffering    off;
        proxy_read_timeout 90s;
    }
}
nginx -t && systemctl reload nginx

# Allow Nginx to make network connections (SELinux)
setsebool -P httpd_can_network_connect 1

Step 2 — Reverse Proxy with SSL Termination

server {
    listen       80;
    server_name  app.example.com;
    return 301   https://app.example.com$request_uri;
}

server {
    listen              443 ssl http2;
    server_name         app.example.com;

    ssl_certificate     /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    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_read_timeout 90s;
    }
}

Step 3 — Upstream Block for Multiple Backends

# Define an upstream group for load balancing
upstream myapp_backend {
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
    keepalive 32;   # Keep connections to backends warm
}

server {
    listen      80;
    server_name app.example.com;

    location / {
        proxy_pass         http://myapp_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;
    }
}

Step 4 — WebSocket Proxying

WebSocket connections require an Upgrade header. Add these directives to proxy WebSocket traffic correctly:

location /ws/ {
    proxy_pass          http://127.0.0.1:3000;
    proxy_http_version  1.1;
    proxy_set_header    Upgrade    $http_upgrade;
    proxy_set_header    Connection "upgrade";
    proxy_set_header    Host       $host;
    proxy_read_timeout  3600s;   # Keep WebSocket connections alive
}

Step 5 — Proxy Specific Paths to Different Backends

server {
    listen      80;
    server_name app.example.com;

    # API routes to a Node.js backend
    location /api/ {
        proxy_pass http://127.0.0.1:4000/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # Static assets served directly by Nginx
    location /static/ {
        alias /var/www/myapp/static/;
        expires 30d;
    }

    # Everything else to the main app
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Step 6 — Buffer and Timeout Tuning

# Add inside the server or location block
proxy_connect_timeout  10s;
proxy_send_timeout     60s;
proxy_read_timeout     60s;

# Buffer settings for large responses
proxy_buffers          8 16k;
proxy_buffer_size      32k;
proxy_busy_buffers_size 64k;

Verification Checklist

nginx -t
curl -I http://app.example.com
curl -si http://app.example.com | grep X-Forwarded
journalctl -u nginx -f

Conclusion

Nginx reverse proxy on RHEL 9 forwards requests to your backend applications while handling SSL termination, header injection, WebSocket upgrade, and path-based routing. The upstream block enables load balancing across multiple backend instances with keepalive connection pooling for maximum throughput.

Next steps: How to Configure Apache mod_proxy as a Reverse Proxy on RHEL 9, How to Configure Nginx Load Balancing on RHEL 9, and How to Configure Nginx Rate Limiting and Connection Throttling on RHEL 9.