DailyUse 项目中的 Nginx 配置解析

结合 DailyUse 当前生产 nginx.conf,解释 web 容器内 Nginx 的角色、HTTP 版本、gzip、缓存、反向代理与 Docker DNS 解析细节。

#tech / dev #tech / dev / project / dailyuse #tech / ops / deploy #resource / nginx #type / journal #status / growing

[!info] related notes

DailyUse 项目中的 Nginx 配置解析

目标

  • 把 DailyUse 当前 web 容器内这份 nginx.conf 按运行时职责讲清楚,而不是只停留在“它能代理 /api”。
  • 说明这份配置为什么这样写,尤其是 HTTP 版本、gzip、缓存、Docker DNS 与 WebSocket/Upgrade 相关细节。

前置条件

步骤

1. 先明确它在整条请求链路里的位置

当前 DailyUse 的链路不是“浏览器直接打到 Nginx”:

Browser
  -> Caddy
  -> web 容器内 Nginx
  -> api:3000

把协议层也展开,会更容易理解 proxy_http_version 1.1 这些配置为什么存在:

Browser
  -> Caddy
     - 对外负责 HTTPS
     - 浏览器侧可能协商 HTTP/1.1、HTTP/2,甚至 HTTP/3
  -> web(Nginx)
     - 这里只 listen 80,所以 Caddy 到 Nginx 是容器内明文 HTTP
  -> api
     - Nginx 显式使用 proxy_http_version 1.1 转发到 api:3000

所以当前 Nginx 的角色不是 TLS 终止层,而是:

  • 前端静态资源服务器
  • SPA 路由兜底层
  • /api/ 内层反向代理
  • 缓存与压缩策略执行层

2. 顶层骨架:main、events、http 在这份配置里各自做什么

main 上下文

user nginx;
worker_processes auto;

error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

这里是整个 Nginx 进程级配置:

  • user nginx;
    • worker 进程以 nginx 用户运行,避免直接用 root 常驻处理请求
  • worker_processes auto;
    • 按容器可见 CPU 核数自动决定 worker 数量
  • error_log ... warn;
    • 默认只记 warn 及以上错误,避免日志过噪
  • pid /var/run/nginx.pid;
    • 记录主进程 pid

events 上下文

events {
    worker_connections 1024;
}
  • worker_connections 1024;
    • 单个 worker 最多可处理 1024 个连接
    • 对当前 DailyUse 这种单机应用来说,这属于比较保守但够用的起点

http 上下文

http 块是这份配置的核心,因为静态资源、压缩、缓存、代理、日志都在这里。

3. http 块里的全局设置到底在解决什么

MIME 与默认类型

include /etc/nginx/mime.types;
default_type application/octet-stream;
  • include /etc/nginx/mime.types;
    • .js.css.svg、字体等常见静态资源能带上正确的 Content-Type
  • default_type application/octet-stream;
    • 如果某个文件类型没匹配到 MIME,就按通用二进制流返回

这保证了前端静态站点不会因为类型错误而触发浏览器异常解析。

Docker DNS 解析

resolver 127.0.0.11 ipv6=off valid=30s;

这行在容器化部署里非常关键。

  • 127.0.0.11 是 Docker 内置 DNS
  • valid=30s 表示解析结果缓存 30 秒
  • ipv6=off 是为了避免在当前容器网络里做无意义的 IPv6 解析

它和后面的这段配置是配套的:

set $api_upstream http://api:3000;
proxy_pass $api_upstream;

因为 proxy_pass 用了变量,Nginx 才会在运行期借助 resolver 重新解析 api
这能避免一个常见容器坑:

api 容器重建后 IP 变了
但 Nginx 还卡着旧 IP
外部持续 502

现在这份写法的预期是:

  • api 重建期间可能短暂失败
  • api 恢复后,Nginx 能重新解析 api:3000 并自动恢复

日志格式

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                '$status $body_bytes_sent "$http_referer" '
                '"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

这份日志格式偏简洁,重点记录:

  • 客户端地址
  • 请求行
  • 状态码
  • 返回体大小
  • Referer
  • User-Agent
  • 转发链里的 X-Forwarded-For

它没有像某些排障配置那样额外记录:

  • $request_time
  • $upstream_response_time
  • $upstream_status

所以它适合日常访问记录,但如果后面要更细地排查慢请求或上游超时,可以考虑临时补更强的日志格式。

4. 传输和连接相关参数

sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 20m;

sendfile on

  • 适合静态资源服务器
  • 减少静态文件发送过程中的用户态拷贝开销

这和 DailyUse 的 web 容器定位完全一致,因为它主要就是发前端构建产物。

tcp_nopush on

  • 更偏向“把响应头和文件块尽量攒整再发”
  • 配合 sendfile 对大块静态资源传输更友好

tcp_nodelay on

  • 减少小包交互延迟
  • 对 keepalive 连接上的动态响应更友好

tcp_nopushtcp_nodelay 看起来像相反策略,但在 Nginx 里通常会一起出现:
前者偏静态文件吞吐,后者偏交互延迟。

keepalive_timeout 65

  • 允许客户端和 Nginx 保持连接复用
  • 避免每个资源请求都重新建 TCP 连接

对前端站点来说,这是默认很有价值的优化,因为一个页面往往会连带请求多份静态资源。

types_hash_max_size 2048

  • 调整 MIME 类型哈希表大小
  • 避免类型映射较多时哈希表过小

这不是 DailyUse 特有配置,更像“静态站点常规整洁项”。

client_max_body_size 20m

  • 单请求体最大 20 MB
  • 可以防止默认值太小导致上传类请求触发 413 Payload Too Large

这说明当前项目已经考虑到了:

  • 文件上传
  • 富文本附件
  • 其他中等大小请求体

5. gzip 压缩在这份配置里是怎么工作的

gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
    text/plain
    text/css
    text/xml
    text/javascript
    application/json
    application/javascript
    application/xml+rss
    application/rss+xml
    font/truetype
    font/opentype
    application/vnd.ms-fontobject
    image/svg+xml;

这是当前 web 容器里负责压缩的核心配置。

gzip on

  • 开启 gzip 压缩
  • 主要收益在文本类资源:HTML、CSS、JS、JSON、SVG、部分字体

gzip_vary on

  • 响应头里会加 Vary: Accept-Encoding
  • 告诉缓存系统:同一个 URL 的 gzip 和非 gzip 响应不能混用

gzip_proxied any

  • 即使请求来自代理链路,也允许压缩响应
  • 在当前结构里,外层还有 Caddy,所以这项能避免“因为前面有代理就不压缩”

gzip_comp_level 6

  • 压缩等级取 6,属于吞吐和压缩率之间比较常见的平衡点
  • 不是压得越狠越好,因为压缩也要吃 CPU

gzip_types

这里列的是除 text/html 之外要额外压缩的类型。
text/html 不需要手工列进去,Nginx 默认就会处理。

这份列表的意图很明确:

  • css/js/json/xml/svg
  • 压字体文件
  • 不对图片、视频、压缩包做重复压缩

所以你会看到:

  • .svg 可以压
  • .png/.jpg/.gif 不在列表里

这和生产实践是一致的,因为位图图片通常本来就已经压缩过,再 gzip 收益很小。

6. WebSocket、Upgrade 与 HTTP 版本为什么要这样写

Upgrade 映射

map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

这段是为了后面 /api/ 代理时正确处理“需要升级连接”和“不需要升级连接”两种情况。

它的意思是:

  • 如果请求里带了 Upgrade,就把 Connection 设成 upgrade
  • 如果没有,就正常 close

这样同一套 /api/ 代理配置既能处理普通 HTTP 请求,也能兼容 WebSocket 升级场景。

为什么显式写 proxy_http_version 1.1

proxy_http_version 1.1;

这是 DailyUse 当前配置里最值得单独点出来的一行。

因为 Nginx 代理到上游时,默认不是最佳的现代反代形态。
显式改成 1.1 的价值有三层:

  1. 支持 keepalive
  2. 支持 chunked/更现代的传输语义
  3. 支持 WebSocket Upgrade

结合这份配置里的:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

可以把它理解成:

当前 web(Nginx) -> api 这段链路,是按“HTTP/1.1 代理 + 可升级连接”来设计的,而不是最简陋的 HTTP/1.0 转发。

这里也顺手澄清一个容易混淆的问题:

  • 浏览器到 Caddy 可能是 HTTP/2 或 HTTP/3
  • web 容器里的 Nginx 只监听 80
  • Nginx -> api 这一段是否支持长连接、Upgrade,取决于这里的 proxy_http_version 1.1

所以你看到“项目用了 HTTP/2/3”与“内层 Nginx 明确写 1.1”并不矛盾。
它们发生在链路的不同段。

7. server 块为什么长这样

server {
    listen 80;
    server_name _;
    root /usr/share/nginx/html;
    index index.html;

listen 80

  • 因为 TLS 已经在 Caddy 层终止
  • web 容器只需要提供内网 HTTP 服务

server_name _

  • 这里不是拿来做公网多域名虚拟主机
  • 本质上是“兜底 server”

因为真正的域名入口和 Host 路由已经在 Caddy 层完成,web 这里不用再复杂分流。

root /usr/share/nginx/html

  • 对应 Dockerfile.web 复制进去的前端构建产物目录
  • 说明这就是标准静态站点容器

8. 为什么 sw.jsindex.html、静态资源缓存策略不同

sw.js

location = /sw.js {
    expires -1;
    add_header Cache-Control "no-cache, no-store, must-revalidate" always;
    add_header Service-Worker-Allowed "/" always;
}

这里很谨慎,因为 Service Worker 是“缓存的总控制器”:

  • no-cache, no-store, must-revalidate
    • 避免浏览器长期缓存旧版 sw.js
  • Service-Worker-Allowed "/"
    • 明确允许它控制根路径作用域

如果 sw.js 被 aggressive cache 住,前端发布后很容易出现:

  • 用户仍在跑旧缓存逻辑
  • 页面资源版本错配
  • “我明明发版了但用户还在旧页面”

index.html

location = /index.html {
    expires -1;
    add_header Cache-Control "public, max-age=0, must-revalidate" always;
}

index.html 是前端壳文件。
它不能像带 hash 的静态资源那样长缓存,因为它负责引用最新的 JS/CSS 文件名。

site.webmanifest

location = /site.webmanifest {
    expires -1;
    add_header Cache-Control "public, max-age=0, must-revalidate" always;
}

manifest 也偏向需要及时更新,不适合走长缓存。

静态资源正则

location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
    expires 30d;
    add_header Cache-Control "public, immutable" always;
}

这里是反过来的策略:

  • 给带构建产物特征的静态资源长缓存
  • immutable 告诉浏览器:只要 URL 不变,就不用反复验证

这套组合说明当前前端缓存策略是典型的:

壳文件短缓存
资源文件长缓存
Service Worker 谨慎缓存

9. /api/ 代理块到底把哪些问题一次性处理了

location /api/ {
    set $api_upstream http://api:3000;
    proxy_pass $api_upstream;
    proxy_http_version 1.1;
    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 Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;
    proxy_buffering off;
}

这段不只是“转发一下请求”,而是一次性处理了 6 类问题。

1. 上游地址解析

  • set $api_upstream http://api:3000;
  • 配合 resolver 127.0.0.11

解决的是 Docker 容器重建后 upstream 地址变化问题。

2. HTTP 版本

  • proxy_http_version 1.1;

解决的是 keepalive、Upgrade、现代代理语义问题。

3. 原始请求上下文传递

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;

这保证 api 至少知道:

  • 原始 Host 是谁
  • 用户来源 IP 是谁
  • 当前请求原始协议是什么

不过要注意:
因为 Nginx 的上游是 Caddy,所以这里的 $scheme 指的是 Caddy 到 Nginx 这一段看到的协议,也就是 HTTP。
如果后端确实需要知道最外层用户请求是不是 HTTPS,还要结合最外层代理头一起看,而不是想当然地以为这里一定是 https

这也意味着当前写法有一个潜在副作用:

Caddy 原本知道用户是 HTTPS 进来的
但 Nginx 再往 api 转发时,可能把 X-Forwarded-Proto 改写成 http

如果未来 api 明确依赖“原始外层协议”去生成回调地址、重定向地址或安全判断,就要重新审视这行配置,而不是默认它天然正确。

4. 升级连接

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

保证 WebSocket 或其他需要 Upgrade 的请求不会被代理层截断。

5. 长请求超时

proxy_read_timeout 300s;
proxy_send_timeout 300s;

300 秒说明当前项目显式为较长的 API 处理时间留了余地。
这比默认较短超时更适合:

  • 大响应
  • 流式返回
  • 较慢的 AI/同步类接口

6. 关闭响应缓冲

proxy_buffering off;

这说明当前项目更倾向于:

  • 让响应尽快往客户端流
  • 减少 Nginx 先整块缓存再转发的行为

它对 SSE、流式输出、长连接响应更友好,但代价是少了一层“由 Nginx 吞下慢客户端”的缓冲保护。

10. / 兜底块除了 SPA 还有什么

location / {
    try_files $uri $uri/ /index.html;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}

try_files $uri $uri/ /index.html

这是 SPA 正常刷新的核心:

  1. 先找静态文件
  2. 再找目录
  3. 都没有时回到 index.html

这就避免了前端路由如 /settings/profile 刷新直接 404。

这些安全头的意义

  • X-Frame-Options SAMEORIGIN
    • 限制被其他站点随意 iframe 嵌入
  • X-Content-Type-Options nosniff
    • 禁止浏览器乱猜 MIME
  • X-XSS-Protection
    • 旧式浏览器兼容性安全头,现代浏览器价值有限,但无害
  • Referrer-Policy strict-origin-when-cross-origin
    • 限制跨站请求时 Referrer 暴露粒度

还要注意一个结构事实:

  • 这些头主要加在 / 兜底路径上
  • 外层 Caddy 也会补一部分安全头

所以当前安全头策略其实是两层协作:

  • Caddy 负责公网入口的通用头
  • web(Nginx) 负责前端站点层的补充头

验证

如果你改了 nginx.conf,验证顺序建议是:

  1. 先确认配置已进入镜像
  2. 再确认缓存头、gzip 和路由行为
  3. 最后确认 /api/ 代理没被改坏

本地构建 web 镜像后,可先看镜像内配置:

docker run --rm <your-web-image> cat /etc/nginx/nginx.conf

验证首页和缓存头:

curl -I http://127.0.0.1/
curl -I http://127.0.0.1/index.html
curl -I http://127.0.0.1/sw.js

验证 gzip:

curl -I -H "Accept-Encoding: gzip" http://127.0.0.1/assets/app.js

如果响应命中压缩,通常能看到:

Content-Encoding: gzip
Vary: Accept-Encoding

验证 /api/ 代理时,要放到完整 compose 环境里看,否则单独跑 web 容器时没有 api:3000 上游。

常见问题

为什么对外是 HTTPS,这里却只 listen 80

  • 因为 TLS 终止发生在 Caddy 层
  • web 容器只处理内网 HTTP

为什么外面可能是 HTTP/2 或 HTTP/3,这里却写 proxy_http_version 1.1

  • 因为那是链路不同段的协议
  • Browser -> CaddyNginx -> api 不是同一段

X-Forwarded-Proto $scheme 有没有坑

  • 有潜在坑
  • 因为这里的 $scheme 反映的是 Caddy -> Nginx 这一段,通常是 http
  • 如果后端以后强依赖“用户最外层是不是 HTTPS”,就要重新评估这一行

为什么已经有 Caddy 了,还要在 Nginx 里配 gzip

  • 因为当前 Caddyfile 没有把压缩作为主执行层
  • 实际压缩策略是由 web 容器里的 Nginx 控制

为什么 proxy_buffering off

  • 因为当前项目更偏向让 API 响应直接流出去
  • 这对长响应、流式输出和实时反馈更友好
创建于 2026/5/7 更新于 2026/5/27