kirin.ceo

home / notes / one line of nginx

Note

一行 nginx 設定讓整個網站變成下載檔

nginx 的 types 區塊不是「附加」,是「取代」。我知道這件事的方式,是讓正式站掛了三分鐘。

日期
2026-08(事故當日)
環境
nginx,靜態站,正式環境
影響
約 3 分鐘,全站 HTML 的 Content-Type 錯誤
限制
這是一次事故紀錄,不是 nginx 設定教學。你的版本與 include 順序可能不同,照抄前請先在隔離環境測

我想做的事

我在做內容協商:讓同一個網址在 Accept: text/markdown 時回傳 Markdown 版本。為此需要讓 nginx 知道 .md 的 MIME type。

所以我在 server 區塊裡加了這個:

types {
    text/markdown  md;
}

重載,然後首頁開始下載。

為什麼

nginx 的 types 指令在同一個 context 裡是取代整張表,不是往上面加一列

原本從 http 層繼承下來的 include mime.types;(幾百種對應)在這個 server 區塊裡整份消失,只剩下我寫的那一列。所有不是 .md 的檔案——包含每一個 .html——都掉進 default_type,也就是 application/octet-stream

application/octet-stream 對瀏覽器的意思是「下載我」。

正確的寫法

types {
    include /etc/nginx/mime.types;   # 先把繼承的整張表補回來
    text/markdown  md;               # 再加自己要的
}
心智模型的錯誤在這裡:我以為 types 是「宣告一個對應」,實際上它是「宣告這個 context 的完整對應表」。同樣的陷阱在 nginx 裡不只一個——add_header 也是:子 context 只要出現一次 add_header,父層的所有 add_header 就全部失效。

我改變的工作方式

這次事故真正讓我改掉的不是那行設定,是我測試設定的方式。

  1. nginx -t 通過不代表行為正確。那行設定語法完全合法,測試也通過。它只是做了跟我想的不同的事。
  2. 先在隔離的 server 上試。我現在會另外開一個只綁 127.0.0.1:8899 的 server 區塊,把要改的設定放進去,用 curl 打它、看 Content-Type,確認之後才動正式的區塊。
  3. 驗證要看回應標頭,不是看頁面有沒有出來。如果我當時只用瀏覽器看首頁,我會看到下載對話框而不知道原因;curl -I 一秒就指出是 Content-Type。

還原時踩到的第二個坑

我想從備份目錄復原,指令大概是這個形狀:

sudo cp /root/nginx-backup-*/site.conf /etc/nginx/sites-enabled/

這個指令不會照我想的做。萬用字元由我的 shell 展開,而我的使用者讀不到 /root/,所以展開失敗,字串原樣傳給 cp

要讓展開發生在 root 身分底下,得把整段交給一個 root 的 shell:

sudo sh -c 'cp /root/nginx-backup-*/site.conf /etc/nginx/sites-enabled/'

在正常的日子這只是個小知識。在還原正式站的時候,它是「為什麼我的還原沒有生效」多出來的一分鐘。

結論

這件事沒有什麼深刻的教訓,就是一個具體的陷阱加上一個具體的習慣改變。我把它寫下來,是因為我確定自己三個月後會忘記 types 是取代不是附加。