How to Configure Nginx as a Reverse Proxy: Complete Tutorial from Setup to Production (2026)

Nginx has cemented its position as the world’s most popular web server and reverse proxy, powering over 34% of all active websites as of early 2026 — though that dominance is not static: Netcraft data reported by CommandLinux showed Nginx’s usage share slipping 0.74 percentage points between June and July 2026, a reminder that market share keeps shifting even for the long-standing leader. Whether you are routing traffic to microservices, terminating SSL, or distributing load across a cluster of application servers, mastering the nginx reverse proxy is one of the most valuable skills a systems administrator or DevOps engineer can develop. In this thorough nginx tutorial, you will go from a fresh server to a hardened, production-ready reverse proxy configuration complete with SSL/TLS, load balancing, caching, rate limiting, and container integration.

This guide reflects the latest stable and mainline releases shipped through September 2026, including critical security patches and new features such as Multipath TCP support and HTTP/2 backend proxying. NGINX.org shipped 1.30.4 stable in July 2026, each patching three distinct CVEs, and has since pushed mainline further ahead with 1.31.5 landing on September 2, 2026, and per EOL.Wiki’s release tracking, 1.31 and 1.30 remain the only two actively supported reverse proxy branches following the 1.31.2 mainline build in June 2026. By the end of this tutorial you will have a fully working reverse proxy that can sit in front of any backend application, whether it runs on bare metal, virtual machines, or inside Docker containers. We will also cover monitoring, troubleshooting, and the most common pitfalls that trip up even experienced engineers. If you are evaluating your options, our Nginx vs Apache comparison provides a detailed side-by-side analysis to help you decide.

Every code block in this article is a complete, copy-paste-ready example tested on Ubuntu 24.04 LTS and Rocky Linux 9. Let us get started.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

Prerequisites and Environment Setup

Before diving into Nginx configuration files, make sure your environment meets the following requirements. This nginx tutorial assumes you have root or sudo access to a Linux server and a basic understanding of the command line. If you plan to follow along with the Docker sections, you will also need Docker Engine installed; our Docker tutorial walks you through that process step by step.

ComponentMinimum VersionRecommended VersionNotesRelease Date
Nginx (stable)1.26.x1.28.3Fixes CVE-2026-27654; production recommendedMarch 24, 2026
Nginx (mainline)1.27.x1.29.7Multipath TCP, default proxy HTTP/1.1 keep-aliveMarch 24, 2026
NGINX PlusR34R36 P2HTTP/3 crash fixes, advanced dashboardFebruary 4, 2026
OpenSSL1.1.13.3.xRequired for TLS 1.3 and modern ciphers2024-2026
Ubuntu / Debian22.04 LTS24.04 LTSLong-term support until 2029April 2024
Rocky Linux / RHEL8.x9.xSELinux support out of the box2022-2025
Docker Engine24.x27.xFor containerized proxy setups2025-2026
Certbot2.x3.xLet’s Encrypt ACME client2025-2026

Make sure your firewall allows inbound connections on ports 80 (HTTP) and 443 (HTTPS). If you are deploying to a cloud provider, our Terraform AWS tutorial shows how to automate security group rules as part of your infrastructure-as-code workflow. You should also have a registered domain name pointed at your server’s public IP address, or at minimum a DNS A record you can update. With these prerequisites in place, let us install Nginx.

Step 1: Installing Nginx on Ubuntu/Debian and RHEL/Rocky Linux

The first step in any nginx tutorial is getting the software installed. While most Linux distributions ship Nginx in their default repositories, those packages are often outdated. For production deployments you should use the official Nginx repository to get the latest stable release, which as of July 2026 is version 1.30.4, patching three distinct CVEs, according to NGINX.org’s release notes; on the mainline side, NGINX.org pushed the version further ahead to 1.31.5 by September 2, 2026, so if you are tracking mainline, confirm you are on that build or newer. Running an older build is not advisable.

Installing on Ubuntu and Debian

On Ubuntu 24.04 LTS or Debian 12, add the official Nginx signing key and repository, then install the package:

# Install prerequisites
sudo apt update
sudo apt install -y curl gnupg2 ca-certificates lsb-release ubuntu-keyring

# Import the official Nginx signing key
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor 
  | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null

# Add the stable repository
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

# Pin the official packages to take priority over distro packages
echo -e "Package: *nPin: origin nginx.orgnPin: release o=nginxnPin-Priority: 900n" 
  | sudo tee /etc/apt/preferences.d/99nginx

# Install Nginx
sudo apt update
sudo apt install -y nginx

# Verify installation
nginx -v
# Expected output: nginx version: nginx/1.28.3

# Enable and start the service
sudo systemctl enable nginx
sudo systemctl start nginx
sudo systemctl status nginx

Installing on RHEL and Rocky Linux

On RHEL 9 or Rocky Linux 9, the process is similar but uses dnf and a .repo file:

# Create the repository file
sudo tee /etc/yum.repos.d/nginx.repo <<'EOF'
[nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
EOF

# Install Nginx
sudo dnf install -y nginx

# Verify
nginx -v

# Enable and start
sudo systemctl enable nginx
sudo systemctl start nginx

# Open firewall ports
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

# If SELinux is enforcing, allow Nginx to connect to upstream backends
sudo setsebool -P httpd_can_network_connect 1

That last setsebool command is critical on SELinux-enabled systems. Without it, Nginx will return 502 Bad Gateway errors when it tries to proxy requests to a backend service. We will revisit this in the troubleshooting section. At this point you should be able to visit your server’s IP address in a browser and see the default Nginx welcome page. Next, let us look at how Nginx is structured.

Step 2: Understanding Nginx Architecture and Configuration Structure

Before writing your first nginx configuration file, it helps to understand the architecture. Nginx uses an event-driven, asynchronous model with a master process and multiple worker processes. The master process reads and validates the configuration, then spawns worker processes that handle actual client connections. Each worker can handle thousands of simultaneous connections using non-blocking I/O, which is why Nginx excels at high concurrency with low memory usage compared to process-per-connection servers like Apache in its default prefork mode. For a deeper dive into these architectural differences, see our Nginx vs Apache comparison.

The configuration file hierarchy on a standard installation from the official repository looks like this:

/etc/nginx/
├── nginx.conf              # Main configuration file
├── conf.d/                 # Server block files (*.conf auto-included)
│   └── default.conf        # Default server block
├── mime.types              # MIME type mappings
├── modules-enabled/        # Symlinks to active dynamic modules (Debian/Ubuntu)
├── fastcgi_params          # FastCGI parameter defaults
├── proxy_params            # Proxy parameter defaults (Debian/Ubuntu)
├── scgi_params             # SCGI parameter defaults
└── uwsgi_params            # uWSGI parameter defaults

The main nginx.conf file contains global directives organized into contexts. The outermost context is called the main context and includes directives like worker_processes and error_log. Inside that you find the events context (connection handling) and the http context (all HTTP traffic). Within http you define one or more server blocks, and within each server block you define location blocks that match URI patterns. This nesting is central to every server configuration you will ever write.

Nginx processes directives through inheritance: settings in the http context are inherited by server blocks unless overridden, and server settings flow into location blocks. This lets you set defaults once and customize only where needed. Always test before reloading:

# Test configuration for syntax errors
sudo nginx -t

# If the test passes, reload without downtime
sudo nginx -s reload

Never restart Nginx in production when a reload will do. The -s reload signal spawns new workers with the updated configuration while old workers finish serving current requests, resulting in zero downtime. A full restart drops all active connections immediately.

Step 3: Basic Nginx Configuration for Static Sites

Before jumping to reverse proxying, it is worth understanding a basic static site configuration, because many of the same directives carry over. Create a new server block file in /etc/nginx/conf.d/. The convention is one file per virtual host. Remove or rename the default configuration first to avoid conflicts:

# Remove the default server block
sudo mv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.bak

# Create a new server block
sudo tee /etc/nginx/conf.d/example.conf <<'EOF'
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    root /var/www/example.com/html;
    index index.html index.htm;

    # Logging
    access_log /var/log/nginx/example.com.access.log;
    error_log  /var/log/nginx/example.com.error.log;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    location / {
        try_files $uri $uri/ =404;
    }

    # Deny access to hidden files
    location ~ /. {
        deny all;
        access_log off;
        log_not_found off;
    }

    # Cache static assets
    location ~* .(css|js|jpg|jpeg|png|gif|ico|svg|woff2?)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}
EOF

# Create the document root and a test page
sudo mkdir -p /var/www/example.com/html
echo "<h1>Hello from Nginx</h1>" | sudo tee /var/www/example.com/html/index.html

# Test and reload
sudo nginx -t && sudo nginx -s reload

This configuration serves files from the document root. The try_files directive checks for the file, then a directory, then returns 404. Security headers block clickjacking and MIME sniffing. The static asset block sets 30-day browser caching with an immutable flag. These are all patterns you will reuse in your reverse proxy configuration.

Understanding static serving also matters because a well-configured Nginx proxy often serves static files directly while proxying dynamic requests to the backend. This hybrid approach dramatically reduces load on your application servers. We will demonstrate that pattern in the next section.

Step 4: Configuring Nginx as a Reverse Proxy

This is the core of the article. An nginx reverse proxy sits between clients and your backend application servers, forwarding requests and returning responses. It provides a single entry point, handles SSL termination, enables load balancing, and shields your backends from direct internet exposure. In 2026, with the rise of microservice architectures and API gateways, the reverse proxy pattern is more relevant than ever, and F5 has been pushing that pattern to the cluster edge: it announced an accelerated rollout of NGINX Gateway Fabric for Kubernetes on March 11, 2026, positioning it as the successor to Ingress-NGINX, and followed up on June 19, 2026 with confirmation that Gateway Fabric conforms to Gateway API 1.5, extending reverse-proxy-style control to AI traffic. If you are deploying microservices on Kubernetes, our Kubernetes and Helm tutorial covers ingress controllers that use Nginx under the hood.

The fundamental directive is proxy_pass, which tells Nginx where to send the request. Here is a complete, production-grade reverse proxy configuration for a Node.js application running on port 3000:

# /etc/nginx/conf.d/app.conf
# Production-grade Nginx reverse proxy configuration

upstream backend_app {
    server 127.0.0.1:3000;
    keepalive 64;
}

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

    # Redirect all HTTP to HTTPS
    return 301 https://$host$request_uri;
}

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

    # SSL configuration (covered in detail in the next section)
    ssl_certificate     /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
    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;

    # Security headers
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "0" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" always;

    # Logging
    access_log /var/log/nginx/app.example.com.access.log combined buffer=512k flush=1m;
    error_log  /var/log/nginx/app.example.com.error.log warn;

    # Request size limits
    client_max_body_size 50M;
    client_body_timeout  60s;
    client_header_timeout 60s;

    # Serve static files directly from Nginx
    location /static/ {
        alias /var/www/app/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Proxy all other requests to the backend
    location / {
        proxy_pass http://backend_app;

        # Preserve original request information
        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 X-Forwarded-Host  $host;
        proxy_set_header X-Forwarded-Port  $server_port;

        # Connection handling for upstream keepalive
        proxy_http_version 1.1;
        proxy_set_header Connection "";

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout    60s;
        proxy_read_timeout    60s;

        # Buffering
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 16k;
        proxy_busy_buffers_size 32k;

        # Error handling
        proxy_next_upstream error timeout http_502 http_503;
        proxy_next_upstream_timeout 10s;
        proxy_next_upstream_tries 3;
    }

    # WebSocket support (if needed)
    location /ws/ {
        proxy_pass http://backend_app;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }

    # Health check endpoint
    location /health {
        access_log off;
        return 200 "OKn";
        add_header Content-Type text/plain;
    }
}

Let us break down the key directives. The upstream block defines your backend server pool. Even with a single backend, using an upstream block is best practice because it enables keepalive connections and makes it trivial to add more servers later for nginx load balancing. The keepalive 64 directive maintains up to 64 idle connections to the backend, avoiding the overhead of establishing a new TCP connection for every proxied request.

The proxy_set_header directives are essential. Without X-Real-IP and X-Forwarded-For, your backend sees all requests as coming from 127.0.0.1, making logging and rate limiting useless. The X-Forwarded-Proto header tells the backend whether the original request was HTTP or HTTPS, critical for applications that generate absolute URLs or enforce HTTPS redirects.

Nginx made proxy HTTP/1.1 with keep-alive the default outright in 1.30.0 stable, released April 14, 2026 per NGINX.org — mainline had previewed the direction back in the 1.29.x series, with 1.29.0 shipping in June 2025 alongside new Early Hints (HTTP 103) support for reverse proxy backends — so these directives are technically only required if you are still running a pre-1.30 stable build. Including them explicitly is good defensive practice regardless of version.

Step 5: SSL/TLS Configuration with Let’s Encrypt

No production reverse proxy should serve traffic over plain HTTP. SSL/TLS termination at the reverse proxy layer is the standard approach, allowing your backend applications to communicate over unencrypted HTTP on the internal network while clients always connect over HTTPS. Configuring nginx ssl correctly is essential for both security and performance.

The easiest and most cost-effective way to obtain trusted SSL certificates is through Let’s Encrypt, which provides free, automated, 90-day certificates. Install Certbot and obtain your first certificate:

# Install Certbot with the Nginx plugin
# Ubuntu / Debian
sudo apt install -y certbot python3-certbot-nginx

# Rocky Linux / RHEL
sudo dnf install -y certbot python3-certbot-nginx

# Obtain a certificate (Nginx must be running and port 80 accessible)
sudo certbot --nginx -d app.example.com -d www.app.example.com 
  --non-interactive --agree-tos --email [email protected] 
  --redirect

# Verify automatic renewal
sudo certbot renew --dry-run

# Set up a systemd timer for auto-renewal (usually installed by default)
sudo systemctl enable --now certbot-renew.timer

# Check timer status
sudo systemctl list-timers | grep certbot

While Certbot’s --nginx plugin can automatically modify your Nginx server blocks, many experienced administrators prefer to manage SSL settings manually for full control. The Mozilla SSL Configuration Generator is an excellent resource for generating recommended cipher suites and protocol settings based on your target audience. For maximum security with broad compatibility, use the “Intermediate” profile.

Here are the critical nginx ssl directives. The ssl_protocols directive controls which TLS versions are accepted; in 2026, support TLS 1.2 and 1.3 only. The ssl_session_cache directive enables session resumption so returning clients skip the full handshake. Disable ssl_session_tickets to avoid key compromise risks. OCSP stapling is also strongly recommended, as it lets Nginx cache certificate revocation status and serve it directly to clients:

# Add these to your server block for optimal SSL configuration
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/app.example.com/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;

# Generate a strong Diffie-Hellman group (one-time setup)
# sudo openssl dhparam -out /etc/nginx/dhparam.pem 2048
ssl_dhparam /etc/nginx/dhparam.pem;

Nginx 1.28.0, released by NGINX.org in April 2025, also added OCSP support in the stream module for non-HTTP TLS termination alongside optimizations that reduced SSL-related CPU and memory usage in reverse proxy configurations. Be aware of CVE-2026-1642, an SSL upstream injection vulnerability fixed February 4, 2026, and note that the 1.30.x stable and 1.31.x mainline branches received further security patches in July 2026 (1.30.4 and 1.31.3 respectively). Ensure you are running at least one of those current releases to stay protected.

Step 6: Load Balancing with Nginx

One of the most powerful features of Nginx is its built-in nginx load balancing capability. By defining multiple servers in an upstream block, Nginx can distribute incoming requests across your backend fleet, improving both performance and availability. This is a natural extension of the reverse proxy pattern and requires minimal additional configuration.

Nginx supports several load balancing algorithms out of the box:

AlgorithmDirectiveBehaviorBest ForAvailability
Round Robin(default)Distributes requests evenly in orderHomogeneous backendsAll versions
Weighted Round Robinweight=NHigher-weight servers get more requestsHeterogeneous hardwareAll versions
Least Connectionsleast_connSends to the server with fewest active connectionsVariable request durationAll versions
IP Haship_hashSame client IP always hits the same serverSession persistence (stateful apps)All versions
Generic Hashhash $keyConsistent hashing on any variableCache distributionAll versions
Randomrandom two least_connPicks two servers at random, selects the one with fewer connectionsLarge server pools1.15.1+
Sticky Sessionssticky cookieRoutes based on a cookie set by NginxStateful applicationsNGINX Plus / 1.29.6+ (open source)

A notable recent development is that Nginx mainline 1.29.6 (released March 10, 2026) introduced sticky session support for upstream blocks in the open-source version, a feature that was previously exclusive to NGINX Plus — specifically NGINX Plus R35, which F5 shipped on August 13, 2025 on top of NGINX Open Source 1.29.0 and which had offered sticky cookie routing to commercial customers for months before the open-source branch caught up. This is a significant improvement for teams running stateful applications who do not want to pay for a commercial license.

Here is a complete nginx load balancing configuration with health checks, weighted distribution, and failover:

# /etc/nginx/conf.d/loadbalancer.conf
# Load-balanced reverse proxy with health checks

upstream app_cluster {
    # Least connections algorithm with weights
    least_conn;

    # Primary application servers
    server 10.0.1.10:8080 weight=5 max_fails=3 fail_timeout=30s;
    server 10.0.1.11:8080 weight=5 max_fails=3 fail_timeout=30s;
    server 10.0.1.12:8080 weight=3 max_fails=3 fail_timeout=30s;

    # Backup server (only receives traffic when all primaries are down)
    server 10.0.1.20:8080 backup;

    # Keep-alive connections to backends
    keepalive 128;
    keepalive_requests 1000;
    keepalive_timeout 60s;
}

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;

    location / {
        proxy_pass http://app_cluster;

        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;

        # Keep-alive to upstream
        proxy_http_version 1.1;
        proxy_set_header Connection "";

        # Failover: retry on another server if the current one fails
        proxy_next_upstream error timeout http_502 http_503 http_504;
        proxy_next_upstream_timeout 10s;
        proxy_next_upstream_tries 3;
    }

    # Status page for monitoring (restrict to internal IPs)
    location /nginx_status {
        stub_status on;
        allow 10.0.0.0/8;
        allow 127.0.0.1;
        deny all;
    }
}

The max_fails and fail_timeout directives provide passive health checking. If a server fails three times within 30 seconds, Nginx marks it unavailable for that window, then sends a test request after the timeout expires. For active health checks, you need NGINX Plus or a third-party module. The backup directive designates a server that only receives traffic when all primaries are down.

If you are running your backend applications in containers, nginx load balancing pairs naturally with service discovery. Our Docker Compose tutorial covers how to scale services and use Nginx as a load balancer in a multi-container environment.

Step 7: Rate Limiting and Security Headers

A well-configured reverse proxy does more than just forward traffic. It is your first line of defense against abuse, DDoS attempts, and brute-force attacks. Rate limiting controls how many requests a client can make within a given time window, while security headers instruct browsers on how to handle your content securely.

Nginx implements rate limiting using the leaky bucket algorithm through the limit_req module. You define a shared memory zone that tracks request rates per key (usually the client IP), then apply limits at the server or location level. Nginx 1.28.0 stable, released April 23, 2025 on top of the 1.27.x branch, introduced support for proxy_*limit_rate variables in rate-limiting zones, per NGINX.org’s release notes, giving you much more flexibility in defining dynamic rate limits.

# Add to the http context in /etc/nginx/nginx.conf or a shared include file

# Define rate limiting zones
# General: 10 requests per second per IP
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;

# API: 30 requests per second per IP
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;

# Login: 5 requests per minute per IP (brute-force protection)
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

# Connection limiting
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;

# --- In the server block ---

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

    # Global connection limit: max 50 simultaneous connections per IP
    limit_conn conn_per_ip 50;
    limit_conn_status 429;

    # General rate limit with burst allowance
    location / {
        limit_req zone=general burst=20 nodelay;
        limit_req_status 429;

        proxy_pass http://backend_app;
        # ... proxy headers ...
    }

    # Stricter rate limit on API endpoints
    location /api/ {
        limit_req zone=api burst=50 nodelay;
        limit_req_status 429;

        proxy_pass http://backend_app;
        # ... proxy headers ...
    }

    # Very strict rate limit on authentication endpoints
    location /auth/login {
        limit_req zone=login burst=10 nodelay;
        limit_req_status 429;

        proxy_pass http://backend_app;
        # ... proxy headers ...
    }

    # Custom error page for rate-limited requests
    error_page 429 /429.html;
    location = /429.html {
        internal;
        default_type text/html;
        return 429 '<html><body><h1>429 Too Many Requests</h1><p>Please slow down and try again later.</p></body></html>';
    }
}

The burst parameter allows temporary rate limit exceedance. With burst=20 and rate=10r/s, Nginx queues up to 20 extra requests beyond the limit. The nodelay flag processes burst requests immediately rather than queuing them, which is usually what you want for user-facing applications.

For security headers, the following set provides strong protection against common web vulnerabilities. Apply them in your server block or, better yet, in a shared include file so every virtual host benefits:

  • Strict-Transport-Security (HSTS): Forces browsers to use HTTPS for all future requests. The max-age=63072000 value (two years) tells browsers to remember this policy for a long time. Include the preload directive if you plan to submit your domain to the HSTS preload list.
  • X-Content-Type-Options: Set to nosniff to prevent browsers from MIME-type sniffing, which can lead to XSS attacks via files served with incorrect content types.
  • X-Frame-Options: Set to SAMEORIGIN to prevent your site from being embedded in iframes on other domains, blocking clickjacking attacks.
  • Content-Security-Policy: The most powerful security header. Define exactly which sources of content are allowed for scripts, styles, images, and other resource types. Start restrictive and loosen as needed.
  • Permissions-Policy: Controls which browser features (camera, microphone, geolocation) your site can use. Deny everything you do not need.

Test your security headers after deploying using free online tools like securityheaders.com. Aim for an A+ rating. If you are automating your deployments, our GitHub Actions CI/CD tutorial shows how to integrate header scanning into your pipeline.

Step 8: Caching and Performance Optimization

Caching at the reverse proxy layer can dramatically reduce load on your backend servers and improve response times for end users. Nginx can cache responses from upstream servers and serve them directly for subsequent identical requests, completely bypassing the backend. This is especially effective for API responses that do not change frequently, rendered HTML pages, and any content that is expensive to generate.

To enable proxy caching, define a cache zone in the http context and then activate it in relevant location blocks:

# In the http context of nginx.conf
proxy_cache_path /var/cache/nginx/proxy
    levels=1:2
    keys_zone=proxy_cache:10m
    max_size=10g
    inactive=60m
    use_temp_path=off;

# In your server block
server {
    listen 443 ssl http2;
    server_name app.example.com;

    # Enable caching for cacheable content
    location / {
        proxy_pass http://backend_app;

        # Cache configuration
        proxy_cache proxy_cache;
        proxy_cache_valid 200 301 302 10m;
        proxy_cache_valid 404 1m;
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;
        proxy_cache_lock on;
        proxy_cache_lock_timeout 5s;

        # Cache key
        proxy_cache_key "$scheme$request_method$host$request_uri";

        # Add cache status header for debugging
        add_header X-Cache-Status $upstream_cache_status always;

        # Bypass cache for authenticated users
        proxy_cache_bypass $cookie_session $http_authorization;
        proxy_no_cache $cookie_session $http_authorization;

        # Standard proxy headers
        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 "";
    }

    # Never cache POST requests or admin areas
    location /admin/ {
        proxy_pass http://backend_app;
        proxy_cache off;
        # ... proxy headers ...
    }
}

The proxy_cache_use_stale directive is particularly valuable: it serves cached content when the backend is down. Combined with proxy_cache_background_update, Nginx returns the stale response immediately while fetching a fresh copy in the background. Even if your backend crashes, users continue to see content.

Beyond caching, several other configuration tweaks improve performance. Enable gzip compression for text-based responses, tune buffer sizes for your workload, and enable sendfile for static content:

  • gzip on: Enable compression. Add gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript; to compress common text formats.
  • gzip_min_length 256: Do not compress tiny responses where the overhead exceeds the savings.
  • gzip_vary on: Add a Vary: Accept-Encoding header so caches know that compressed and uncompressed versions are different.
  • sendfile on and tcp_nopush on: Use the kernel’s sendfile system call for static files and optimize TCP packet construction.
  • worker_connections 4096: Increase from the default 1024 if you expect high concurrency. Each worker can handle this many simultaneous connections.
  • multi_accept on: Allow each worker to accept multiple connections at once instead of one at a time.

Nginx 1.28.0 also brought QUIC performance improvements, which benefit HTTP/3 connections. If you are serving content to modern browsers and want the lowest possible latency, consider enabling HTTP/3 alongside HTTP/2 in your listener directives. For applications that interact with cloud services, refer to our cloud computing guide for best practices on optimizing network paths between your proxy and your backends.

Step 9: Docker and Nginx as a Containerized Reverse Proxy

Running Nginx inside a Docker container is an increasingly common pattern, especially in development and staging environments. It provides isolation, reproducibility, and easy integration with Docker networking. In a containerized setup, Nginx sits in the same Docker network as your backend services and proxies traffic to them using their container names as hostnames. Our Docker tutorial covers the fundamentals of containerization if you are new to the concept.

Here is a complete Docker Compose setup with Nginx as a reverse proxy in front of two backend services. This example is adapted from patterns covered in our Docker Compose tutorial:

# docker-compose.yml
version: "3.9"

services:
  nginx:
    image: nginx:1.28.3
    container_name: nginx-proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certbot/conf:/etc/letsencrypt:ro
      - ./certbot/www:/var/www/certbot:ro
    depends_on:
      - api
      - frontend
    networks:
      - webnet
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  api:
    image: your-api:latest
    container_name: api-service
    expose:
      - "3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://db:5432/app
    networks:
      - webnet
      - backend
    restart: unless-stopped

  frontend:
    image: your-frontend:latest
    container_name: frontend-service
    expose:
      - "8080"
    networks:
      - webnet
    restart: unless-stopped

  db:
    image: postgres:16
    container_name: postgres-db
    environment:
      - POSTGRES_DB=app
      - POSTGRES_USER=appuser
      - POSTGRES_PASSWORD_FILE=/run/secrets/db_password
    volumes:
      - pgdata:/var/lib/postgresql/data
    networks:
      - backend
    secrets:
      - db_password
    restart: unless-stopped

networks:
  webnet:
    driver: bridge
  backend:
    driver: bridge
    internal: true

volumes:
  pgdata:

secrets:
  db_password:
    file: ./secrets/db_password.txt

The corresponding config file that you mount into the container uses Docker service names as upstream hostnames:

# nginx/conf.d/app.conf (mounted into the container)
upstream api_service {
    server api:3000;
    keepalive 32;
}

upstream frontend_service {
    server frontend:8080;
    keepalive 32;
}

server {
    listen 80;
    server_name app.example.com;
    return 301 https://$host$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;

    # API routes
    location /api/ {
        proxy_pass http://api_service;
        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 "";
    }

    # Frontend routes
    location / {
        proxy_pass http://frontend_service;
        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 "";
    }

    location /health {
        access_log off;
        return 200 "OKn";
        add_header Content-Type text/plain;
    }
}

The upstream blocks reference services by their Docker Compose names (api and frontend). Docker’s built-in DNS resolves these to container IPs. The backend network is marked internal: true, meaning the database is only reachable from the API service, never from the internet or the Nginx container.

Nginx 1.28.0 added automatic upstream hostname re-resolution, which is especially useful in containerized environments where container IPs change after restarts. Previously, Nginx cached DNS resolution at startup and never updated it. The new behavior periodically re-resolves upstream hostnames, keeping routing correct in dynamic environments.

Step 10: Monitoring and Logging Configuration

Visibility into your Nginx proxy layer is essential for diagnosing issues, tracking performance, and planning capacity. Nginx provides several mechanisms for monitoring and logging that you should configure from day one.

The built-in stub_status module exposes basic connection metrics. Enable it on a restricted endpoint as shown earlier in the load balancing section. For richer metrics, consider the ngx_http_api_module available in NGINX Plus R36 — which F5 shipped on December 1, 2025 on top of open-source 1.29.3 with HTTP CONNECT forward-proxy support, and which is covered by security updates only through May 13, 2026 and reaches full end of support on September 2, 2026, a roughly nine-month security window worth tracking if you are on a support contract. F5 has since restructured the product line: as of May 2026, NGINX Plus splits into separate LTS and Continuous Release tracks, with NGINX Plus 37.0 — which also introduced HTTP/2 upstream proxying — debuting as the first LTS release and carrying three years of support, per NGINX’s own documentation. The current LTS build, PLS.37.0.6.2 as of September 15, 2026, is based on NGINX 1.29.8, so confirm you are tracking that version if long-term stability matters more than chasing every Continuous Release. Otherwise, use open-source exporters like the Nginx Prometheus exporter to feed metrics into your monitoring stack. If you manage a large reverse-proxy fleet, NGINX Instance Manager 2.23.0, also released September 2, 2026, added support for Ubuntu 26.04 LTS, easing centralized visibility across mixed-OS deployments.

For logging, Nginx’s default combined log format is a good start but lacks upstream timing information. Define a custom log format that includes proxy-specific variables:

# In the http context of nginx.conf
log_format proxy_detailed
    '$remote_addr - $remote_user [$time_local] '
    '"$request" $status $body_bytes_sent '
    '"$http_referer" "$http_user_agent" '
    'rt=$request_time '
    'urt=$upstream_response_time '
    'uct=$upstream_connect_time '
    'uht=$upstream_header_time '
    'us=$upstream_status '
    'ua=$upstream_addr '
    'cs=$upstream_cache_status';

# Use the custom format in server blocks
access_log /var/log/nginx/app.access.log proxy_detailed buffer=512k flush=1m;

# Conditional logging: skip health check noise
map $request_uri $loggable {
    ~*^/health   0;
    ~*^/favicon  0;
    default      1;
}

access_log /var/log/nginx/app.access.log proxy_detailed if=$loggable;

The key timing variables to monitor are $request_time (total time Nginx spent processing the request), $upstream_response_time (time waiting for the backend to respond), and $upstream_connect_time (time to establish a connection to the backend). If $request_time is high but $upstream_response_time is low, the bottleneck is in Nginx itself (possibly buffer sizes or client connection speed). If $upstream_response_time is high, the problem is in your backend application.

Log rotation is handled automatically by logrotate on most distributions. Verify the config at /etc/logrotate.d/nginx sends a USR1 signal to reopen log files after rotation. For centralized logging, use a log shipper like Filebeat or Fluent Bit to forward logs to Elasticsearch or a similar platform.

Set error_log to warn in production. The debug level generates enormous output and can fill disks rapidly. Only enable it temporarily when investigating a specific issue, and note that debug logging requires the --with-debug compile flag.

Common Pitfalls and How to Avoid Them

After years of configuring Nginx in production environments, certain mistakes appear again and again. Here are the most common pitfalls and how to avoid them:

Pitfall 1: Forgetting to pass the Host header. When you configure proxy_pass without setting proxy_set_header Host $host, Nginx sends the upstream server name (from the upstream block or the proxy_pass URL) as the Host header. This breaks virtual hosting on the backend, causes incorrect URL generation, and confuses applications that check the Host header for security purposes. Always set proxy_set_header Host $host; in every proxied location block.

Pitfall 2: Trailing slash mismatch in proxy_pass. There is a subtle but critical difference between proxy_pass http://backend and proxy_pass http://backend/. With the trailing slash, Nginx strips the matched location prefix before forwarding. Without it, the full original URI is passed. For example, with location /api/ and proxy_pass http://backend/, a request to /api/users is forwarded as /users. Without the trailing slash, it is forwarded as /api/users. Misunderstanding this causes 404 errors that are maddening to debug.

Pitfall 3: Not enabling keepalive connections to upstreams. Without a keepalive directive in your upstream block, Nginx opens a new TCP connection for every single proxied request. Under high load this creates thousands of short-lived connections, wasting resources and adding latency. Always add keepalive 64 (or more) and set proxy_http_version 1.1 with proxy_set_header Connection "" to enable persistent connections.

Pitfall 4: Using if directives incorrectly inside location blocks. The Nginx if directive does not work like a programming language conditional. Inside a location block, if creates an implicit nested location and can cause unexpected behavior. The official Nginx documentation warns that if is evil inside location blocks. Use map and try_files as alternatives.

Pitfall 5: Ignoring SELinux on RHEL-based systems. On Rocky Linux, RHEL, and AlmaLinux, SELinux is enabled and enforcing by default. Nginx will be denied network connections to backend servers unless you run setsebool -P httpd_can_network_connect 1. The symptom is a 502 Bad Gateway error with Permission denied in the error log. Do not disable SELinux; just set the proper boolean.

Pitfall 6: Running Nginx with too few worker processes. Set worker_processes to auto, which matches the number of CPU cores. Setting it to 1 on a multi-core server leaves capacity unused. Setting it higher than the core count wastes memory due to context switching.

Pitfall 7: Not testing configuration before reload. Always run nginx -t before nginx -s reload. A syntax error will cause the reload to fail silently or crash the master process. Integrate nginx -t into your CI/CD pipeline as covered in our GitHub Actions CI/CD tutorial.

Troubleshooting Guide

When something goes wrong with your nginx configuration, systematic troubleshooting saves hours of guesswork. The following table covers the most common issues you will encounter when running a reverse proxy and how to resolve them:

SymptomLikely CauseDiagnostic CommandSolutionNotes
502 Bad GatewayBackend not running or unreachablecurl -v http://127.0.0.1:3000Start the backend service; check upstream address and portMost common Nginx error
502 with “Permission denied” in logSELinux blocking network connectionsausearch -m avc -ts recentsetsebool -P httpd_can_network_connect 1RHEL/Rocky Linux specific
504 Gateway TimeoutBackend too slow to respondCheck $upstream_response_time in access logIncrease proxy_read_timeout; optimize backendDefault timeout is 60s
413 Request Entity Too LargeRequest body exceeds limitCheck client_max_body_size valueIncrease client_max_body_size in server or location blockDefault is 1MB
301 Redirect LoopBoth Nginx and backend forcing HTTPSCheck response headers with curl -IDisable redirect in backend; let Nginx handle SSL terminationPass X-Forwarded-Proto header
Real client IP not visible in backendMissing proxy headersLog $http_x_forwarded_for in backendAdd proxy_set_header X-Real-IP $remote_addrAlso configure backend to trust the header
WebSocket connections droppingMissing Upgrade/Connection headersCheck browser dev tools Network tabAdd proxy_set_header Upgrade $http_upgrade and Connection "upgrade"Increase proxy_read_timeout for WS
SSL handshake failureCertificate chain incomplete or expiredopenssl s_client -connect host:443Use fullchain.pem not cert.pem; renew certificateCheck expiry with certbot certificates
“no resolver defined” errorDynamic upstream names without resolverCheck error_log for the full messageAdd resolver 127.0.0.53 valid=30s; in http contextCommon in Docker setups
Nginx serves default page instead of vhostserver_name mismatch or no SNIcurl -H "Host: app.example.com" http://IPVerify server_name matches the requested domain exactlyCheck DNS resolution

The single most valuable troubleshooting tool is the error log. Always check /var/log/nginx/error.log first when something breaks. For more detail, temporarily increase the log level to info or debug for a specific server block. You can also use curl -v from the Nginx server itself to test connectivity to your backends, which isolates whether the problem is in Nginx or upstream.

If you are debugging SSL issues, the openssl s_client command is indispensable. Run openssl s_client -connect your-domain.com:443 -servername your-domain.com to see the full certificate chain, protocol version, and cipher suite. Mismatched server names, expired intermediate certificates, and missing chain certificates are the most common causes of nginx ssl handshake failures.

Advanced Tips for Production Deployments

Once your Nginx proxy is running, the following advanced techniques will help you get the most out of it in production:

Tip 1: Use HTTP/2 to backends. Support for proxying with HTTP/2 to backend servers, via the proxy_http_version 2.0 directive, first appeared in mainline with 1.29.4 on December 9, 2025 and landed in the stable branch with 1.30.0 on April 14, 2026, per NGINX.org, specifically to improve reverse-proxy upstream performance. That same 1.29.4 mainline release also added Encrypted ClientHello support, which further hardens TLS proxy performance by hiding the SNI hostname from on-path observers during the handshake. This enables multiplexing on the backend connection, which is particularly beneficial when your backend serves many small responses or when you are proxying to gRPC services. As of mid-2026 you can rely on this feature in production on either branch.

Tip 2: Use Early Hints (103 responses). Nginx 1.29.0 mainline, which shipped on June 24, 2025 according to blog.nginx.org, added support for Early Hints, allowing the proxy to send 103 status responses to the client before the full response is ready. This lets browsers preload critical resources like CSS and JavaScript while waiting for the backend to generate the page, significantly improving perceived load times.

Tip 3: Use the map directive for complex routing. Instead of using nested if blocks, use map to create variables based on request attributes. This is more efficient, less error-prone, and easier to read. You can map based on URI patterns, headers, cookies, or any other request variable to control caching, rate limiting, or backend selection.

Tip 4: Implement graceful deployments with upstream health. During rolling deployments, temporarily mark the server being updated as down in the upstream block and reload. Nginx will stop sending new requests to that server while existing requests complete naturally. After the deployment, remove the down flag and reload again. For automated rolling deployments, our Kubernetes and Helm tutorial covers how Kubernetes handles this automatically.

Tip 5: Use njs for advanced logic. The njs scripting module (version 0.9.6 as of March 2026) brings JavaScript to your Nginx config files. The latest release supports optional chaining and nullish coalescing operators, making complex request processing much more readable. Use njs for tasks like JWT validation, request transformation, or dynamic routing that would be awkward with pure Nginx directives.

Tip 6: Enable Multipath TCP for resilience. Multipath TCP (MPTCP) support reached the Nginx stable branch in 1.30.0 on April 14, 2026, per NGINX.org, after debuting in mainline; it lets connections use multiple network paths simultaneously to enhance reverse-proxy resilience. This improves resilience on servers with multiple network interfaces and can improve throughput. Enable it at the OS level and Nginx will use it automatically for client connections.

Tip 7: Tune worker_rlimit_nofile. Each connection requires a file descriptor. If you expect thousands of concurrent connections, increase worker_rlimit_nofile to at least twice your worker_connections value. Also update the system-level limits in /etc/security/limits.conf and the systemd unit file.

Tip 8: Use proxy_pass_trailers for HTTP trailers. Added in Nginx 1.28.0, the proxy_pass_trailers directive passes HTTP trailer headers from the backend response to the client. This is useful for gRPC and other protocols that use trailers to convey status information after the response body.

For thorough infrastructure automation that includes Nginx provisioning, SSL certificate management, and DNS configuration as code, explore our Terraform AWS tutorial which demonstrates how to automate the entire stack from the ground up.

Related Coverage

This nginx tutorial is part of our broader infrastructure and DevOps coverage. Here are related articles that complement what you have learned:

Frequently Asked Questions

What is the difference between a forward proxy and a reverse proxy?

A forward proxy acts on behalf of clients to access external resources (think corporate web filters or VPNs). A reverse proxy acts on behalf of backend servers to handle incoming requests. An nginx reverse proxy accepts requests from the internet and forwards them to internal application servers. The client never communicates directly with the backend, which provides security, load distribution, SSL termination, and caching benefits.

Which Nginx version should I use in production: stable or mainline?

For most production deployments, use the stable branch (currently 1.28.3). It receives only critical bug fixes and security patches, minimizing the risk of regressions. The mainline branch (1.29.7) receives new features and improvements more frequently, which means it also carries a higher risk of unexpected behavior changes. Use mainline only if you need a specific feature that has not been backported to stable, such as HTTP/2 backend proxying or Multipath TCP support. The official Nginx project recommends mainline for testing and development environments. If you are running the commercial product instead, note that F5 restructured NGINX Plus in May 2026 into separate LTS and Continuous Release tracks — the LTS track, which carries three years of support, is the closer analogue to open-source stable, while Continuous Release mirrors mainline’s faster cadence.

How do I handle WebSocket connections through an Nginx reverse proxy?

WebSocket connections require the HTTP Upgrade mechanism. You must explicitly pass the Upgrade and Connection headers to the backend. Create a dedicated location block for your WebSocket endpoint with proxy_set_header Upgrade $http_upgrade; and proxy_set_header Connection "upgrade";. Also increase proxy_read_timeout to a high value (86400 seconds for 24 hours) to prevent Nginx from closing idle WebSocket connections. A complete example is provided in the Step 4 configuration above.

Can Nginx handle SSL termination for multiple domains on a single IP address?

Yes. Nginx uses Server Name Indication (SNI), a TLS extension that lets the client specify the hostname during the SSL handshake. This enables Nginx to select the correct certificate per domain even on a shared IP. Define separate server blocks for each domain with their own ssl_certificate and ssl_certificate_key directives. SNI is supported by all modern browsers and is the standard approach in 2026.

How do I configure Nginx for zero-downtime deployments?

Nginx itself reloads with zero downtime using nginx -s reload, which gracefully transitions to the new configuration. For backend deployments, mark the server being updated as down in the upstream block and reload. Deploy the update, remove the down flag, then reload again. Repeat for each server. In Kubernetes environments, rolling update strategies handle this automatically. Our Kubernetes and Helm tutorial covers this pattern in detail.

What are the performance limits of Nginx as a reverse proxy?

A single Nginx instance on modern hardware can handle over 100,000 concurrent connections and tens of thousands of requests per second. Actual limits depend on hardware, configuration complexity, and traffic patterns. SSL termination is the most CPU-intensive operation; with AES-NI hardware acceleration, expect 10,000-20,000 new TLS handshakes per second per core. For cached responses, throughput is typically network-bound rather than CPU-bound.

How do I secure my Nginx reverse proxy against common attacks?

Start with the security headers from Step 7 (HSTS, CSP, X-Content-Type-Options). Enable rate limiting against brute-force attacks. Restrict admin endpoints by IP with allow/deny. Keep Nginx updated to patch vulnerabilities like CVE-2026-27654 and CVE-2026-1642. Hide the version with server_tokens off and set client_max_body_size to prevent oversized requests. For WAF-level protection, integrate ModSecurity, or step up to F5 WAF for NGINX 5.15 — released September 2, 2026 with support for NGINX Plus R37.1 — or place another cloud WAF in front of your nginx ssl endpoint.

Should I use Nginx or a cloud load balancer like AWS ALB?

They are not mutually exclusive. Cloud load balancers like AWS ALB excel at cross-zone distribution and integration with auto-scaling. Nginx excels at application-level routing, caching, and request transformation. A common architecture uses a cloud load balancer for zone distribution and Nginx on each server for local proxying and static file serving. Our cloud computing guide covers multi-layer architecture design.

Marcus Chen

Marcus Chen

Gaming & Consumer Tech Editor

Marcus Chen is a senior editor at Tech Insider, where he leads coverage of the US online gaming market, including sweepstakes and social casinos, alongside consumer technology. He evaluates operators on their published terms, licensing and RNG certifications, stated redemption policies, and corroborating independent reporting, and writes plainly about what the evidence supports. Tech Insider does not run first-party money tests and does not gamble with reader funds. Marcus has reported on the technology and online-gaming industries for more than a decade.

View all articles