在使用 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 的卷是一種獨立於容器生命周期而持久化數據的機制。
即使刪除容器,卷中的數據仍會保留。
卷主要有以下兩種類型:
有關卷的詳細解釋,請參考以下文章,該文章非常易於理解。
本文將專注於解釋命名卷如何對待 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。
不過,這樣的副作用就是會引發如本次如此「卷中的內容保持過舊」的問題。
總結到這裡的內容,實際上觀察一下切換分支時問題的發生過程。
docker compose up由於命名卷為空,映像中的 node_modules 被複製到卷中。
容器參考卷側的 node_modules,因此正常運作。
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 重新創建容器,也無法刪除卷。
保留其他卷(如數據庫數據等),僅刪除 node_modules 的卷。
docker compose down
docker volume rm <專案名稱>_app_node_modules
docker compose up -d --build
卷名將以專案名稱為前綴。
可使用 docker volume ls 確認實際名稱。
使用 -v 選項刪除所有卷。
docker compose down -v
docker compose up -d --build
雖然方便,但注意這會刪除 所有其他卷的數據,包括資料庫和儲存內容。
pnpm install在 docker-compose.yml 的 command 中插入啟動時執行 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 的故事中查看,也會非常高興!