news 2026/9/23 19:19:41

Nginx as a Reverse Proxy

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Nginx as a Reverse Proxy

“Nginx or a reverse proxy?” is a category error worth unpacking.Reverse proxyis arole; Nginx is oneimplementationof it. The useful questions are what the role actually requires, how Nginx implements it, and when a different implementation fits better.

Abbreviation glossary

AbbreviationFull English name中文
rProxyreverse proxy反向代理
TLSTransport Layer Security传输层安全协议
SSLSecure Sockets Layer安全套接层(TLS 的前身)
SNIServer Name Indication服务器名称指示
L4 / L7OSI Layer 4 (transport) / Layer 7 (application)四层(传输层)/ 七层(应用层)
ALBApplication Load Balancer (AWS)应用负载均衡器
NLBNetwork Load Balancer (AWS)网络负载均衡器
SSEServer-Sent Events服务器推送事件
WAFWeb Application FirewallWeb 应用防火墙
xDSExtensible Discovery Service (Envoy’s config API)可扩展发现服务
CRDCustom Resource Definition (Kubernetes)自定义资源定义
RPSRequests Per Second每秒请求数
TTFBTime To First Byte首字节时间

1. The role, stated precisely

A reverse proxy is a server that terminates a client connection, then originates aseparateconnection to one or more upstream servers on the client’s behalf. Two connections, two independent lifecycles. Everything interesting follows from that split:

  • Because the connections are independent, the proxy canfan out(load balancing),retry(a failed upstream attempt need not fail the client),cache,rewrite, andterminate TLSwithout the client knowing.
  • Because the proxy terminates TLS, it becomes the place where certificates, cipher policy, and HTTP protocol versions are decided — the client may speak HTTP/2 while the upstream speaks HTTP/1.1.
  • Because the client only ever sees the proxy, upstream topology is free to change. This is the property that makes rolling deploys, blue/green, and canaries possible at all.

For how this differs from a forward proxy, see forward and reverse proxy. The rest of this article is about the reverse direction only.

连接 1
TLS 1.3 + HTTP/2

连接 2
HTTP/1.1 keepalive

连接池复用

被动健康检查
max_fails / fail_timeout

proxy_cache

Client
浏览器 / 移动端

Nginx
反向代理

upstream app-1

upstream app-2

upstream app-3
(标记为 down)

磁盘缓存区


2. Why Nginx scales: the architecture in one section

Nginx’s design choice, made in 2004 against Apache’s process-per-connection model, isevent-driven, non-blocking, fixed worker count.

  • Onemasterprocess reads config, binds listening sockets, and manages workers. It runs as root only to bind privileged ports and to open files.
  • Nworkerprocesses (worker_processes auto;→ one per CPU core) each run a single-threaded event loop overepoll(Linux) /kqueue(BSD). A worker holds tens of thousands of connections simultaneously because a connection that is waiting costs only a file descriptor and a small state struct — not a thread stack.
  • Workers share nothing except shared-memory zones you declare explicitly (proxy_cache_pathkeys,limit_req_zonecounters,upstreamstate). This is why rate limits and connection limits in open-source Nginx areper-worker-shared-zone, not per-cluster — a distinction that bites when you size limits.

The practical consequences for you as an operator:

  • Blocking a worker blocks every connection it holds.Disk I/O is the usual culprit;aioandsendfileexist for this. Any third-party module doing synchronous work — a naive Lua script making a blocking call — will destroy tail latency.
  • Memory is bounded and predictable.A worker’s footprint is dominated by buffers you configured, not by concurrency. This is why Nginx behaves gracefully at the point where thread-per-request servers fall over.
  • Config reload is graceful by construction.nginx -s reloadspawns new workers with the new config; old workers stop accepting and drain in-flight requests. Zero dropped connections, no connection reuse across the boundary.

3.proxy_pass: the mechanics, including the trailing-slash trap

This is the single most misunderstood directive in Nginx.

location /api/ { proxy_pass http://backend; # NO trailing slash } # GET /api/users → upstream receives /api/users
location /api/ { proxy_pass http://backend/; # trailing slash } # GET /api/users → upstream receives /users

The rule:if theproxy_passvalue contains a URI component (anything after the host, including a bare/), the part of the request URI matched by thelocationprefix is replaced by that URI.If there is no URI component, the original request URI is passed through unchanged.

Two corollaries that cost people afternoons:

  • With aregexlocation or alocationusing named captures, the URI-replacement form is not allowed — Nginx requires you to construct the target explicitly, usually with variables andrewrite.
  • Using avariableinproxy_pass(proxy_pass http://$upstream_host;) changes the resolution semantics entirely: Nginx then resolves the name at request time using theresolverdirective, rather than once at startup. This is the standard trick for upstreams whose DNS changes — and the standard cause ofno resolver defined to resolve ...errors.
# Dynamic upstream resolution, re-resolved per TTL resolver 10.0.0.2 valid=30s ipv6=off; location /svc/ { set $target "service.internal.example:8080"; proxy_pass http://$target/; }

Without this,Nginx resolves upstream hostnames once at startup and caches the result forever.On a platform where backends get new IPs — Kubernetes, ECS, any autoscaling group — a staticproxy_pass http://service.internal:8080;will keep hammering a dead IP until you reload. This is one of the most common production surprises when moving Nginx into a container platform.


4. Headers: the inheritance rule nobody remembers

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;

The rule:proxy_set_headerdirectives are inherited from an outer block only if the inner block defines none of its own.Add a singleproxy_set_headerinside alocationand every inherited header fromserverorhttpsilently disappears. This is the mechanism behind “it worked until I added one header and the app started 404-ing on Host”.

TheHostheader matters more than it looks:

  • Default isproxy_set_header Host $proxy_host;— theupstream’sname. Virtual-hosted backends, absolute URL generation, and cookie domains all break.
  • $hostis the request’s Host header with the port stripped, falling back toserver_name. Usually what you want.
  • $http_hostis the raw client-supplied value including port. Use when the upstream needs to reconstruct exact URLs; be aware it is fully attacker-controlled.

$proxy_add_x_forwarded_forappends$remote_addrto any existingX-Forwarded-For.That existing value came from the client and is a lie until proven otherwise.If your Nginx is the internet-facing edge, overwrite rather than append:

proxy_set_header X-Forwarded-For $remote_addr; # edge: do not trust what arrived

If Nginx isbehinda trusted load balancer, usereal_ipto establish the true client address before anything else reads it:

set_real_ip_from 10.0.0.0/8; # the trusted LB range, and only that real_ip_header X-Forwarded-For; real_ip_recursive on; # walk right-to-left past trusted hops

Get this wrong in either direction and you have either broken geolocation and rate limiting, or built a trivially spoofable IP allowlist.


5. Buffering: the reason your streaming endpoint is broken

By default Nginxbuffers the upstream response: it reads the response as fast as the upstream can produce it, into memory (proxy_buffers) and spilling to disk (proxy_max_temp_file_size), then feeds it to the client at the client’s pace.

This is the right default for most traffic. It frees the upstream worker — typically an expensive thread in a Java or Python app — the instant the response is produced, rather than holding it for the seconds a mobile client on a poor connection needs to receive it. Slow-client absorption is arguably Nginx’s single largest contribution to backend capacity.

It is exactly wrong for:

  • Server-Sent Events and long-poll— events accumulate in the buffer and arrive in a burst, or never.
  • Streaming LLM token responses— the user sees nothing, then the whole answer at once.
  • Large uploadswithproxy_request_buffering on(the default for requests) — the whole body lands on the proxy’s disk before the upstream sees a byte.
location /stream/ { proxy_pass http://backend; proxy_buffering off; # forward each chunk immediately proxy_cache off; proxy_read_timeout 3600s; # a long-lived stream is not a stuck request chunked_transfer_encoding on; add_header X-Accel-Buffering no; # also tells any upstream Nginx to stop buffering } location /upload/ { proxy_pass http://backend; proxy_request_buffering off; # stream the body through client_max_body_size 0; # 0 = no limit; set a real number in production }

X-Accel-Buffering: nois worth remembering in the other direction too — anupstream applicationcan emit that header to ask the fronting Nginx to disable buffering for that response, without any proxy config change.


6. Upstreams, keepalive, and timeouts

upstream backend { # Algorithms: round-robin (default), least_conn, ip_hash, hash <key> [consistent] least_conn; server 10.0.1.10:8080 max_fails=3 fail_timeout=10s weight=2; server 10.0.1.11:8080 max_fails=3 fail_timeout=10s; server 10.0.1.12:8080 backup; # only used when all primaries are down keepalive 32; # persistent connections retained PER WORKER keepalive_timeout 60s; keepalive_requests 1000; } server { location / { proxy_pass http://backend; proxy_http_version 1.1; # REQUIRED for upstream keepalive proxy_set_header Connection ""; # REQUIRED: clear the inherited "close" proxy_connect_timeout 2s; # TCP connect — should be small proxy_send_timeout 30s; # between successive writes to upstream proxy_read_timeout 30s; # between successive reads — NOT total duration proxy_next_upstream error timeout http_502 http_503; proxy_next_upstream_tries 2; proxy_next_upstream_timeout 5s; } }

Points that matter in production:

Upstream keepalive needs all three lines.keepalive 32;alone does nothing: withoutproxy_http_version 1.1andproxy_set_header Connection "";Nginx still sendsConnection: closeand opens a fresh TCP connection per request. On a TLS-to-upstream path that is a full handshake per request, and it shows up as a flat tens-of-milliseconds tax on TTFB.

keepalive Nis per worker, not global.With 8 workers andkeepalive 32, the upstream may see up to 256 idle connections from this one proxy. Size upstream connection limits accordingly.

proxy_read_timeoutis an inactivity timer, not a total-request budget.An upstream that dribbles a byte every 29 seconds will never time out. If you need a hard ceiling, enforce it upstream or in front.

proxy_next_upstreamretries are dangerous on non-idempotent requests.errorandtimeoutare in the default set, and atimeouton a POST means the upstream may well have processed it. Nginx hasnon_idempotentas an opt-in for this reason — theabsenceof that keyword means POST/PATCH/LOCK are not retried, which is correct. Do not add it casually.

Health checking in open-source Nginx is passive only.max_fails/fail_timeoutmark a server down after real requests fail — meaning real users absorb the failures, and a server that recovers is only rediscovered when thefail_timeoutwindow lapses and a user’s request is used as the probe. Active health checks (health_checkdirective) are an Nginx Plus feature. This is one of the clearest reasons teams move to Envoy or a cloud load balancer.


7. TLS termination and re-encryption

server { listen 443 ssl; http2 on; server_name api.example.com; ssl_certificate /etc/nginx/tls/fullchain.pem; # leaf + intermediates, in order ssl_certificate_key /etc/nginx/tls/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; # TLS 1.3: let the client choose ssl_session_cache shared:SSL:10m; # shared across workers — important ssl_session_tickets off; # unless you rotate ticket keys properly ssl_stapling on; # OCSP stapling for the server cert ssl_stapling_verify on; resolver 1.1.1.1 valid=300s; location / { # Re-encrypt to the upstream proxy_pass https://backend; proxy_ssl_verify on; proxy_ssl_trusted_certificate /etc/nginx/tls/internal-ca.pem; proxy_ssl_name backend.internal.example; # SNI + verification name proxy_ssl_server_name on; # actually send SNI — off by default proxy_ssl_session_reuse on; } }

Two defaults that surprise people:

  • proxy_ssl_verifyisoffby default.Nginx will happily proxy to an upstream presenting any certificate, including an expired self-signed one from an attacker who has won a DNS race. If you re-encrypt, verify.
  • proxy_ssl_server_nameisoffby default, so Nginx does not send SNI to the upstream. Against any modern multi-tenant TLS endpoint this fails, often with a confusing certificate error rather than an obvious one.

ssl_session_cache shared:...must be shared, notbuiltin. The builtin cache is per worker, so a client whose resumption attempt lands on a different worker does a full handshake. On a busy edge this is a measurable CPU difference.


8. Complete, annotated config

A realistic edge config combining the above:

user nginx; worker_processes auto; worker_rlimit_nofile 65535; events { worker_connections 16384; multi_accept on; } http { # --- logging with the fields you will actually need at 03:00 --- log_format main '$remote_addr $host "$request" $status $body_bytes_sent ' 'rt=$request_time uct=$upstream_connect_time ' 'uht=$upstream_header_time urt=$upstream_response_time ' 'ua=$upstream_addr us=$upstream_status ' 'cache=$upstream_cache_status rid=$request_id'; access_log /var/log/nginx/access.log main buffer=32k flush=5s; sendfile on; tcp_nopush on; keepalive_timeout 65s; server_tokens off; # do not advertise the version # --- rate limiting: per worker-shared zone, 10 MB ≈ 160k IPs --- limit_req_zone $binary_remote_addr zone=perip:10m rate=20r/s; limit_conn_zone $binary_remote_addr zone=conn_perip:10m; proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=static:100m max_size=10g inactive=60m use_temp_path=off; upstream backend { least_conn; server 10.0.1.10:8080 max_fails=3 fail_timeout=10s; server 10.0.1.11:8080 max_fails=3 fail_timeout=10s; keepalive 32; } server { listen 443 ssl; http2 on; server_name api.example.com; ssl_certificate /etc/nginx/tls/fullchain.pem; ssl_certificate_key /etc/nginx/tls/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:10m; # Behind a trusted cloud LB — establish the real client IP first set_real_ip_from 10.0.0.0/8; real_ip_header X-Forwarded-For; real_ip_recursive on; limit_req zone=perip burst=40 nodelay; limit_conn conn_perip 20; # Shared proxy settings — remember: any proxy_set_header in an inner # block discards ALL of these. Re-declare or use an include file. proxy_http_version 1.1; proxy_set_header Connection ""; 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-Request-ID $request_id; proxy_connect_timeout 2s; proxy_read_timeout 30s; location /healthz { access_log off; return 200 "ok\n"; } location /static/ { proxy_pass http://backend; proxy_cache static; proxy_cache_valid 200 301 302 10m; proxy_cache_use_stale error timeout updating http_502 http_503; proxy_cache_lock on; # collapse concurrent misses proxy_cache_background_update on; add_header X-Cache-Status $upstream_cache_status always; } location /events { proxy_pass http://backend; proxy_buffering off; proxy_cache off; proxy_read_timeout 3600s; } location /ws { proxy_pass http://backend; proxy_set_header Upgrade $http_upgrade; # WebSocket upgrade proxy_set_header Connection "upgrade"; # NOTE: this block's proxy_set_header list replaces the server-level # one entirely — Host and X-Forwarded-* must be repeated here. proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_read_timeout 3600s; } location / { proxy_pass http://backend; } } }

proxy_cache_lock ondeserves a callout: without it, N concurrent requests for the same cold cache key all go to the upstream. That is the cache stampede that turns a cache expiry into an outage.

Verify before reloading, always:

nginx-t# parse and validatenginx-T|less# dump the FULLY resolved config, includes expandednginx-sreload# graceful: new workers take over, old ones drain

nginx -Tis the one to reach for when a directive “isn’t taking effect” — it shows exactly what Nginx assembled from your includes.


9. Nginx versus the other implementations of the role

Nginx (OSS)HAProxyEnvoyTraefikCaddyCloud ALB/NLB
Primary identityWeb server that proxies wellDedicated load balancerProgrammable L7 proxyContainer-native edge routerBatteries-included web serverManaged service
Config modelStatic file + reloadStatic file + reloadDynamic via xDS APIDynamic from labels/CRDsStatic file, very terseConsole / API / IaC
Config changeGraceful reloadGraceful reload (seamless since 1.8)Hot, no reloadAutomatic on service changeReload or admin APIAPI call
Active health checksPlus onlyYes, richYes, incl. outlier detectionYesBasicYes
ObservabilityBasicstub_status; logsVery detailed stats socketBest in class— per-upstream histogramsGoodBasicCloudWatch
Circuit breaking / outlier ejectionNoPartialYesPartialNoPartial
Automatic TLS certificatesNo (use certbot)NoNoYesYesYes (ACM)
gRPC / HTTP/2 upstreamYes (grpc_pass)YesNative, first-classYesYesALB: yes
Static file servingExcellentNoNoNoExcellentNo
CachingYes, solidNoLimitedNoVia pluginCloudFront separately
Memory footprintLowLowestHighModerateModeraten/a
ExtensibilityModules (compile-time), Lua via OpenRestyLua, SPOEWASM, Lua, external filtersMiddleware pluginsModules in GoLambda@Edge etc.
Typical sweet spotEdge: TLS + static + proxy in one processPure high-RPS TCP/HTTP balancingService mesh data plane; dynamic fleetsKubernetes / Docker ingressSmall services, zero-config HTTPSYou do not want to run it

Choose Nginx whenyou want one low-footprint process doing TLS termination, static assets, caching, and proxying, over a topology that changes on the order of deploys rather than seconds. It is the highest value-per-megabyte option in this table and the most widely understood.

Choose something else when:

  • Upstreams change continuously and you cannot reload for every change →Envoy(xDS) orTraefik(service discovery).
  • You need active health checks, outlier ejection, and circuit breaking without paying for Nginx Plus →EnvoyorHAProxy.
  • You need per-upstream latency histograms and retry/timeout budgets as first-class telemetry →Envoy.
  • You are doing pure L4 balancing at very high connection rates →HAProxyor anNLB.
  • You want TLS certificates to just work with no certbot cron →CaddyorTraefik.
  • The operational burden is not worth it → a managedALB, accepting the loss of caching and config expressiveness.

A very common and entirely reasonable production shape isboth: a cloud load balancer for the public IP, TLS certificates, and cross-zone distribution, with Nginx behind it doing the routing, caching, header work, and static serving that the cloud LB cannot express.


10. Operational checklist

  • nginx -tin CI;nginx -Tdiffed on config changes
  • proxy_http_version 1.1+proxy_set_header Connection ""whereverkeepaliveis set
  • resolverconfigured, and variable-basedproxy_passused for any upstream whose IP changes
  • X-Forwarded-Foroverwritten at the true edge,set_real_ip_fromscoped to the trusted range only
  • proxy_buffering offon every streaming and SSE route, and nowhere else
  • proxy_ssl_verify onandproxy_ssl_server_name onon every re-encrypted upstream
  • ssl_session_cache shared:(neverbuiltin)
  • proxy_cache_lock onon cacheable routes
  • $upstream_connect_time/$upstream_header_time/$upstream_response_timein the access log — without them you cannot tell a slow upstream from a slow proxy
  • server_tokens off, andclient_max_body_sizeset to a real value
  • Know that health checks are passive, and that the first failures are paid for by users

See also

  • Forward proxy and reverse proxy
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 19:19:23

计算机单片机毕设实战-基于 STM32 的多参数环境感知与柜体自动启闭系统设计 基于 STM32 的智能柜体通风除湿与声光报警系统设计(012009)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/23 19:12:03

BPSK匹配滤波实战:根升余弦成形与匹配滤波联合设计

简介&#xff1a;本资源是一份面向通信工程专业本科生与数字信号处理初学者的MATLAB仿真实验包&#xff0c;聚焦BPSK调制系统中匹配滤波与根升余弦脉冲成形的核心原理验证。资源通过完整闭环仿真&#xff0c;解决数字通信接收端如何在加性高斯白噪声环境下提升信噪比、抑制码间…

作者头像 李华
网站建设 2026/9/23 19:10:56

基于Java的实时评分系统毕设:从WebSocket到数据库设计全解析

简介&#xff1a;面向赛事评分场景的Java实时评分系统毕业设计项目&#xff0c;针对传统手写评分、人工计分慢且易错的问题&#xff0c;利用大屏展示、手机扫码与实时计算&#xff0c;提供一套从评分到结果展示的完整方案。压缩包内共61个文件&#xff0c;体积仅138KB&#xff…

作者头像 李华
网站建设 2026/9/23 19:06:55

鼻咽癌免疫治疗新机制:GP化疗激活ILB细胞增强T细胞抗肿瘤活性

1. 鼻咽癌免疫治疗研究新突破&#xff1a;GP化疗激活ILB细胞机制解析中山大学团队在《Nature Medicine》发表的这项研究&#xff0c;为我们揭示了吉西他滨联合顺铂&#xff08;GP方案&#xff09;治疗鼻咽癌的全新免疫调节机制。作为一名长期关注肿瘤免疫治疗的科研工作者&…

作者头像 李华
网站建设 2026/9/23 19:06:46

小番茄目标检测数据集XML转YOLO格式与训练避坑指南

简介&#xff1a;本资源是面向计算机视觉初学者与农业AI应用开发者的YOLO小番茄目标检测专用数据集&#xff0c;聚焦农作物成熟度识别这一典型工业落地场景。数据集共1790个文件&#xff0c;包含895张PNG格式实拍图像与895份对应XML标注文件&#xff0c;每份XML精确记录小番茄的…

作者头像 李华