Lesson 24 / 25

Case Study: A Complete Production Front End

Combine everything into one production-ready configuration.

One configuration, many responsibilities

A product has a React single-page app, a REST API on three instances, a WebSocket service for live updates and an admin panel. The NGINX design: a catch-all default server returning 444; an HTTP server that answers ACME challenges and redirects everything else to https://www.example.com; an HTTPS server with TLS 1.2/1.3, HTTP/2 and HTTP/3, HSTS and a shared security-headers snippet; /assets/ served from disk with gzip_static and year-long immutable caching; / with try_files ... /index.html and no-cache; /api/ proxied to a least_conn upstream with keepalive, forwarded headers, a 1-second connect timeout, a retry on connection errors only, per-IP rate limiting with 429 responses and a 1-second micro-cache for public product listings; /ws/ with WebSocket upgrade headers and a one-hour read timeout; /admin/ restricted to the office VPN range plus SSO through auth_request; JSON access logs with request IDs forwarded to the API; and stub_status on localhost for metrics. Changes go through Git, CI runs nginx -t in a container, and deployment performs a graceful reload.

The API part of the configuration

Rate limiting, micro-caching, keepalive and forwarded headers together.

upstream api_backend {
    least_conn;
    server 10.0.1.11:8080 max_fails=3 fail_timeout=15s;
    server 10.0.1.12:8080 max_fails=3 fail_timeout=15s;
    server 10.0.1.13:8080 max_fails=3 fail_timeout=15s;
    keepalive 64;
}

limit_req_zone $binary_remote_addr zone=api:20m rate=20r/s;
proxy_cache_path /var/cache/nginx/api keys_zone=api_cache:50m max_size=1g inactive=10m;

server {
    # ... TLS, HTTP/2, HTTP/3, security headers as shown earlier ...
    location /api/ {
        limit_req zone=api burst=40 nodelay;
        limit_req_status 429;

        proxy_pass http://api_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Request-ID $request_id;
        proxy_connect_timeout 1s;
        proxy_read_timeout 15s;
        proxy_next_upstream error timeout;

        proxy_cache api_cache;
        proxy_cache_valid 200 1s;
        proxy_cache_use_stale error timeout updating http_502 http_503;
        proxy_cache_lock on;
        proxy_no_cache $http_authorization;      # never cache authenticated calls
        proxy_cache_bypass $http_authorization;
    }
}

Keep configuration in Git and test it in CI

Run docker run --rm -v $PWD/nginx:/etc/nginx:ro nginx:stable nginx -t in CI so syntax errors never reach production, and review configuration changes like code.

Quick check: In the case study, why are requests with an Authorization header excluded from the micro-cache?

  • Authenticated responses may be user-specific, so caching them could leak data between users
  • Authorization headers are too large to cache
  • NGINX cannot read headers
  • Caching would disable rate limiting
Answer

Authenticated responses may be user-specific, so caching them could leak data between users — User-specific responses must not be stored in a shared cache keyed only by URL.