Nginx as a Reverse Proxy
Configure Nginx as a reverse proxy and load balancer: server blocks, upstreams, TLS, caching, rate limiting, and troubleshooting tips.
What it is and when to use it
Nginx is a high-performance web server that is most often used as a reverse proxy, TLS terminator, static file server and HTTP load balancer. Its event-driven design handles tens of thousands of concurrent connections with modest memory.
Use it in front of application servers to terminate HTTPS, route by hostname or path, compress and cache responses, absorb slow clients and spread traffic across several backends. It is also the engine behind the Kubernetes ingress-nginx controller.
Quick start
On Debian or Ubuntu, install and verify.
sudo apt update && sudo apt install -y nginx
sudo nginx -t # test configuration
sudo systemctl reload nginx # apply without dropping connections
sudo tail -f /var/log/nginx/error.log
Or run it in Docker with your config mounted.
docker run --rm -p 8080:80 \
-v "$PWD/default.conf:/etc/nginx/conf.d/default.conf:ro" nginx:stable
A minimal reverse proxy in /etc/nginx/conf.d/app.conf:
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}
Core concepts
Contexts and directives. Configuration is a tree: main, events, http, server and location. Directives inherit downward unless overridden. The server block chosen depends on listen and server_name; the location chosen depends on the request path using exact match, longest prefix and regex rules.
Upstreams and load balancing. Define a pool and choose a method.
upstream app_pool {
least_conn;
server 10.0.1.10:3000 max_fails=3 fail_timeout=10s;
server 10.0.1.11:3000;
server 10.0.1.12:3000 backup;
keepalive 32;
}
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;
location / {
proxy_pass http://app_pool;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name app.example.com;
return 301 https://$host$request_uri;
}
Methods include round robin (default), least_conn, ip_hash and hash for consistent keys. Passive health checks come from max_fails; active checks require Nginx Plus or an external tool.
WebSockets need the upgrade headers.
location /ws/ {
proxy_pass http://app_pool;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
Caching and compression.
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=appcache:10m max_size=1g inactive=60m;
location /api/public/ {
proxy_cache appcache;
proxy_cache_valid 200 5m;
proxy_cache_use_stale error timeout updating;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://app_pool;
}
Rate limiting.
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
location /login {
limit_req zone=perip burst=20 nodelay;
proxy_pass http://app_pool;
}
Pitfalls
- A trailing slash in proxy_pass changes path rewriting. With a URI part, the matched location prefix is replaced; without, the original path is forwarded unchanged.
- Forgetting the Host and X-Forwarded headers breaks redirects, absolute URLs and client IP logging in the backend. Behind another proxy, use the real_ip module with a trusted address list.
- The if directive inside location is fragile. Prefer map blocks and try_files.
- Default body size is 1 MB, so uploads fail with 413 until you raise client_max_body_size.
- Nginx resolves upstream hostnames at startup. For dynamic DNS, use a resolver directive and a variable in proxy_pass.
- A 502 means the backend refused or closed the connection; 504 means a timeout. Check the error log first, and tune proxy_connect_timeout and proxy_read_timeout deliberately.
- Always run nginx -t before reload, and keep configuration in version control.
How it fits an architecture
Nginx commonly forms the edge tier: DNS or a cloud load balancer sends traffic to Nginx, which terminates TLS, applies rate limits and routes to application pools or to a Kubernetes ingress. Configuration can be rendered by Ansible templates or Helm values and rolled out through CI. For service-to-service traffic with dynamic discovery and rich telemetry, a proxy such as Envoy, usually inside a mesh like Istio, is a better fit.
Key takeaways
- Nginx excels as a reverse proxy, TLS terminator and static server.
- Define upstream pools and choose a balancing method suited to your traffic.
- Always forward Host and X-Forwarded headers and handle WebSocket upgrades explicitly.
- Test with nginx -t, then reload rather than restart.
- Use caching and rate limiting to protect backends, and watch trailing slash behavior in proxy_pass.