生产环境图片访问 404 排障手记(Docker + Spring Boot + 1Panel + Nginx)

TL;DR

  • 404 的根因不是代码,而是 Docker 挂载目录导致容器内看不到宿主机文件。
  • 两种可行方案:
    • 方案 A(最终采用):Nginx 直出宿主机目录 /data/uploads
    • 方案 B:Nginx 转发到后端,后端静态资源映射读取容器内 /app/uploads(或挂载后的宿主机目录)。
  • 关键点:
    • 给 Nginx 容器挂载宿主机目录 /data/uploads:/data/uploads:ro
    • 给后端容器明确上传目录(相对路径 ./uploads/app/uploads)。
    • location 匹配优先级与 proxy_pass/alias 尾斜杠语义。

一、背景

  • 访问 URL:https://example.com/api/uploads/…/xxx.jpg
  • 目标:尽量只改 Nginx,就能访问到上传的图片。
  • 实际:访问长期 404。

二、现象 & 关键日志

  • 早期 Nginx 错误日志:

    open() "/usr/local/openresty/nginx/html/api/uploads/..." failed (2: No such file or directory)
    

    说明命中了其它 location(如 location /),没有命中我们为 /api/uploads/ 配的块。

  • 调整后错误日志:

    open() "/data/uploads/feedback/20251117/xxx.jpg" failed (2: No such file or directory)
    

    说明命中了 /api/uploads/ 的配置,但 Nginx 容器内不存在 /data/uploads(没挂载,容器看不到宿主机目录)。

  • 后端日志(用于确认后端路径解析):

    配置的上传路径: ./uploads
    绝对路径: /app/uploads/
    目录是否存在: true
    目录是否可读: true
    

    说明后端容器工作目录是 /app,相对路径 ./uploads 会解析到 /app/uploads

三、根因

  1. Nginx 容器内未挂载宿主机目录 /data/uploads,导致直出方案里 Nginx 看不到文件。
  2. 早期 location 优先级不当,或 alias/root/rewrite 配置不严谨,命中到了默认站点目录。
  3. 若走后端代理,后端容器未使用正确的上传目录(默认走 /app/uploads),或没有在容器内创建该目录。

四、解决方案

方案 A:Nginx 直出宿主机目录(最终采用)

  1. 在 1Panel 给 Nginx/OpenResty 容器新增挂载:
  • 主机目录:/data/uploads
  • 容器目录:/data/uploads
  • 权限:只读
  1. Nginx 配置(必须放在 location /api/location / 之前,并使用 ^~ 提升优先级):
location ^~ /api/uploads/ {
    alias /data/uploads/;           # 末尾斜杠必须有
    try_files $uri =404;            # 文件不存在立即 404,不回退到其他 location

    expires 30d;
    add_header Cache-Control "public, immutable";
    add_header Access-Control-Allow-Origin *;
}
  • 也可用 rewrite + root 等价写法(任选一种,勿混用):
location ^~ /api/uploads/ {
    rewrite ^/api/uploads/(.*)$ /$1 break;  # 去掉前缀
    root /data/uploads;                     # 结果路径:/data/uploads/<相对路径>
    try_files $uri =404;

    expires 30d;
    add_header Cache-Control "public, immutable";
    add_header Access-Control-Allow-Origin *;
}
  1. 重载并验证:
nginx -t && nginx -s reload
# 验证容器可见性(在 Nginx 容器内)
docker exec <nginx容器名> ls -lh /data/uploads/feedback/20251116/
# 访问验证(先用确认存在的老文件)
curl -I "https://example.com/api/uploads/feedback/20251116/xxxx.jpg"

适用场景:不经后端,降低后端负载,直接由前端网关分发静态文件。


方案 B:代理到后端静态资源(后端映射到容器内目录或宿主机)

  1. Nginx 将 /api/uploads/ 转发为后端的 /uploads/
location ^~ /api/uploads/ {
    proxy_pass http://127.0.0.1:8080/uploads/;  # 注意最后的斜杠!
    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_redirect off; proxy_buffering off;
    expires 30d; add_header Cache-Control "public, immutable";
}
  1. 后端 Spring Boot 静态资源映射(兼容 /uploads/**/api/uploads/**):
  • WebConfig.addResourceHandlers() 增加:
    • registry.addResourceHandler("/uploads/**", "/api/uploads/**").addResourceLocations("file:/app/uploads/")(示例)
    • 规范化路径:使用 getCanonicalPath() 处理 ...
    • 启用诊断日志,打印“配置的上传路径 / 绝对路径 / 目录是否存在”等信息
  1. 后端上传目录:
  • application-prod.yml
    file:
      upload:
        path: ./uploads         # 相对 /app
        url-prefix: https://example.com/api/uploads
    
  • 在后端容器内创建目录:mkdir -p /app/uploads
  1. 验证链路:
  • 查看后端日志是否打印“目录存在 true、可读 true”
  • 访问 https://example.com/api/uploads/...,命中后端静态资源

适用场景:需要统一由后端鉴权、限流、审计或自定义资源逻辑时。


五、常见坑与避坑要点

  • location 优先级:location ^~ /api/uploads/ 必须在 location /api/location / 之前。
  • alias 末尾斜杠:alias /data/uploads/; 尾斜杠必须有,否则拼接路径错误。
  • proxy_pass 尾斜杠:proxy_pass http://127.0.0.1:8080/uploads/; 会把 /api/uploads/... 改写到 /uploads/...;如果少了斜杠会保留原路径。
  • 容器可见性:直出方案需要把宿主机目录挂载到 Nginx 容器;后端方案需要容器内存在 /app/uploads(或映射到宿主机目录)。
  • 默认根目录误命中:未命中专用 location 时会落到默认站点根目录(如 /usr/local/openresty/nginx/html),导致路径不对。
  • 验证顺序:
    1. 先在容器内 ls 确认文件可见。
    2. nginx -t && nginx -s reload
    3. 最后 curl -I 直连 URL 验证。

六、最终结论

  • 本次采用“方案 A:Nginx 直出宿主机目录”。
  • 在 1Panel 中为 Nginx 容器新增 /data/uploads:/data/uploads:ro 挂载后,配合 aliastry_files,访问恢复正常。
  • 同时,保留后端双路径映射与日志,便于后续排障与回退。

七、附录:配置片段汇总

  • Nginx 直出(最终使用):
location ^~ /api/uploads/ {
    alias /data/uploads/;
    try_files $uri =404;
    expires 30d;
    add_header Cache-Control "public, immutable";
    add_header Access-Control-Allow-Origin *;
}
  • Nginx 转发后端:
location ^~ /api/uploads/ {
    proxy_pass http://127.0.0.1:8080/uploads/;  # 注意尾斜杠
    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_redirect off; proxy_buffering off;
    expires 30d; add_header Cache-Control "public, immutable";
}
  • 后端 application-prod.yml(示例):
file:
  upload:
    path: ./uploads
    url-prefix: https://example.com/api/uploads
  • 验证命令:
# 验证容器能否看到文件
docker exec <nginx容器名> ls -lh /data/uploads/feedback/20251116/

# 检查 Nginx 配置 & 重载
nginx -t && nginx -s reload

# 访问验证
curl -I "https://example.com/api/uploads/feedback/20251116/xxxx.jpg"