← DevOps← DevOps
DevOpsDevOps18 Th7, 2026Jul 18, 202616 phút đọc14 min read

Web Server, Proxy & Load BalancerWeb Servers, Proxies & Load Balancers

Thuộc bộ kiến thức DevOps Roadmap.

Tổng quan

Giữa internet và code ứng dụng của bạn là một lớp gồm web server, proxy và load balancer. Lớp này đảm nhiệm TLS termination, phục vụ file tĩnh, nén và cache response, định tuyến request đến đúng backend, hấp thụ các đợt tăng traffic đột biến, và che chắn ứng dụng khỏi traffic dị dạng hoặc độc hại. Gần như mọi HTTP request trên production đều đi qua ít nhất một trong các thành phần này — thường là nhiều lớp nối tiếp (CDN → cloud load balancer → Nginx ingress → app).

Với DevOps engineer, đây là công việc hằng ngày: viết config Nginx, cấu hình TLS termination, tinh chỉnh cache header, định nghĩa health check, và lựa chọn giữa L4 và L7 load balancing. Đây cũng là nơi đầu tiên cần nhìn vào khi có sự cố — lỗi 502/504, connection reset và lỗi TLS đều lộ ra ở lớp này, và một proxy có log thời gian upstream biến “site chậm” thành “backend số 3 chậm”.

Các khái niệm ở đây có tính chuyển đổi cao. Dù công cụ cụ thể là Nginx, HAProxy, Envoy, Traefik, AWS ALB hay Kubernetes ingress controller, những ý tưởng cốt lõi đều giống nhau: reverse proxy, upstream pool, thuật toán cân bằng tải, health check, xử lý kết nối và caching. Học mô hình một lần thì mọi công cụ chỉ còn là chuyện cú pháp.

Kiến thức nền tảng

Web server thực sự làm gì

Về cốt lõi, một web server (a) nhận kết nối TCP, (b) parse HTTP request, và (c) tạo response — từ file tĩnh trên đĩa hoặc bằng cách ủy thác cho code ứng dụng. Cơ chế ủy thác rất quan trọng:

Trục còn lại là mô hình concurrency, quyết định server xử lý được bao nhiêu kết nối khi tải cao.

So sánh các web server

NginxApache HTTP ServerCaddyIIS
Kiến trúcEvent-driven, worker bất đồng bộ (ít process, mỗi cái nhiều kết nối)Process/thread cho mỗi kết nối (MPM event cải thiện)Event-driven (Go, goroutine)Tích hợp Windows, I/O bất đồng bộ
Concurrency khi tải caoXuất sắc, ít bộ nhớTốt với MPM event; nặng hơn với preforkRất tốtTốt trên Windows
Cấu hìnhCác block trong nginx.conf (khai báo)Directive + .htaccess theo thư mục (ghi đè lúc chạy)Caddyfile (tối giản) hoặc JSON APIGUI / web.config
HTTPS tự độngKhông (dùng certbot / acme.sh)Không (dùng certbot / mod_md) — ACME tích hợp, bật mặc địnhQua ACME client cho Windows
Vai trò điển hìnhReverse proxy, file tĩnh, LB, K8s ingressHosting truyền thống, app phụ thuộc .htaccess (PHP shared hosting)Service vừa và nhỏ muốn TLS không cần cấu hìnhStack Windows/.NET
Nội dung độngQua upstream (proxy/FastCGI)Module nhúng (mod_php) hoặc proxyQua reverse proxyASP.NET in-process

Nginx thống trị mảng reverse proxy/ingress nhờ tốn ít bộ nhớ và chịu được concurrency cao; Apache vẫn phổ biến ở nơi cần sự linh hoạt theo thư mục của .htaccess (shared hosting, PHP cũ); Caddy thắng ở sự đơn giản và là cách nhanh nhất để có HTTPS đúng, tự gia hạn; IIS là mặc định trong các shop Windows. EnvoyHAProxy cũng đáng nhắc tới: Envoy là proxy L7 hiện đại làm nền cho service mesh (Istio) và API gateway với observability phong phú và cấu hình động (xDS); HAProxy là load balancer hiệu năng cao đã được kiểm chứng, mạnh ở cả L4 và L7.

Forward proxy vs reverse proxy

Mẹo nhớ: forward proxy giấu client; reverse proxy giấu server. API gateway là một reverse proxy chuyên biệt bổ sung auth, rate limiting, biến đổi request và policy theo từng route cho microservice.

Các lớp cache và CDN

Caching là công cụ hiệu năng có đòn bẩy cao nhất ở lớp này — request nhanh nhất là request không bao giờ chạm tới origin.

Khái niệm cache quan trọng:

Khái niệm chính

Các thuật toán load balancing

Thuật toánCách chọn backendKhi nào dùng
Round robinLần lượt từng backend (có thể gắn trọng số)Backend stateless đồng nhất — lựa chọn mặc định hợp lý
Weighted round robinTỉ lệ theo trọng số từng serverInstance kích thước khác nhau; rollout/canary dần theo trọng số
Least connectionsBackend có ít kết nối đang hoạt động nhấtRequest có thời lượng chênh lệch lớn
Least response timeĐộ trễ thấp + ít kết nối nhấtService nhạy độ trễ (Nginx Plus / HAProxy)
IP hashHash IP của client → luôn về cùng backendSession affinity đơn giản không cần cookie
Consistent hashingHash theo một key (URI, user ID) với remap tối thiểu khi pool đổiTối ưu cache locality; pool sharded/stateful co giãn
Random + power of two choicesChọn ngẫu nhiên hai backend, lấy bên ít tải hơnPool rất lớn; tránh hiệu ứng bầy đàn với ít state

Health check khiến cân bằng tải trở nên an toàn: passive (đánh dấu backend chết sau N request thật thất bại, ví dụ Nginx max_fails/fail_timeout) và active (probe riêng gọi /healthz theo lịch). Không có health check, load balancer vẫn vô tư đẩy traffic vào node đã chết. Phân biệt liveness (tiến trình còn sống?) với readiness (có phục vụ được ngay bây giờ?) để node quá tải hoặc đang khởi động được rút ra thay vì chập chờn.

Session affinity (“sticky session”) ghim một client vào một backend (qua cookie hoặc IP hash) để state session trong bộ nhớ nhất quán — nhưng nó phá vỡ phân phối tải đều và làm phức tạp việc scale. Ưu tiên backend stateless với kho session dùng chung (Redis, JWT) và tránh affinity khi có thể.

L4 vs L7 load balancing

L4 (transport)L7 (application)
Hoạt động trênIP + port TCP/UDPToàn bộ HTTP request (method, path, host, header, cookie)
Tốc độ / overheadRất nhanh, ít CPU, throughput caoTốn CPU hơn (parse HTTP, thường terminate TLS)
Quyết định routingTheo từng kết nốiTheo từng request (theo path, host, header/cookie, canary %)
TLSPassthrough (hoặc terminate mà không inspect)Terminate; có thể mã hóa lại về backend
ObservabilityMức kết nốiPhong phú: status code, độ trễ, metric theo route
Tính năngChỉ chuyển tiếpRetry, redirect, rewrite, WAF, rate limiting
Ví dụAWS NLB, HAProxy chế độ TCP, IPVS, MetalLBAWS ALB, Nginx, HAProxy chế độ HTTP, Envoy, Traefik

Quy tắc chung: L7 cho microservice HTTP (routing phong phú, retry, observability, TLS termination); L4 khi bạn mang giao thức không phải HTTP, cần throughput cực lớn với ít CPU, muốn không phá vỡ TLS end-to-end (passthrough), hoặc cần giữ IP nguồn của client mà không dùng thủ thuật header. Nhiều stack dùng cả hai: một NLB L4 phía trước để trải các kết nối thô, rồi các proxy L7 phía sau để routing.

Tầng proxy như một ranh giới bảo mật

Tầng proxy là điểm thực thi bảo mật tự nhiên:

Xử lý kết nối và timeout

Hai điểm tinh tế thường xuyên cắn người vận hành:

Cấu hình Nginx mẫu

Reverse proxy với upstream pool và keepalive:

upstream app_backend {
    least_conn;
    server 10.0.1.10:8080 max_fails=3 fail_timeout=30s;
    server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
    server 10.0.1.12:8080 backup;              # chỉ dùng khi các server kia chết
    keepalive 32;                              # tái dùng kết nối upstream
}

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

    location / {
        proxy_pass http://app_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection        "";  # bắt buộc cho keepalive upstream
        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_connect_timeout 5s;
        proxy_send_timeout    30s;
        proxy_read_timeout    60s;
        proxy_next_upstream error timeout http_502 http_503;   # thử backend kế tiếp
    }
}

TLS termination (kèm redirect HTTP→HTTPS và cấu hình hiện đại):

server {
    listen 80;
    server_name app.example.com;
    return 301 https://$host$request_uri;      # ép HTTPS
}

server {
    listen 443 ssl;
    http2 on;
    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;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;             # để client TLS 1.3 tự chọn
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_stapling on;                           # OCSP stapling
    ssl_stapling_verify on;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    server_tokens off;                         # ẩn phiên bản Nginx

    client_max_body_size 10m;

    location / {
        proxy_pass http://app_backend;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    }
}

Proxy caching kèm stale-on-error và chống stampede:

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m
                 max_size=1g inactive=60m use_temp_path=off;

server {
    location /api/ {
        proxy_cache api_cache;
        proxy_cache_key "$scheme$request_method$host$request_uri";
        proxy_cache_valid 200 302 10m;
        proxy_cache_valid 404      1m;
        proxy_cache_use_stale error timeout updating http_502 http_503 http_504;
        proxy_cache_background_update on;        # làm tươi ở nền
        proxy_cache_lock on;                     # gộp các cache miss đồng thời (chống stampede)
        add_header X-Cache-Status $upstream_cache_status;   # HIT / MISS / STALE
        proxy_pass http://app_backend;
    }
}

Phục vụ file tĩnh với cache dài hạn, immutable:

location /static/ {
    root /var/www/app;
    expires 1y;
    add_header Cache-Control "public, immutable";
    access_log off;
    gzip_static on;                              # phục vụ file .gz nén sẵn nếu có
}

Rate limit một endpoint:

limit_req_zone $binary_remote_addr zone=login:10m rate=5r/s;

location /login {
    limit_req zone=login burst=10 nodelay;
    proxy_pass http://app_backend;
}

Lệnh vận hành:

nginx -t                     # kiểm tra config TRƯỚC khi áp dụng — không bao giờ reload mù
nginx -s reload              # reload mượt: worker mới nhận config, worker cũ rút dần
nginx -T                     # in toàn bộ config hiệu lực (mọi include)
curl -I -H "Host: app.example.com" http://127.0.0.1/   # test routing tại chỗ không cần DNS

Tương đương với Caddy (HTTPS tự động trong ba dòng):

app.example.com {
    reverse_proxy 10.0.1.10:8080 10.0.1.11:8080 {
        lb_policy least_conn
        health_uri /healthz
    }
}

Caddy tự cấp và gia hạn certificate — không certbot, không cron — đó là lý do nó được ưa chuộng cho service nhỏ và công cụ nội bộ.

Best Practices

  1. Luôn chạy nginx -t và dùng graceful reload — kiểm tra config cộng với nginx -s reload cho phép thay đổi không downtime; lỗi cú pháp không bao giờ được chạm tới traffic đang chạy. Trong CI, chạy nginx -t với config đã render trong container.
  2. Terminate TLS ở biên, nhưng cân nhắc mã hóa lại — TLS termination tập trung hóa việc quản lý cert; với môi trường nhạy cảm, bị quản chế hoặc zero-trust, mã hóa lại chặng proxy→backend (hoặc dùng service mesh với mTLS). Không bao giờ gửi credential qua chặng nội bộ plaintext trong mạng không tin cậy.
  3. Chuyển tiếp danh tính thật của client — set X-Forwarded-For, X-Forwarded-ProtoHost, đồng thời cấu hình app chỉ tin proxy của bạn (Nginx real_ip / set_real_ip_from). Nếu không, log, rate limit, geolocation và quyết định auth đều dùng IP của proxy và đều sai (và có thể bị giả mạo).
  4. Health check phải phản ánh mức sẵn sàng thật — probe một endpoint kiểm tra cả các dependency quan trọng, không chỉ “process còn sống”; tách liveness với readiness để node quá tải hoặc đang khởi động được rút ra thay vì chập chờn.
  5. Đặt timeout tường minh ở mọi nơi — connect/send/read trên proxy và upstream. Giá trị mặc định thường không giới hạn hoặc quá rộng, và một backend chậm có thể ngốn hết worker connection của proxy và lan thành sự cố toàn diện.
  6. Cấu hình keepalive cho upstreamkeepalive trong block upstream cộng proxy_http_version 1.1Connection "" tránh handshake TCP/TLS mới cho mỗi request; nguồn gây độ trễ và cạn kiệt port thường xuyên và vô hình.
  7. Phục vụ dữ liệu stale khi backend lỗiproxy_cache_use_stale (Nginx) hoặc grace mode (Varnish) giữ người dùng vẫn được phục vụ khi origin sập; kết hợp proxy_cache_lock/proxy_cache_background_update để tránh cache stampede khi một entry phổ biến hết hạn.
  8. Không bao giờ để backend lộ trực tiếp — security group chỉ nhận traffic từ tầng load balancer/proxy (tham chiếu security group của nó); xác minh bằng port scan từ bên ngoài và audit định kỳ.
  9. Rate limit và giới hạn kích thước ở biênlimit_req/limit_conn theo IP hoặc token và client_max_body_size bảo vệ backend khỏi lạm dụng và tấn công cạn kiệt tài nguyên, rẻ hơn nhiều so với scale out.
  10. Bật HTTP/2 (và HTTP/3 nếu hỗ trợ) về phía client — multiplexing cải thiện rõ rệt tốc độ tải trang; chặng proxy→backend có thể giữ HTTP/1.1 với keepalive, đơn giản hơn và thường là đủ.
  11. Cache mạnh tay nhưng đúng cách — cache dài hạn immutable cho static asset có hash, TTL ngắn kèm revalidate cho API response, Vary đúng, và không bao giờ cache công khai nội dung đã xác thực theo user. Phát X-Cache-Status để thấy tỉ lệ hit.
  12. Ẩn thông tin server và gửi security headerserver_tokens off;, cộng với HSTS, X-Content-Type-Options: nosniff, X-Frame-Options/frame-ancestors và Content-Security-Policy phù hợp với app.
  13. Nén response — gzip hoặc Brotli cho text/JSON/HTML/CSS/JS (không cho ảnh/video đã nén sẵn); ưu tiên static asset nén sẵn (gzip_static). Thắng lớn về băng thông và độ trễ với chi phí nhỏ.
  14. Log dạng structured kèm các trường cache/upstream — thêm $upstream_addr, $upstream_status, $upstream_response_time$upstream_cache_status; điều tra 5xx và độ trễ trở nên dễ dàng khi thấy rõ backend nào phục vụ request và cache có hit hay không.
  15. Giữ config proxy trong version control và template hóa — config là code: review qua PR, lint trong CI (nginx -t trong container), render theo từng môi trường bằng công cụ templating, deploy bằng configuration management hoặc Kubernetes ingress controller, tuyệt đối không sửa tay trên server đang chạy.
  16. Tinh chỉnh giới hạn worker và kết nối theo workloadworker_processes auto;, worker_connections đủ lớn, và giới hạn file-descriptor ở mức OS; mặc định giả định một máy nhỏ và âm thầm chặn throughput trên máy lớn hơn.

Tài liệu tham khảo

Part of the DevOps Roadmap knowledge base.

Overview

Between the internet and your application code sits a layer of web servers, proxies, and load balancers. This layer terminates TLS, serves static files, compresses and caches responses, routes requests to the right backend, absorbs traffic spikes, and shields applications from malformed or malicious traffic. Nearly every production HTTP request passes through at least one of these components — often several in sequence (CDN → cloud load balancer → Nginx ingress → app).

For DevOps engineers this layer is daily work: writing Nginx configs, setting up TLS termination, tuning cache headers, defining health checks, and choosing between L4 and L7 load balancing. It’s also the first place to look during incidents — 502/504 errors, connection resets, and TLS failures all surface here, and a proxy that logs upstream timing turns “the site is slow” into “backend #3 is slow.”

The concepts are portable. Whether the concrete tool is Nginx, HAProxy, Envoy, Traefik, an AWS ALB, or a Kubernetes ingress controller, the same ideas apply: reverse proxying, upstream pools, balancing algorithms, health checks, connection handling, and caching. Learn the model once and every tool becomes a matter of syntax.

Fundamentals

What a web server actually does

At its core a web server (a) accepts TCP connections, (b) parses HTTP requests, and (c) produces responses — either from static files on disk or by delegating to application code. The delegation mechanism matters:

The other axis is the concurrency model, which determines how many connections a server handles under load.

Web servers compared

NginxApache HTTP ServerCaddyIIS
ArchitectureEvent-driven, async workers (few processes, many connections each)Process/thread per connection (MPM event improves this)Event-driven (Go, goroutines)Windows-integrated, async I/O
Concurrency under loadExcellent, low memoryGood with event MPM; heavier with preforkVery goodGood on Windows
Configurationnginx.conf blocks (declarative)Directives + per-dir .htaccess (runtime override)Caddyfile (minimal) or JSON APIGUI / web.config
Automatic HTTPSNo (use certbot / acme.sh)No (use certbot / mod_md)Yes — built-in ACME, on by defaultVia Windows ACME clients
Typical roleReverse proxy, static files, LB, K8s ingressLegacy hosting, .htaccess-dependent apps (PHP shared hosting)Small/medium services wanting zero-config TLSWindows/.NET stacks
Dynamic contentVia upstream (proxy/FastCGI)Embedded modules (mod_php) or proxyVia reverse proxyASP.NET in-process

Nginx dominates the reverse-proxy/ingress niche due to its low memory footprint and high concurrency; Apache remains common where .htaccess per-directory flexibility matters (shared hosting, legacy PHP); Caddy wins on simplicity and is the fastest way to get correct, auto-renewing HTTPS; IIS is the default in Windows shops. Envoy and HAProxy deserve mention too: Envoy is the modern L7 proxy powering service meshes (Istio) and API gateways with rich observability and dynamic (xDS) config; HAProxy is a battle-tested high-performance load balancer strong at both L4 and L7.

Forward proxy vs reverse proxy

Memory aid: forward proxy hides the client; reverse proxy hides the server. An API gateway is a specialized reverse proxy adding auth, rate limiting, request transformation, and per-route policy for microservices.

Caching layers and CDNs

Caching is the highest-leverage performance tool at this layer — the fastest request is the one that never reaches your origin.

Key cache concepts:

Key Concepts

Load balancing algorithms

AlgorithmHow it picks a backendWhen to use
Round robinEach in turn (optionally weighted)Homogeneous stateless backends — the sensible default
Weighted round robinProportional to per-server weightMixed instance sizes; gradual rollout / canary by weight
Least connectionsFewest active connectionsRequests with highly variable duration
Least response timeLowest latency + fewest connectionsLatency-sensitive services (Nginx Plus / HAProxy)
IP hashHash of client IP → same backendCheap session affinity without cookies
Consistent hashingHash of a key (URI, user ID) with minimal remapping on pool changeCache locality; sharded/stateful pools that scale up and down
Random + power of two choicesPick two at random, take the less loadedVery large pools; avoids herd behavior with little state

Health checks make balancing safe: passive (mark a backend down after N failed real requests, e.g. Nginx max_fails/fail_timeout) and active (a dedicated probe hitting /healthz on a schedule). Without health checks, a balancer happily routes traffic to a dead node. Distinguish liveness (is the process alive?) from readiness (can it serve traffic right now?) so an overloaded or warming-up node drains instead of flapping.

Session affinity (“sticky sessions”) pins a client to one backend (via cookie or IP hash) so in-memory session state stays consistent — but it defeats even load distribution and complicates scaling. Prefer stateless backends with shared session storage (Redis, JWT) and avoid affinity where you can.

L4 vs L7 load balancing

L4 (transport)L7 (application)
Operates onIP + TCP/UDP portFull HTTP request (method, path, host, headers, cookies)
Speed / overheadVery fast, low CPU, high throughputMore CPU (parses HTTP, often terminates TLS)
Routing decisionsPer connectionPer request (path-based, host-based, header/cookie, canary %)
TLSPassthrough (or terminate without inspecting)Terminates; can re-encrypt to backend
ObservabilityConnection-levelRich: status codes, latency, per-route metrics
FeaturesJust forwardingRetries, redirects, rewrites, WAF, rate limiting
ExamplesAWS NLB, HAProxy TCP mode, IPVS, MetalLBAWS ALB, Nginx, HAProxy HTTP mode, Envoy, Traefik

Rule of thumb: L7 for HTTP microservices (rich routing, retries, observability, TLS termination); L4 when you carry non-HTTP protocols, need extreme throughput with minimal CPU, want end-to-end TLS not to be broken (passthrough), or need to preserve the client source IP without header tricks. Many stacks use both: an L4 NLB in front to spread raw connections, then L7 proxies behind it for routing.

The proxy tier as a security boundary

The proxy tier is the natural enforcement point:

Connection handling and timeouts

Two subtleties bite operators constantly:

Sample Nginx configurations

Reverse proxy with an upstream pool and keepalive:

upstream app_backend {
    least_conn;
    server 10.0.1.10:8080 max_fails=3 fail_timeout=30s;
    server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
    server 10.0.1.12:8080 backup;              # only used if the others are down
    keepalive 32;                              # reuse upstream connections
}

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

    location / {
        proxy_pass http://app_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection        "";  # required for upstream keepalive
        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_connect_timeout 5s;
        proxy_send_timeout    30s;
        proxy_read_timeout    60s;
        proxy_next_upstream error timeout http_502 http_503;   # retry the next backend
    }
}

TLS termination (with HTTP→HTTPS redirect and modern settings):

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

server {
    listen 443 ssl;
    http2 on;
    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;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;             # let TLS 1.3 clients choose
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_stapling on;                           # OCSP stapling
    ssl_stapling_verify on;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    server_tokens off;                         # hide Nginx version

    client_max_body_size 10m;

    location / {
        proxy_pass http://app_backend;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    }
}

Proxy caching with stale-on-error and stampede protection:

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m
                 max_size=1g inactive=60m use_temp_path=off;

server {
    location /api/ {
        proxy_cache api_cache;
        proxy_cache_key "$scheme$request_method$host$request_uri";
        proxy_cache_valid 200 302 10m;
        proxy_cache_valid 404      1m;
        proxy_cache_use_stale error timeout updating http_502 http_503 http_504;
        proxy_cache_background_update on;        # refresh in the background
        proxy_cache_lock on;                     # collapse concurrent misses (anti-stampede)
        add_header X-Cache-Status $upstream_cache_status;   # HIT / MISS / STALE
        proxy_pass http://app_backend;
    }
}

Serving static files with long-lived, immutable caching:

location /static/ {
    root /var/www/app;
    expires 1y;
    add_header Cache-Control "public, immutable";
    access_log off;
    gzip_static on;                              # serve pre-compressed .gz if present
}

Rate limiting an endpoint:

limit_req_zone $binary_remote_addr zone=login:10m rate=5r/s;

location /login {
    limit_req zone=login burst=10 nodelay;
    proxy_pass http://app_backend;
}

Operational commands:

nginx -t                     # validate config BEFORE applying — never reload blind
nginx -s reload              # graceful reload: new workers pick up config, old ones drain
nginx -T                     # dump the full effective config (all includes)
curl -I -H "Host: app.example.com" http://127.0.0.1/   # test routing locally without DNS

Caddy equivalent (automatic HTTPS in three lines):

app.example.com {
    reverse_proxy 10.0.1.10:8080 10.0.1.11:8080 {
        lb_policy least_conn
        health_uri /healthz
    }
}

Caddy provisions and renews the certificate automatically — no certbot, no cron — which is why it’s a favourite for small services and internal tools.

Best Practices

  1. Always run nginx -t and use graceful reloads — config validation plus nginx -s reload means zero-downtime changes; a syntax error should never reach live traffic. In CI, run nginx -t against the rendered config in a container.
  2. Terminate TLS at the edge, but consider re-encryption — TLS termination centralizes certificate management; for sensitive, regulated, or zero-trust environments, re-encrypt proxy→backend (or use a service mesh with mTLS). Never send credentials over plaintext internal hops in untrusted networks.
  3. Forward the real client identity — set X-Forwarded-For, X-Forwarded-Proto, and Host, and configure the app to trust only your proxy (Nginx real_ip / set_real_ip_from). Otherwise logs, rate limits, geolocation, and auth decisions all use the proxy’s IP and are wrong (and spoofable).
  4. Health checks must reflect real readiness — probe an endpoint that verifies critical dependencies, not just “process is up”; separate liveness from readiness so overloaded or starting nodes drain rather than flap.
  5. Set explicit timeouts everywhere — connect/send/read on proxy and upstream. Defaults are often unbounded or too generous, and one slow backend can exhaust proxy worker connections and cascade into a full outage.
  6. Configure upstream keepalivekeepalive in the upstream block plus proxy_http_version 1.1 and Connection "" avoids a new TCP/TLS handshake per request; a frequent, invisible source of latency and port exhaustion.
  7. Serve stale on backend failureproxy_cache_use_stale (Nginx) or grace mode (Varnish) keeps users served during origin outages; pair with proxy_cache_lock/proxy_cache_background_update to prevent cache stampedes when a popular entry expires.
  8. Never expose backends directly — security groups should accept traffic only from the load balancer/proxy tier (reference its security group); verify with an external port scan and periodic audits.
  9. Rate limit and size-limit at the edgelimit_req/limit_conn per IP or token and client_max_body_size protect backends from abuse and resource-exhaustion attacks far more cheaply than scaling out.
  10. Enable HTTP/2 (and HTTP/3 where supported) toward clients — multiplexing meaningfully improves page load; the proxy→backend hop can stay HTTP/1.1 with keepalive, which is simpler and usually sufficient.
  11. Cache aggressively but correctly — long-lived immutable caching for hashed static assets, short TTLs with revalidation for API responses, correct Vary, and never cache per-user authenticated content publicly. Emit X-Cache-Status so you can see hit rates.
  12. Hide server details and send security headersserver_tokens off;, plus HSTS, X-Content-Type-Options: nosniff, X-Frame-Options/frame-ancestors, and a Content-Security-Policy appropriate to the app.
  13. Compress responses — gzip or Brotli for text/JSON/HTML/CSS/JS (not for already-compressed images/video); prefer pre-compressed static assets (gzip_static). Big bandwidth and latency wins for little cost.
  14. Log in structured form with cache/upstream fields — include $upstream_addr, $upstream_status, $upstream_response_time, and $upstream_cache_status; 5xx and latency triage becomes trivial when you can see which backend served the request and whether the cache was hit.
  15. Keep proxy configs in version control and template them — configs are code: review them in PRs, lint in CI (nginx -t in a container), render per-environment with a templating tool, deploy via configuration management or a Kubernetes ingress controller, and never hand-edit on live servers.
  16. Tune worker and connection limits for the workloadworker_processes auto;, sufficient worker_connections, and OS-level file-descriptor limits; the defaults assume a small box and silently cap throughput on larger ones.

References