一、问题现象

项目在本地开发环境一切正常,上传头像、简历等文件后能立即显示。
部署到云服务器(Ubuntu + Docker + Nginx)后,前端界面能正常访问,API能正常登录、返回数据,但所有上传的图片/文件均无法访问(404 Not Found)

环境信息:

  • 后端:ASP.NET Core 6/8(Docker容器)

  • 前端:Vue/React(Nginx容器,同时担任反向代理)

  • 部署方式:Docker Compose

  • 网络模式:容器间通过服务名通信(backend:8000


二、排查过程(完整实录)

第一步:确认文件是否真的上传到了容器内部

bash

docker exec -it portfolio-backend /bin/bash
ls -la /app/wwwroot/uploads

输出结果:

text

-rw-r--r-- 1 root root 101117 Jun 29 03:27 test.jpg

结论:文件确实存在,不是上传失败的问题。


第二步:绕过Nginx,直接访问后端容器

bash

docker run --rm --network container:portfolio-backend appropriate/curl --head http://127.0.0.1:8000/uploads/test.jpg

返回结果:

text

HTTP/1.1 200 OK
Content-Type: image/jpeg
Server: Kestrel

结论:后端ASP.NET Core应用程序能够正常提供静态文件服务,问题出在Nginx代理层。


第三步:检查Nginx容器能否连通Backend

bash

docker exec -it portfolio-frontend ping backend -c 2

输出:

text

PING backend (172.19.0.2): 56 data bytes
64 bytes from 172.19.0.2: seq=0 ttl=64 time=0.138 ms

结论:容器间网络通信正常,DNS解析也无问题。


第四步:检查Nginx配置,发现致命隐患

原Nginx配置(精简版):

nginx

# 反向代理后端API
location /api/ {
    proxy_pass http://backend:8000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

# 反向代理后端静态文件
location /uploads/ {
    proxy_pass http://backend:8000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

# 前端静态资源缓存策略
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
    expires 30d;
    add_header Cache-Control "public, immutable";
}

问题根源

  • Nginx 的 location 匹配规则中,正则表达式(~ 和 ~*)的优先级高于普通前缀匹配

  • 当请求 /uploads/test.jpg 时:

    • 先被 location ~* \.(jpg|...)$ 命中(因为它是正则匹配)

    • Nginx 尝试在 Nginx容器自身的 root 目录下找 /uploads/test.jpg,而不是转发给 Backend

    • 文件不存在 → 404


三、解决方案(两种方式)

✅ 方案一:使用 ^~ 修饰符提升优先级(推荐)

nginx

# 反向代理后端静态文件(^~ 强制优先于正则)
location ^~ /uploads/ {
    proxy_pass http://backend:8000;
    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;
}

原理^~ 告诉Nginx,只要路径以 /uploads/ 开头,就直接命中该规则,不再向下匹配任何正则表达式


❌ 方案二:删除正则缓存规则(不推荐)

如果删除 location ~* \.(js|css|...)$,确实能解决问题,但会导致前端静态资源(JS/CSS/字体等)失去缓存能力,严重拖慢页面加载速度,不推荐


四、技术点解析:Nginx Location 匹配优先级(核心)

Nginx 的 location 匹配顺序(由高到低):

优先级匹配类型示例
1精确匹配location = /path
2^~ 修饰的字符串匹配location ^~ /uploads/
3正则表达式匹配(按顺序)location ~ \.jpg$ 或 location ~* \.(jpg|png)$
4普通字符串匹配(最长优先)location /api/ 或 location /

关键点:正则匹配(~)的优先级高于普通字符串匹配(无修饰符)。
所以 /uploads/test.jpg 会因为匹配到 .jpg 的正则,而忽略 location /uploads/


五、完整 Nginx 配置(生产环境推荐)

nginx

server {
    listen 8888;
    server_name your-domain.com;

    root /usr/share/nginx/html;
    index index.html;

    # Vue/React 路由(SPA)
    location / {
        try_files $uri $uri/ /index.html;
    }

    # API反向代理
    location /api/ {
        proxy_pass http://backend:8000;
        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;
        client_max_body_size 50m;
    }

    # 用户上传文件代理(^~ 强制优先)
    location ^~ /uploads/ {
        proxy_pass http://backend:8000;
        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_buffering off;                # 大文件流式传输
        client_max_body_size 50m;            # 上传文件大小限制
    }

    # 前端静态资源缓存(正则匹配,仅作用于前端自身文件)
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
        # 注意:由于 ^~ /uploads/ 的存在,这里不会劫持 /uploads/ 下的请求
    }

    # Gzip 压缩
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
    gzip_min_length 1000;
}

六、经验总结

  1. Nginx Location匹配顺序是“看不见的坑”
    不要想当然地认为 /uploads/ 和 \.jpg$ 会按书写顺序生效。必须熟悉匹配优先级规则。

  2. ^~ 是解决“前缀 vs 正则”冲突的首选
    当你想让某个路径优先于所有正则匹配时,^~ 是最干净、最安全的方案。

  3. 保持“前端静态资源”和“后端上传文件”分离
    前端的 JS/CSS 适合长期缓存,后端的用户头像/文件需要实时访问,两者在Nginx中应分别配置,各司其职。

  4. Docker环境下的排查技巧

    • 用 docker exec 进入容器直接看文件是否存在

    • 用 docker run --rm --network container:xxx 临时工具容器测试端口连通性

    • 用 docker logs 查看Nginx访问日志和错误日志


七、彩蛋:两个独立项目共用 uploads 目录会冲突吗?

如果你的服务器上部署了两个独立的ASP.NET Core项目,且都通过Nginx代理,但端口不同(例如 8888 和 1314),那么完全不会有冲突

因为Nginx会先根据 listen 端口分发请求,不同端口的 server 块相互隔离,即使两个块里都有 location ^~ /uploads/,也各自代理到不同的后端容器,互不干扰。


八、相关命令速查表

用途命令
查看容器内文件docker exec -it 容器名 ls -la /app/wwwroot/uploads
容器间网络连通性测试docker exec -it nginx容器 ping backend容器名
在Nginx容器内测试后端接口docker exec nginx容器 curl -I http://backend:8000/uploads/test.jpg
查看Nginx访问日志docker logs nginx容器名 --tail 50
重载Nginx配置docker exec nginx容器名 nginx -s reload
测试Nginx配置文件语法docker exec nginx容器名 nginx -t

希望这篇记录能帮助到遇到同样问题的开发者。技术分享,共同进步!🚀


如果对你有帮助,欢迎点赞、收藏、转发,也可以在评论区交流你的排坑经历~

更多推荐