Files
danyang-match/infrastructure/nginx-production.conf
WorkBuddy 797b1c46cf refactor(storage): 对象存储从 MinIO 换成 SeaweedFS(钉 4.47)
动机:MinIO 的 GitHub 仓库已于 2026-02 归档,不再产出安全补丁,已累积多个
CVE 且不修;本项目存身份证 / 实名材料 / 用户照片,无人修 CVE 才是真正驱动因素
(Docker Hub 下架只是表象,quay.io 仍在提供)。

变更:
- 三套 compose(dev / staging / prod)的 minio 服务替换为 seaweedfs 4.47:
  覆盖 entrypoint 渲染 -s3.config 注入 identity(S3 网关默认不鉴权,必须显式开启),
  -volume.max=100 解除第二个 bucket 写入时的 InternalError。
- 新增一次性 storage-init 容器,经 filer 接口预建 public / private 桶
  (SeaweedFS PutObject 到不存在的 bucket 直接失败;全仓库原无任何 createBucket)。
- S3Config 显式开启 path-style:AWS SDK v1 默认 virtual-host 会把 bucket 拼进
  主机名,容器名在 DNS 中不存在必然失败(本机有 http_proxy 时易误判为 502 拒绝)。
- ci.yml:镜像校验与起栈步骤改为 seaweedfs + storage-init。
- 运维脚本 health-check.sh / run-uat.sh 的 MinIO 探针改为 SeaweedFS
  (S3 网关开启鉴权后匿名根请求返回 403,200/403 均算存活)。

注:path-style 与建桶属换存储的必要项;presigned URL 的 host 须对外可达这一
既有缺陷(原 minio:9000 直发客户端不可达)留待下一轮「改缺陷」处理。
2026-09-22 14:26:48 +08:00

188 lines
7.2 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Production Nginx Configuration
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 2048;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format production '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'rt=$request_time';
access_log /var/log/nginx/access.log production;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=50r/s;
limit_req_zone $binary_remote_addr zone=auth:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=upload:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone=ws:10m rate=20r/s;
# Maintenance mode flag
map $http_x_maintenance_mode $maintenance_mode {
default 0;
"1" 1;
}
upstream backend {
server backend:8080;
}
# 管理端容器(同一 danyang-net 网络,服务名 admin-web —— 见 docker-compose.prod.yml)。
# 本容器是 nginx:alpine 纯净镜像,自身不含管理端产物,只能靠代理取。
upstream admin_web {
server admin-web:80;
}
# HTTP -> HTTPS redirect (uncomment for production)
# server {
# listen 80;
# server_name match.danyang.gov.cn;
# return 301 https://$server_name$request_uri;
# }
server {
listen 443 ssl http2;
server_name match.danyang.gov.cn;
# SSL
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "DENY" 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;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' wss:;" always;
# Maintenance mode
location = /maintenance {
return 503 '<h1>系统维护中</h1><p>请稍后再访问。</p>';
}
# 根路径没有内容(管理端在 /admin/ 下,小程序走 /api/)。
# 显式 404:当前之所以是 404,只是因为本配置没有 server 级 root,
# 而 nginx 默认的 html 目录(/etc/nginx/html)不存在 —— 属偶然事实。
# 一旦有人为了别的用途补上 root,根路径就会开始返回那个目录下的内容。
# 用精确匹配钉住这一行为,且不影响其它未匹配路径的既有处理。
location = / {
return 404;
}
# Health checks (no auth needed)
location = /api/v1/health/live {
proxy_pass http://backend;
proxy_set_header Host $host;
}
location = /api/v1/health/ready {
proxy_pass http://backend;
proxy_set_header Host $host;
}
location = /version {
proxy_pass http://backend;
proxy_set_header Host $host;
}
# Admin Web (SPA) —— 子路径部署在 /admin/ 下(构建 base=/admin/,见 admin-web/vite.config.ts)。
#
# 2026-09-18 修正:此处原为 `alias /usr/share/nginx/html/;`,而本容器是 nginx:alpine
# 纯净镜像 —— 那个目录里只有镜像自带的欢迎页,并没有管理端产物。结果是访问 /admin/
# 返回 200 + nginx 欢迎页,属于"假成功",比 404 更难被发现(页面能打开,但不是管理端)。
# 真正的产物在 admin-web 容器内,故改为代理到 upstream。
#
# 代理**不剥离** /admin 前缀:管理端构建 base 为 /admin/,其 index.html 引用的资源
# 本身就以 /admin/ 开头,由 admin-web 容器内的 nginx(admin-web/nginx-admin.conf)
# 统一处理这个前缀,三处入口(生产 / 本地整套栈 / staging 直连)因此形态一致。
#
# 暴露面提醒:管理端登录页不应无条件对公网开放。当前依赖外层防火墙/WAF 控制;
# 若需在 nginx 层收敛,在此 location 内加 allow/deny 白名单(运营办公网段 + VPN)。
location = /admin {
return 301 /admin/;
}
location /admin/ {
# 只写 upstream 名,不写端口:upstream 块里已声明 `server admin-web:80;`,
# 此处再带 :80 会让 nginx 报 `upstream "admin_web" may not have port 80` 而拒绝启动。
proxy_pass http://admin_web;
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 proxy
location /api/ {
limit_req zone=api burst=100 nodelay;
proxy_pass http://backend;
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_connect_timeout 30s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# WebSocket proxy
location /ws/ {
limit_req zone=ws burst=20 nodelay;
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 3600s;
}
# Auth endpoints (strict rate limiting)
location ~* ^/api/v1/auth/ {
limit_req zone=auth burst=5 nodelay;
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Upload endpoints
location ~* ^/api/v1/photos/upload {
limit_req zone=upload burst=5 nodelay;
client_max_body_size 10M;
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# SeaweedFS public bucket (read-only)
location /uploads/ {
proxy_pass http://backend/;
proxy_set_header Host $host;
expires 7d;
add_header Cache-Control "public, immutable";
}
}
}