跳至主要内容

為什麼在 Docker 中使用匿名 volume 處理 `node_modules` 是個好方法?

在團隊開發中使用 Docker 的主要優點是能統一開發環境,讓團隊成員有一致的開發體驗。設定 Node.js 或前端專案的 Docker 開發環境時,需要理解 Docker 和 Docker Compose 的一些運作機制。本文將介紹如何使用匿名 volume 管理 node_modules 資料夾,解決常見的開發問題。

Node.js 開發的常見挑戰與需求

在 Node.js 或前端專案的 Docker 化過程中,常見的需求與挑戰包括:

  1. 開發時的即時重載:修改本地程式碼後,能在容器內即時看到變更並進行測試。
  2. node_modules 的隔離:避免容器內的 node_modules 與宿主機的 node_modules 相互影響,處理不同作業系統間原生相依套件的差異,讓開發者能在本地或 Docker 環境中運行專案。
  3. 生產環境的輕量化:為生產環境建構體積小、安全且只包含必要檔案的容器映像檔。

node_modules 的兼容性問題

直接在不同作業系統的宿主機上安裝 node_modules,再將其映射到 Docker 容器中,經常會引發以下兼容性問題:

  • 作業系統差異:不同作業系統(如 Windows 與 Linux)對某些相依套件有不同的編譯需求。在 Windows 上執行 npm install 產生的 node_modules,可能無法在基於 Linux 的 Docker 容器中正常運作。
  • 二進制兼容性:部分 Node.js 模組依賴系統特定的二進制檔案。宿主機與容器的作業系統環境不同時,這些二進制檔案可能不兼容,導致應用在容器內執行失敗。

匿名 volume 如何解決 node_modules 的問題?

匿名 volume 是解決 node_modules 隔離與兼容性問題的有效方式。

匿名 volume 的概念:在 docker-compose.yml 中定義像 /app/node_modules 這樣的 volume 但不指定宿主機上的來源路徑時,Docker 會自動建立一個由 Docker 管理的匿名資料 volume,此 volume 獨立於宿主機的檔案系統,專供容器使用。

匿名 volume 的好處

  1. 環境隔離與避免依賴衝突

    • 匿名 volume 使容器內的 node_modules 與宿主機上的 node_modules 保持隔離。即使宿主機上有 node_modules 資料夾(例如為了讓 IDE 或編輯器有 TypeScript 提示功能),也不會影響容器內安裝的相依套件。
    • 這能避免原生相依套件在不同系統間複製產生的問題。
  2. 應用的可移植性與環境一致性

    • 在容器內生成 node_modules 並存放在匿名 volume 中,確保相依套件適用於容器環境,不受宿主機配置影響。
    • 專案在不同開發者間以及在開發、測試到生產環境間有更好的一致性。
  3. 開發彈性與一致體驗

    • 開發者可使用不同作業系統開發相同應用,不需擔心運行環境差異。
    • node_modules 位於容器內的預期路徑(如 /app/node_modules),使本地或容器內開發的目錄結構保持一致,不需更改相依套件的安裝位置。
  4. 簡化配置與減少錯誤

    • 匿名 volume 的配置相對簡單,可以自動處理 node_modules 的隔離,避免了複雜的手動配置可能導致的錯誤。

實作方式:Dockerfile 與 Docker Compose 配置

以下將展示如何透過 Dockerfiledocker-compose.yml 來實現使用匿名 volume 管理 node_modules

Dockerfile (多階段建構範例)

為了同時滿足開發和生產環境的需求,我們通常會使用多階段建構(multi-stage build)。

# ---- Builder Stage ----
FROM node:16.1.0-buster AS builder

WORKDIR /app

# 先複製 package.json 和 lock 檔案,充分利用 Docker 快取
# 僅當這些檔案變更時才重新安裝相依套件
COPY package.json /app/

# 使用 Yarn
COPY yarn.lock /app/
RUN yarn install --frozen-lockfile

# 或者使用 NPM
# COPY package-lock.json /app/
# RUN npm ci

# 複製專案的其餘程式碼
COPY . .

# 執行建構命令 (例如:產生前端靜態檔案)
RUN npm run build

# ---- Production Stage ----
# 使用輕量級的 Nginx 映像檔來提供服務
FROM nginx:1.19.10-alpine
# 從 builder 階段複製建構好的靜態檔案到 Nginx 的服務目錄
COPY --from=builder /app/build /usr/share/nginx/html
# 複製 Nginx 設定檔 (可選)
COPY nginx.default.conf /etc/nginx/conf.d/default.conf

Dockerfile 說明

  • 多階段建構 (AS builder):我們首先使用一個包含完整 Node.js 環境的 builder 映像檔來安裝相依套件和建構專案。
  • 優化相依套件安裝:先複製 package.jsonyarn.lock (或 package-lock.json) 並執行安裝命令。這樣,只有在這些檔案發生變化時,Docker 才需要重新執行安裝相依套件的步驟,否則會使用快取,加快映像檔建構速度。
  • 生產環境映像檔:接著,我們使用一個非常輕量級的 nginx:alpine 映像檔作為最終的生產環境映像檔,並只從 builder 階段複製建構好的產物(例如 build 資料夾)。
  • 優點:這種方法不僅能顯著減小最終生產映像檔的體積(因為不包含開發時的相依套件和原始碼),還能提高安全性(減少潛在的攻擊面)。

Docker Compose 配置範例 (開發環境)

開發環境需要不同的設定,以便即時反映程式碼變更並使用匿名 volume:

version: '3.9' # 指定 Docker Compose 檔案格式版本
services:
app: # 服務名稱,可自訂
build:
context: . # Dockerfile 的路徑
target: builder # 指定使用 Dockerfile 中的 builder 階段
ports:
- "3000:3000" # 映射容器的 3000 連接埠到宿主機的 3000 連接埠 (假設開發伺服器運行在 3000)
volumes:
- .:/app # 將當前目錄映射到容器的 /app 目錄,進行源碼共享
- /app/node_modules # 建立匿名 volume,保護容器內的 node_modules 不被宿主機的內容覆蓋
command: npm run start # 容器啟動後執行的命令 (例如:啟動開發伺服器)

Docker Compose 配置解析

  • build.target: builder: 指示 Docker Compose 在建構時只建到 builder 階段,取得完整 Node.js 開發環境。
  • volumes:
    • .:/app: 綁定掛載 (bind mount),將本地專案根目錄映射到容器內的 /app。這樣本地修改會同步到容器,開發伺服器能偵測變更並自動重載。
    • /app/node_modules: 匿名 volume 的設定,為容器內的 /app/node_modules 路徑創建匿名 volume。
      • 運作方式:容器啟動時,若 Dockerfile 中的安裝步驟已在 /app/node_modules 產生相依套件,這些套件會保留在匿名 volume 中。無論宿主機上是否有 node_modules,綁定掛載 .:/app 都不影響容器內的 node_modules,確保容器使用在容器環境中正確安裝的相依套件。

使用匿名 volume 的注意事項

匿名 volume 中的資料更新

匿名 volume 的一個特性是其內容會被 Docker 保留,即使容器停止或重新啟動。這在大多數情況下是期望的行為,因為它避免了每次重啟都重新下載相依套件。

然而,有時這也可能導致問題:如果你更新了 package.jsonyarn.lock,並重新建構了映像檔(docker-compose build),新的映像檔中可能包含了更新版本的相依套件邏輯。但如果直接 docker-compose up,Docker 可能會重新使用上一次啟動時建立的、包含舊版 node_modules 的匿名 volume。

解決方案: 若要確保匿名 volume 中的 node_modules 與映像檔中的定義一致(即強制 Docker 重新根據映像檔的內容初始化匿名 volume,或使用新的匿名 volume),可以在啟動時加上 -V--renew-anon-volumes 參數:

docker-compose up -V

這會移除與服務相關聯的匿名 volume,並在容器啟動時重新創建它們,從而確保 node_modules 是最新的。

管理考量

  • volume 數據持久化:雖然匿名 volume 在容器停止或被刪除時其數據通常不會自動被保存(除非你明確地管理它們或它們是持久化的),但對於 node_modules 這種通常在映像檔建構時就確定的內容,這通常不是問題。其主要目的是隔離和兼容性,而非持久化需要動態修改的用戶數據。
  • 多容器管理:如果有多個服務或容器都使用類似的匿名 volume 策略,雖然 Docker 會分別管理它們,但在複雜場景下,追蹤和管理這些由 Docker 自動命名的 volume 可能會稍微增加一些複雜度。不過,對於 node_modules 這種特定用途,其好處遠大於潛在的管理成本。

總結

在 Docker 中設定 Node.js 開發環境的幾個關鍵實踐:

  1. 使用多階段建構:為生產環境實現建構效能和安全性。
  2. 優化相依套件安裝:先安裝相依套件再複製原始碼,以加快映像檔建構速度。
  3. 隔離容器與宿主機的 node_modules:透過匿名 volume 支援本地與 Docker 雙重開發模式,解決跨平台兼容性問題。

使用匿名 volume 來處理 node_modules 不僅解決了跨作業系統的兼容性問題,避免了宿主機與容器間的直接依賴衝突,還提高了開發環境的穩定性、應用的可移植性以及團隊協作的彈性。這種配置確保了開發環境的隔離與一致性,是現代軟體開發中使用 Docker 進行 Node.js 應用開發時的推薦做法。


參考資料

Docker Compose with Node.js