# Novalon 站点边缘配置(HTTP→HTTPS + HSTS + gzip + limit_req + 反代 standalone 应用) # # ⚠️ 这是一段 **http 上下文 include 片段**(放进 conf.d/ 或被主配置 include), # 不是完整的 nginx.conf:它没有 events{}/http{} 外壳。 # 历史缺陷:旧版 Dockerfile 与 docker-compose.yml 把本文件 COPY/MOUNT 到 # /etc/nginx/nginx.conf,而本文件只有裸 server 块 → 直接 nginx -t 即失败; # 需要完整生产网关(含 gitea / jenkins 虚拟主机)时用 nginx-static-production.conf。 # # 头部分工(唯一来源,勿在此叠加): # - 文档级安全头 CSP / X-Frame-Options / X-Content-Type-Options / Referrer-Policy / # Permissions-Policy / X-XSS-Protection / X-DNS-Prefetch-Control 全部由 # next.config.mjs 的 headers() 随应用一起发布;本文件不再 add_header 这些名字。 # 原因:nginx add_header 不会替换上游同名头部,只做追加,两层各设一次就会让浏览器收到 # `X-Frame-Options: DENY, SAMEORIGIN` 这类非法的重复/互斥值(验收 §5 确认的线上缺陷)。 # - 本文件只保留确实属于边缘的东西:TLS 与 301、HSTS(Next 不发送,且只有终结 TLS 的 # 一层知道协议与证书状态)、gzip、limit_req,以及应用未设置的 Content-Disposition。 # - 页面不再由 nginx 直服静态 HTML:output 为 standalone 后,HTML 由 Next 运行时 # (ISR/SSR/Draft)产出,静态资源与 /uploads/ 仍可由磁盘直供(见下,缺文件即 404)。 limit_req_zone $binary_remote_addr zone=novalon_page:10m rate=50r/s; upstream novalon_app { # 应用与 nginx 同机(systemd / 端口发布)时用 127.0.0.1; # 与 docker-compose.yml 同网络时改为容器名:server novalon-website:3000; server 127.0.0.1:3000; keepalive 32; } server { listen 80; server_name novalon.cn www.novalon.cn; # ACME http-01 challenge 必须留在 80 且不能被跳转吞掉 location /.well-known/acme-challenge/ { root /usr/share/nginx/html; try_files $uri =404; } location / { return 301 https://www.novalon.cn$request_uri; } } server { listen 443 ssl http2; server_name novalon.cn www.novalon.cn; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; # 边缘自有头部:Next 不发送 HSTS(它无法得知终结点使用的协议),故只在这里设置 add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; # 注意:以下 location 均含 add_header,按 nginx 语义不再继承 server 级头部, # 故每块各自重述 HSTS;应用级头部一律不在此重述(由 next.config.mjs 负责)。 # 预构建产物可用磁盘直服时启用(root 需与部署侧同步目录一致); # 未配置磁盘副本时删除本块即可,全部流量交给 novalon_app。 location /_next/static/ { alias /var/www/novalon/_next/static/; expires 1y; # Cache-Control 由 Next 官方声明为「immutable 资源不可覆盖」,此处仅补充磁盘直服 # 路径(Next 未应答)所需的缓存语义,与应用头部不冲突、不同名重复。 add_header Cache-Control "public, max-age=31536000, immutable" always; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; try_files $uri @novalon_proxy; } # 本地字体(约束:字体全部本地文件,禁外部 CDN) location /fonts/ { alias /var/www/novalon/fonts/; expires 1y; add_header Cache-Control "public, max-age=31536000, immutable" always; add_header Access-Control-Allow-Origin "*"; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; try_files $uri @novalon_proxy; } # 媒体库接口可签发短期签名 URL:需要直取文件时优先走下面 location / 的反代; # 本块仅在存在磁盘副本(如 CDN 回源目录)时生效。 location ~* \.(jpg|jpeg|png|gif|webp|avif|ico)$ { root /var/www/novalon; expires 1y; add_header Cache-Control "public, max-age=31536000, immutable" always; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; try_files $uri @novalon_proxy; } # 上传件:应用自身对 /uploads/ 只发 nosniff/CSP(同源落点风险), # 文档与压缩包在这里强制下载。Content-Disposition 应用未设置 ⇒ 不构成跨层重复。 location /uploads/ { limit_req zone=novalon_page burst=20 nodelay; proxy_pass http://novalon_app; 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; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; location ~* /uploads/.*\.(pdf|zip|docx?|xlsx?|pptx?)$ { add_header Content-Disposition "attachment" always; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; } } # 页面 / API / 管理后台 / 缺文件的静态资源:交给 Next standalone 运行时, # 应用头部(含 CSP/XFO)随响应原样透传。 location / { limit_req zone=novalon_page burst=20 nodelay; proxy_pass http://novalon_app; 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 X-Forwarded-Host $host; # 复用 upstream keepalive:Connection 置空。生产 standalone 无 HMR/WebSocket, # 若将来需要 Upgrade 透传,请像 nginx-static-production.conf 那样加 $connection_upgrade map。 proxy_set_header Connection ""; proxy_hide_header X-Powered-By; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; } # 静态资源磁盘副本未命中时的回源出口 location @novalon_proxy { proxy_pass http://novalon_app; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; } gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_min_length 256; gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml application/rss+xml image/svg+xml; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; }