🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

介紹

在使用 Docker Compose 將 node_modules 管理為命名卷的環境中,執行 docker compose build 然而遇到無法找到套件的問題。

本文旨在以實際發生的情況為基礎,解釋命名卷的運作方式,使讀者能夠確定原因並找到解決方案。

發生的事象

在 Docker Compose 中進行開發,在切換分支後,執行 docker compose up -d --build 進行重建。
雖然新增加了庫,但是由於 Dockerfile 中有 pnpm install,我以為應該不會有問題。

但是,啟動後立即發生了以下錯誤。

Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@ai-sdk/google'
imported from /app/src/someModule.ts
    at packageResolve (node:internal/modules/esm/resolve:767:81)
    ...

咦,為什麼!重建後卻找不到!

因為在 Dockerfile 中執行了 pnpm install,所以應該所有在最新的 package.json 中列出的套件都已經安裝。
可是卻說「找不到」。這個原因在於 命名卷

本文將討論的內容與不討論的內容

將討論的內容

  • 命名卷的初始化規則及 node_modules 不更新的原因
  • docker compose up -d --build 的內部流程
  • 解決方法

不討論的內容

  • Docker 或卷本身的基礎說明
  • 綁定掛載與命名卷的使用區分詳細

Docker 的卷

Docker 的卷是一種獨立於容器生命周期而持久化數據的機制。
即使刪除容器,卷中的數據仍會保留。

卷主要有以下兩種類型:

  • 綁定掛載:將主機的目錄掛載到容器中
  • 命名卷:Docker 管理的具名儲存區域

有關卷的詳細解釋,請參考以下文章,該文章非常易於理解。

本文將專注於解釋命名卷如何對待 node_modules

docker compose up -d --build 的內部流程

docker compose up -d --build 的概念性流程如下(實際上如果沒有變更,create/recreate 可能會被跳過)。

1. 建構(建立映像)
   → 執行 Dockerfile
   → pnpm install 將寫入映像內的檔案系統
   → 此時暫不考慮卷

2. create/recreate(必要時)
   → 將命名卷掛載到容器
   → 如果卷不存在則新建
   → 如果卷已存在則直接使用現有的

3. 啟動(容器啟動)
   → 執行 command(或 CMD)

這裡重要的是,在 build 階段,卷尚不相關

在 Dockerfile 的 pnpm install 中安裝的套件僅寫入映像內的檔案系統。
卷的掛載發生在之後的 create/recreate 階段。

換句話說,不管 build 階段安裝了多少最新的套件,一旦在 create/recreate 階段掛載了現有的卷,映像側的 node_modules 就會被隱藏。

命名卷的初始化規則

在本次的 docker-compose.yml 中,我們將 node_modules 掛載為命名卷。

volumes:
  - app_node_modules:/app/node_modules

命名卷將映像中的內容複製的時機有明確的規則。

卷的狀態 會發生什麼
空(第一次) 映像內的 node_modules 被複製到卷中
有內容(第二次及以後) 卷中的內容將被直接使用(映像側完全忽略)

在預設行為中,只有在第一次掛載空的命名卷時,映像內的內容才會被複製到卷中(可透過 volume-nocopy 禁用)。

自第二次以後,無論映像重建多少次,卷中的內容將保持不變。
這就是為什麼即使執行 docker compose build 之後,新套件也不會反映的原因。

為什麼使用命名卷

為什麼要將 node_modules 設為命名卷呢?

在開發環境中,使用綁定掛載將主機的源代碼同步到容器中。

volumes:
  - ./:/app                                # 綁定掛載(同步整個專案)
  - app_node_modules:/app/node_modules     # 命名卷(保護 node_modules)

綁定掛載將主機的目錄直接掛載到容器中。
這裡問題在於綁定掛載的「覆蓋(obscure)」動作。

Docker 官方文檔中也有這樣的說明。

如果你將檔案或目錄綁定掛載到容器中已存在檔案或目錄的目錄中,原存在的檔案會被掛載遮蔽。
(進行綁定掛載時,容器內原有的檔案將被隱藏)
引用: 綁定掛載 | Docker Docs

也就是說,即便在 Dockerfile 中執行 pnpm install 並將套件安裝到 /app/node_modules,一旦掛載了主機的目錄,容器內的 node_modules 就會被隱藏。

如果主機側沒有 node_modules,則會變成空。如果直接在 macOS 上安裝的 node_modules 在容器(Linux)中使用,可能會因為作業系統差異而導致錯誤。

使用命名卷,可以保護 node_modules 不被綁定掛載覆蓋。
這樣能保持在容器內使用 pnpm install 安裝的 Linux 版 node_modules

不過,這樣的副作用就是會引發如本次如此「卷中的內容保持過舊」的問題。

切換分支造成問題的時間序列

總結到這裡的內容,實際上觀察一下切換分支時問題的發生過程。

1. 在分支A 中首次執行 docker compose up

由於命名卷為空,映像中的 node_modules 被複製到卷中。

容器參考卷側的 node_modules,因此正常運作。

2. 切換到 develop 並執行 docker compose up -d --build

在 develop 中增加了 @ai-sdk/google
在 build 階段映像會更新,但命名卷中已經有內容。

容器參考的仍然是卷側。
而卷中只含有分支A 時的 node_modules,因此無法找到 @ai-sdk/google

結果

源代碼是 develop 的最新版本,但 node_modules 仍然是分支A 的內容。
這樣的不一致導致了 ERR_MODULE_NOT_FOUND

即便加上 --build,映像也只是重新構建,而命名卷獨立存在。
即便使用 --force-recreate 重新創建容器,也無法刪除卷。

解決方法

方法A: 僅刪除 node_modules 的卷並重新建構

保留其他卷(如數據庫數據等),僅刪除 node_modules 的卷。

docker compose down
docker volume rm <專案名稱>_app_node_modules
docker compose up -d --build

卷名將以專案名稱為前綴。
可使用 docker volume ls 確認實際名稱。

方法B: 刪除所有卷並重新建構

使用 -v 選項刪除所有卷。

docker compose down -v
docker compose up -d --build

雖然方便,但注意這會刪除 所有其他卷的數據,包括資料庫和儲存內容。

方法C: 每次啟動時執行 pnpm install

docker-compose.ymlcommand 中插入啟動時執行 pnpm install 的方法。

app:
  command: sh -c "pnpm install --frozen-lockfile && pnpm run dev"

這樣會在容器內的處理過程中執行 pnpm install,能夠直接修改卷中的內容。
加上 --frozen-lockfile 時,如果 pnpm-lock.yaml 沒有差異幾乎能瞬間完成。

這樣就不需要每次切換分支時手動刪除卷,但啟動時間會延遲幾秒到十幾秒。

如果知曉更合適的方法,歡迎留言告訴我。

此問題發生的時間早見表

操作 是否需要刪除卷
package.json 中增加或刪除套件 需要
pnpm-lock.yaml 被修改 需要
切換分支(依賴有所不同的情況下) 需要
僅變更源代碼 不需要

最後

當執行 docker compose build 後卻無法找到套件的問題,如果不理解命名卷的運作方式,往往很難找到原因。

總結要點如下:

  • docker compose up -d --build 在建構階段卷並不相關
  • 只有在首次掛載空的命名卷時(預設下)映像的內容會被複製
  • 從第二次開始,卷中的內容將直接使用,映像側完全被忽略
  • 在變更 package.json 或切換分支時需要刪除卷

若出現 ERR_MODULE_NOT_FOUND,首先要懷疑命名卷的問題。

參考文獻

株式会社シンシア

在株式会社シンシア,我們正在招募沒有實務經驗的工程師和學生實習生,與我們一同工作。
※ 有關在シンシア的工作情況可以參考此處。

我們每年有超過100位沒有實務經驗的求職者申請,進行技術面試。
如果您發現這篇文章有任何學習收穫,期待您能在 wantedly 的故事中查看,也會非常高興!


原文出處:https://qiita.com/kuma_3838/items/a116aa00af0884675b25


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝20  
563
🥈
我愛JS
💬2  
7
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付