DailyUse 项目中的 Nginx 配置解析
结合 DailyUse 当前生产 nginx.conf,解释 web 容器内 Nginx 的角色、HTTP 版本、gzip、缓存、反向代理与 Docker DNS 解析细节。
[!info] related notes
DailyUse 项目中的 Nginx 配置解析
目标
- 把 DailyUse 当前
web容器内这份nginx.conf按运行时职责讲清楚,而不是只停留在“它能代理/api”。 - 说明这份配置为什么这样写,尤其是 HTTP 版本、gzip、缓存、Docker DNS 与 WebSocket/Upgrade 相关细节。
前置条件
- 这篇笔记讲的是 DailyUse 当前项目里的具体配置,不是通用 Nginx 指令大全。
- 当前生效来源是本机仓库根目录的
nginx.conf,由Dockerfile.web打进dailyuse-web镜像。 - 如果你先想理解整套部署链路,先看 DailyUse 项目单机部署实战(Docker + Caddy + Nginx + PowerSync)。
步骤
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 进程以
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 内置 DNSvalid=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_nopush 和 tcp_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 的价值有三层:
- 支持 keepalive
- 支持 chunked/更现代的传输语义
- 支持 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.js、index.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 正常刷新的核心:
- 先找静态文件
- 再找目录
- 都没有时回到
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,验证顺序建议是:
- 先确认配置已进入镜像
- 再确认缓存头、gzip 和路由行为
- 最后确认
/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 -> Caddy和Nginx -> api不是同一段
X-Forwarded-Proto $scheme 有没有坑
- 有潜在坑
- 因为这里的
$scheme反映的是Caddy -> Nginx这一段,通常是http - 如果后端以后强依赖“用户最外层是不是 HTTPS”,就要重新评估这一行
为什么已经有 Caddy 了,还要在 Nginx 里配 gzip
- 因为当前
Caddyfile没有把压缩作为主执行层 - 实际压缩策略是由
web容器里的 Nginx 控制
为什么 proxy_buffering off
- 因为当前项目更偏向让 API 响应直接流出去
- 这对长响应、流式输出和实时反馈更友好