我想从服务器上下载一份 PDF,点开链接,浏览器直接 404。链接我记着,IP 也一样,路径没错。

ssh 上去看一眼:

ls -l /opt/shortlink/public/download/
-rw-r--r-- 1 ubuntu ubuntu 284713 Oct 15 09:12 book-list-0-12.pdf
-rw-r--r-- 1 ubuntu ubuntu 301244 Oct 15 10:03 0-12岁分级阅读书单.pdf

文件在,权限是 644,时间戳也对。那就不是磁盘的事,是 nginx 的事。

先确认请求到底落在哪个 server 块

浏览器 F12 看响应头只能看到 Server: nginx/1.22.1,不够。得在服务器上复现,而且要走同一份配置:

curl -skI -H "Host: hldip.com" https://127.0.0.1/download/book-list-0-12.pdf

拿到的是:

HTTP/1.1 404 Not Found
Server: nginx/1.22.1
Content-Type: text/html

-s 静默,-k 跳过证书校验,-H "Host: ..." 手动指定域名让 127.0.0.1 能命中正确的 server 块——这三点一个都不能少。少一个你测的就是另一个站点。

返回 404 说明请求确实打到了这台机器,是 nginx 内部决定不给。接着把展开后的完整配置打出来:

sudo nginx -T 2>/dev/null | grep -n 'location'

-T 比 cat /etc/nginx/sites-enabled/* 靠谱,它会把所有 include 进来的文件、连同默认站点一起展开。grep 结果里出现了两行:

239:    location ^~ /download/ {
240:        root /var/www/downloads;
241:        autoindex on;
242:    }
296:    location ^~ /download/ {
297:        auth_basic off;
298:        alias /opt/shortlink/public/download/;
299:    }

两段都是 ^~ /download/。nginx 启动时一声不吭,systemctl reload 也照样成功——它不做重复检测。

第一段是更早时候部署另一个应用留下的,那套东西我早就删干净了,nginx 配置却忘了清。于是文件在 /opt/shortlink/public/download/,nginx 拿着请求去 /var/www/downloads/ 找,那个目录现在压根不存在。

nginx 到底按什么顺序挑 location

不是"谁写在前面谁赢",是固定优先级:

  1. location = /path —— 精确匹配,命中立刻返回,不再往下看
  2. location ^~ /prefix —— 前缀匹配里最长的一条,命中后跳过正则匹配
  3. location ~ /re、location ~* /re —— 正则,按配置文件里出现的顺序,第一个命中的生效
  4. 普通前缀 location /prefix —— 最长的那条
  5. location / —— 兜底

第 2 条里那句"跳过正则"是重点。我主站里有一段 location ~* ^/(Tabs|Profile|Groups|...),靠的就是普通前缀匹配完还要过一遍正则。如果这段前面加了 ^~,这个正则就永远不会执行了。

而我这次的情况更朴素:两段一模一样的 ^~ /download/,长度一样。nginx 对完全相同优先级的情况,取配置文件里先出现的那段,也就是 239 行。

修的时候不是"再加一段覆盖掉",nginx 没有这个语义——同优先级的后一段压根不会被读到。正确做法是把旧的删掉:

sudo nano /etc/nginx/sites-enabled/hldip.com

删掉 239–242 这段,保存,然后:

sudo nginx -t && sudo systemctl reload nginx

nginx -t 必须过。它不过的话 reload 就是空操作,我会误以为已经生效了,然后白折腾半小时。reload 走的是平滑重载,不断已有连接,比 restart 安全。

改完再验:

curl -skI -H "Host: hldip.com" https://127.0.0.1/download/book-list-0-12.pdf | head -3
HTTP/1.1 200 OK
Server: nginx/1.22.1
Content-Type: application/pdf

顺便说下 alias 和 root 到底差在哪

修完那段我顺手统一成了 alias,然后想起另一个坑。这俩的拼接规则完全不一样。

root 是拼接:

location /download/ {
    root /opt/shortlink/public;
}

请求 /download/a.pdf → 找 /opt/shortlink/public + /download/a.pdf。

alias 是替换,把 location 那段前缀整个换掉:

location /download/ {
    alias /opt/shortlink/public/download/;
}

请求 /download/a.pdf → 前缀 /download/ 被替换成右边整条,结果 /opt/shortlink/public/download/a.pdf。

效果一样,但 alias 少一次拼接。写静态目录我一般用 alias,读起来意图更直白——右边那行就是文件真正所在。

坑在 alias 的替换是纯字符级的,末尾斜杠必须对齐。我有过一次配置写成 location /download(不带斜杠)+ alias /opt/shortlink/public/download/,请求 /download/a.pdf 拼出来是 /opt/shortlink/public/downloada.pdf,少一个斜杠,直接 404。规律很简单:location 结尾有没有斜杠,跟 alias 右边,严格保持一致就行,两边都带或者都不带。

我现在固定用的验证命令

curl -skI -H "Host: 域名" https://127.0.0.1/路径 | head -1
curl -sk  -H "Host: 域名" https://127.0.0.1/路径 | grep -c "内容里的关键词"
sudo nginx -T | grep -n 'location'

第二条的 grep -c 那个数字必须是 1。返回 0 说明页面在但内容没更新,返回 1 才是真的换上了。

有一点要特别提醒:别用 curl https://blog.hldip.com/... 来测本机配置。那是绕出去又回来,测到的是 DNS 和公网链路,不是你眼前这份 nginx 配置。本机测就 127.0.0.1 + -H "Host:"。


nginx 可配的项不多,但 location 这套优先级规则藏在文档深处,而且重复定义不报错、命中哪段也不提示。踩过这一次之后,我每加一段 location,都会顺手 grep 一遍有没有重名的前缀。