Set Up a Reverse Proxy with Nginx

Khimananda Oli 10 min read Database
Set Up a Reverse Proxy with Nginx

By Khimananda Oli | Last reviewed: August 2026

You need to set up a reverse proxy with Nginx when your application requires SSL termination, load balancing, or protection from direct client exposure. Unlike a forward proxy that acts on behalf of clients, a reverse proxy sits in front of your backend servers, handling TLS, caching, and request routing while keeping your infrastructure topology hidden. This guide walks through the exact configuration patterns I use in production across AWS, Azure, and on-premise Ubuntu servers.

How Do You Set Up a Reverse Proxy with Nginx for Basic Traffic Forwarding?

The foundation of any reverse proxy configuration is the relationship between the frontend listener and the backend upstream. Before writing config files, ensure you have completed the prerequisite steps to install Nginx on Ubuntu and verified that your firewall allows ports 80 and 443. A common mistake is editing /etc/nginx/nginx.conf directly; instead, create modular site configurations in /etc/nginx/sites-available/ to maintain auditability and simplify rollbacks.

Client BrowserHTTPS :443Nginx Reverse ProxySSL TerminationHeader InjectionAccess LoggingRate LimitingBackend App 1HTTP :8080Backend App 2HTTP :8080Backend App 3HTTP :8080
Basic Nginx reverse proxy architecture: clients connect via HTTPS to Nginx, which terminates SSL and forwards plain HTTP to multiple backend application servers.

Define the Upstream Backend Pool

The upstream directive tells Nginx where to send proxied traffic. Place this outside your server block, typically at the top of your site configuration file or in a dedicated /etc/nginx/conf.d/upstreams.conf.

upstream backend_app {
    least_conn;
    server 127.0.0.1:8080 weight=3;
    server 127.0.0.1:8081 weight=2;
    server 192.168.1.50:8080 backup;
    keepalive 32;
}
  • least_conn: Routes new requests to the backend with the fewest active connections, preventing overload on slow endpoints.
  • weight: Distributes traffic proportionally; useful when backends have different CPU/RAM allocations.
  • backup: Only receives traffic when all primary servers are unavailable — ideal for DR sites or overflow capacity.
  • keepalive: Maintains persistent connections to backends, reducing TCP handshake overhead by up to 40% under load.

Create the Server Block with Proxy Pass

This minimal configuration forwards all requests to the upstream pool while preserving essential client metadata.

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

server {
    listen 443 ssl http2;
    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;

    location / {
        proxy_pass http://backend_app;
        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_http_version 1.1;
        proxy_set_header Connection "";
    }
}

The proxy_http_version 1.1 and empty Connection header are critical when using keepalive connections to upstreams. Without them, Nginx defaults to HTTP/1.0 and closes connections after each request, negating the performance benefit of the keepalive directive. Always test configuration syntax with sudo nginx -t before reloading.

How Do You Configure SSL Termination and Security Headers in Nginx?

SSL termination at the reverse proxy layer offloads cryptographic work from your application servers, simplifying certificate management and enabling centralized security policies. After you set up free SSL with Let's Encrypt and Certbot, harden the TLS configuration beyond Certbot's defaults.

Harden TLS Parameters for Modern Browsers

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

Disabling session tickets prevents forward secrecy compromise if server memory is dumped. OCSP stapling eliminates an extra round-trip to the CA during TLS handshake, improving Time-to-First-Byte by 50–100ms for users in regions with high latency to certificate authorities — a tangible win for audiences in Nepal connecting to global CAs.

Inject Mandatory Security Headers

Add these inside your HTTPS server block to mitigate XSS, clickjacking, and MIME-type sniffing attacks:

add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" always;

The always parameter ensures headers are added even on error responses (4xx, 5xx). Omitting it is a frequent oversight that leaves error pages unprotected. Validate your header configuration using browser dev tools or curl -I before deploying to production.

How Does Nginx Compare to Other Reverse Proxy Solutions in 2026?

Choosing the right reverse proxy depends on your operational context. While Nginx dominates traditional deployments, newer alternatives serve specific niches. Understanding these trade-offs prevents costly re-architecture later.

FeatureNginxCaddyTraefikHAProxy
Automatic HTTPSVia Certbot/scriptBuilt-in ACMEBuilt-in ACMEVia external tool
Configuration StyleDeclarative fileCaddyfile / JSON APIDynamic discoveryDeclarative file
Kubernetes IntegrationIngress ControllerIngress ControllerNative CRDsIngress Controller
Max Concurrent Connections~100K+ per worker~50K estimated~70K estimated~200K+ optimized
L7 Load Balancing AlgorithmsRound-robin, least_conn, ip_hashRound-robin, least_connRound-robin, WRR, dynamicAll + custom Lua
Memory Footprint (idle)~5–10 MB~30–50 MB~60–100 MB~10–20 MB
Best ForStatic + app proxy, cachingAuto-TLS, simple setupsCloud-native, K8s dynamicPure L4/L7 LB, HA

In practice, Nginx remains the default for teams needing fine-grained control over caching, header manipulation, and legacy protocol support. Caddy excels when automatic certificate renewal is the primary concern and configuration complexity must stay minimal. Traefik shines in Kubernetes environments where service discovery drives routing rules dynamically. HAProxy is unmatched for pure load-balancing throughput and advanced health-check logic but lacks built-in web-serving capabilities. For most full-stack applications requiring both static asset serving and API proxying, Nginx offers the best balance of performance, flexibility, and ecosystem maturity.

How Do You Optimize Nginx Reverse Proxy Performance Under High Load?

Default Nginx settings assume modest traffic. Production systems serving thousands of concurrent users require explicit tuning of buffers, timeouts, and connection pools. These optimizations apply whether you're running on a single VPS or behind an AWS ALB.

Incoming RequestTLS HandshakeRequest Bufferingclient_body_bufferclient_max_body_sizelarge_client_headerTimeout EnforcementCache Layerproxy_cache_pathCache Key LookupHIT → Serve DirectMISS → ForwardUpstream PoolKeepalive ConnsLoad BalancingHealth ChecksResponse BufferingResponseto Client
Nginx reverse proxy request processing pipeline: incoming requests pass through buffering, optional cache lookup, and upstream connection pooling before returning responses to clients.

Tune Buffer Sizes and Timeouts

# In http or server context
client_body_buffer_size 16k;
client_max_body_size 50m;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;

proxy_buffer_size 8k;
proxy_buffers 16 8k;
proxy_busy_buffers_size 16k;

proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 60s;

Undersized buffers cause Nginx to write temporary files to disk, destroying latency. Oversized buffers waste RAM under concurrency. The values above suit typical web APIs and Laravel/Node.js applications. For file-upload-heavy services, increase client_max_body_size and consider streaming uploads with proxy_request_buffering off to avoid memory spikes.

Enable Response Caching for Idempotent Endpoints

# In http context
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app_cache:10m max_size=1g inactive=60m use_temp_path=off;

# In location block
location /api/public/ {
    proxy_cache app_cache;
    proxy_cache_valid 200 10m;
    proxy_cache_valid 404 1m;
    proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
    add_header X-Cache-Status $upstream_cache_status;
    proxy_pass http://backend_app;
}

Caching at the reverse proxy layer shields backends from repetitive reads. The use_temp_path=off directive writes cache files directly to their final destination, avoiding an extra filesystem move operation. Monitor cache hit ratios via the X-Cache-Status header during staging validation. For deeper observability into cache performance and backend latency, integrate with your existing stack as described in the Prometheus and Grafana full monitoring stack guide.

How Do You Implement Health Checks and Failover in Nginx Upstreams?

Passive health checks (relying on failed requests) cause user-visible errors during failover. Active health checks probe backends continuously, removing unhealthy nodes before traffic reaches them. Note that active checks require Nginx Plus or the open-source nginx_upstream_check_module. For standard open-source Nginx, combine passive checks with robust error handling.

Configure Passive Health Checks with Fallback

upstream backend_app {
    server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:8081 max_fails=3 fail_timeout=30s;
    server 192.168.1.50:8080 backup;
}

server {
    location / {
        proxy_pass http://backend_app;
        proxy_next_upstream error timeout http_502 http_503 http_504;
        proxy_next_upstream_tries 2;
        proxy_next_upstream_timeout 10s;
    }
}

The max_fails=3 fail_timeout=30s pair marks a server unavailable after three failures within 30 seconds, then retries it after the timeout expires. proxy_next_upstream defines which response codes trigger failover to the next healthy backend. Setting proxy_next_upstream_tries 2 limits retry chains, preventing cascading delays when multiple backends degrade simultaneously.

Add Application-Level Health Endpoints

Expose a lightweight /health endpoint in your application that verifies database connectivity and critical dependencies. Configure Nginx to bypass caching and skip rate limiting for this path:

location = /health {
    proxy_pass http://backend_app;
    proxy_cache off;
    limit_req_status 200;
    access_log off;
    allow 10.0.0.0/8;
    allow 172.16.0.0/12;
    deny all;
}

Restricting access to internal CIDRs prevents external actors from probing your health endpoint. External load balancers (AWS ALB, GCP LB) should target this endpoint for their own health checks, creating a two-tier verification system that catches both network-level and application-level failures.

Without CacheWith Proxy CacheClient Req 1Backend HitLatency: 180msClient Req 2Backend HitLatency: 175msClient Req 1Backend HitLatency: 180msClient Req 2CACHE HITNo Backend CallLatency: 8msBackend Load: 100%Backend Load: ~40%
Performance comparison: Nginx reverse proxy with caching reduces repeat request latency from 175ms to 8ms and cuts backend load by over half for cacheable content.

Production Checklist for Your Nginx Reverse Proxy Deployment

Before marking your setup complete, verify these items against your live configuration. Skipping any one has caused incidents I've personally responded to at 3 AM.

  1. Syntax validation: Run sudo nginx -t after every change. Automate this in CI/CD pipelines that deploy config via Ansible or Terraform.
  2. Reload safety: Use sudo systemctl reload nginx instead of restart to preserve existing connections during config updates.
  3. Log rotation: Confirm /etc/logrotate.d/nginx exists and rotates access/error logs daily. Unrotated logs fill disks silently.
  4. Worker sizing: Set worker_processes auto; and worker_connections 1024; minimum. Monitor with stub_status module to detect connection saturation.
  5. Security audit: Scan headers with Mozilla Observatory or securityheaders.com. Fix any grade below B before going live.
  6. Backup config: Version-control all Nginx configs in Git. Tag releases matching deployment timestamps for instant rollback correlation.

A properly configured reverse proxy is invisible when working correctly and catastrophic when misconfigured. Treat your Nginx configuration with the same rigor as application code: review, test, version, and monitor it continuously.

Next Steps for Your Reverse Proxy Infrastructure

You now have a production-grade foundation to set up a reverse proxy with Nginx that handles SSL, load balancing, caching, and security enforcement. The next logical steps depend on your scale: implement structured logging for request tracing, add rate limiting to prevent abuse, or integrate with service mesh for east-west traffic control. If your team needs hands-on assistance architecting or auditing Nginx deployments across multi-cloud or compliance-regulated environments, reach out to discuss your infrastructure requirements.

Frequently Asked Questions

Run nginx -t to validate syntax before reloading. This prevents downtime from misconfigured proxy_pass directives or missing upstream blocks in your site configuration files.

Add proxy_pass http://localhost:3000 inside a location block. Include proxy_set_header Host $host and proxy_set_header X-Real-IP $remote_addr to preserve client headers correctly.

Yes. You must set Upgrade and Connection headers explicitly using proxy_set_header directives. Standard HTTP proxying will drop WebSocket handshakes without these specific configuration additions in 2026.

A trailing slash replaces the matched location prefix. Without it, Nginx appends the full original URI to the upstream address, often causing unexpected routing behavior in backend applications.

Configure ssl_certificate and ssl_certificate_key in the server block. Set proxy_pass to http://upstream while listening on port 443, handling encryption at the proxy layer only.

The upstream service is unreachable or crashed. Check backend logs, verify the proxy_pass port matches your application, and ensure SELinux or firewalls allow local connections.

Yes. Define an upstream block with multiple server directives. Use least_conn or ip_hash algorithms to distribute traffic evenly across healthy backend instances automatically.

Set proxy_set_header X-Real-IP $remote_addr and proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for. Configure your backend framework to trust these headers for accurate logging.

Adjust proxy_read_timeout and proxy_connect_timeout values. Default sixty seconds often fails for long-running API requests or slow backend responses in production environments.

Caddy automates TLS but lacks Nginx caching and granular control. Choose Nginx for high-traffic production workloads requiring custom buffering, rate limiting, or complex upstream configurations.

Enable proxy_cache_path and proxy_cache directives. Define cache keys using request variables and set valid expiration times to reduce backend load significantly.

No. Use systemctl reload nginx to apply changes gracefully. This spawns new worker processes with updated configs while existing connections complete without interruption.

Use allow and deny directives inside the location block. Place deny all last to create a whitelist, protecting admin panels or internal APIs from public exposure.

Increase client_max_body_size and adjust proxy_buffer_size plus proxy_buffers. Undersized buffers cause 413 errors or temporary file writing that degrades upload performance.

Enable debug logging and add add_header X-Debug-Upstream $upstream_addr temporarily. Inspect response headers with curl -v to verify correct header forwarding and upstream selection.