pnpm deploy 怎麼做?把 monorepo 的單一 app 裝成 production image
monorepo 的 Docker image 很容易在「可以建置」之後,仍把整個 workspace、所有 devDependencies 和一堆 symlink 帶進 production。結果不是 image 太大,就是容器啟動時才發現它依賴 repository 根目錄的 node_modules。
pnpm 的 deploy 命令就是處理這個邊界:先在 workspace 內建置指定 app,再用 pnpm --filter <app> --prod deploy <target> 複製該 app 與它需要的依賴,產生可以單獨搬到 runtime image 的目錄。 它和 pnpm install --prod 在根目錄執行的目的不同,也不是只刪掉 devDependencies。
pnpm deploy 解決的是哪一段?
官方文件把 pnpm deploy 定義為從 workspace 部署一個 package。部署時,目標 app 的檔案會被複製到 target directory,包含 workspace 依賴在內的所有依賴會安裝到隔離的 node_modules,目標目錄因此可以被複製到伺服器執行。
| 做法 | 產物範圍 | 常見問題 |
|---|---|---|
根目錄 pnpm install --prod | 整個 workspace 的 production 依賴 | 不會自動替你裁成單一 app |
pnpm --filter app build | 執行指定 app 的 build script | 只建置,不會建立可攜式 runtime 目錄 |
pnpm --filter app --prod deploy out | app 檔案、專屬 lockfile 與 production 依賴 | 仍要驗證 workspace 設定與輸出檔案 |
Docker multi-stage + deploy | build layer 與 runtime layer 分離 | 需要把 build 輸出與啟動命令對齊 |
如果你還在整理多個 Vue app 或共用 package,可以先看 npm Workspaces 的 monorepo 結構與部署邊界。本文聚焦 pnpm 的 production packaging,不重複介紹 workspace 入門。
先讓 workspace 依賴能被 deploy 解析
目前 pnpm 的 deploy 預設要求 workspace 啟用 inject-workspace-packages。在 pnpm-workspace.yaml 裡,設定名稱使用 YAML 的 camelCase:
packages: - 'apps/*' - 'packages/*'
injectWorkspacePackages: trueapp 依賴共用 package 時,使用明確的 workspace protocol:
{ "name": "@acme/web", "dependencies": { "@acme/ui": "workspace:*" }, "scripts": { "build": "vite build", "start": "node dist/server.js" }}injectWorkspacePackages 的作用是讓本機 workspace 依賴以 hard-link 方式注入,而不是只靠 symlink。pnpm 文件也說明,這個設定不代表所有依賴都會實體複製;dedupeInjectedDeps 仍可能在不需要不同 peer dependency graph 時使用 symlink。部署時真正的檢查點,是最後的 deploy 目錄能否脫離 monorepo 執行。
如果你的專案還不能使用 injected dependencies,可以先用 --legacy:
pnpm --filter @acme/web --prod deploy --legacy out--legacy 會停用 dedicated lockfile 的新 deploy 實作,也允許沒有 injectWorkspacePackages: true 的 workspace。這是相容路徑,不要一邊使用 --legacy,一邊以為已驗證新版 deploy 的 lockfile 行為。
Docker multi-stage 的基本流程
下面的 Dockerfile 讓 build image 保留完整 workspace,runtime image 只接收 deploy 產生的 /out:
FROM node:22-alpine AS buildRUN corepack enableWORKDIR /repo
COPY . .RUN pnpm install --frozen-lockfileRUN pnpm --filter @acme/web build
FROM build AS prunedRUN pnpm --filter @acme/web --prod deploy /out
FROM node:22-alpine AS runtimeWORKDIR /appENV NODE_ENV=production
COPY --from=pruned /out ./EXPOSE 3000CMD ["node", "dist/server.js"]這個流程有三個容易弄錯的地方:
- 先 build,再 deploy:如果
dist/是 build script 產生的,部署前必須先建立它。 --prod放在 deploy 前:它告訴 pnpm 不要把 app 的devDependencies安裝到 target directory。- runtime 啟動檔要在 target 內:
CMD指向的dist/server.js必須被files、.npmignore或.gitignore的篩選規則保留下來。
官方文件也示範在第二個 image 或額外 build stage 執行 pnpm deploy,再把結果複製到新的 Node runtime image。若 build image 不是以 build stage 為基礎,記得在執行 deploy 的 stage 啟用 Corepack 或安裝與 lockfile 相容的 pnpm。
--filter 要和 package name 對齊
--filter 可以使用 workspace package name 或目錄 selector。正式部署建議用 package name,因為它會直接表達「哪一個 package 是產品入口」:
pnpm --filter @acme/web buildpnpm --filter @acme/web --prod deploy out如果 deploy 顯示找不到 package,先跑:
pnpm list --depth -1 --recursivepnpm --filter @acme/web --fail-if-no-match deploy out--fail-if-no-match 可以讓錯誤在選擇階段就停止,而不是最後產生一個看似成功、其實沒有 app 檔案的目錄。若問題是 workspace package 名稱與 glob 沒對齊,可參考 ERR_PNPM_WORKSPACE_PKG_NOT_FOUND 的名稱與 glob 排查。
先檢查 deploy 目錄,不要只看 Docker build 成功
部署前至少檢查下面幾項:
rm -rf ./outpnpm --filter @acme/web --prod deploy out
test -f out/package.jsontest -f out/pnpm-lock.yamltest -f out/dist/server.jsfind out -maxdepth 2 -type f | sort | sed -n '1,80p'pnpm deploy 預設會複製專案檔案,但 pnpm 文件規定了檔案來源優先順序:package 的 files 欄位、app 目錄的 .npmignore,再到 .gitignore。如果 dist/、template、runtime 設定或必要的 migration 檔案被 ignore,容器可能仍能完成 image build,卻在啟動時失敗。
out 裡的依賴也要以 runtime 啟動命令驗證,而不是只檢查 node_modules 存在:
( cd out node -e "console.log(require('./package.json').name)" node dist/server.js)如果 server 會長時間執行,請在測試腳本中加入 timeout 或健康檢查;上面的命令只示範工作目錄與檔案邊界。
不要把 deploy 當成「自動修好所有 workspace」
pnpm deploy 只會依照 package manifest、workspace graph 與檔案篩選規則產生 target。它不會替你決定:
- app 是否已先建置,或 build output 是否需要其他 package 的產物;
- runtime 是否還會讀取 monorepo 根目錄的設定檔;
- Docker build context 是否把 secret、source map 或測試資料帶進 image;
- server 啟動時是否需要 migration、static assets 或 OS 套件;
- 多個 app 是否應各自產生獨立 deploy 目錄。
這些是 deployment contract,應放進 CI 的 build、檢查與 smoke test,而不是等 production container 報 MODULE_NOT_FOUND 才追查。
結論:先建立可攜式目錄,再組 runtime image
對 pnpm monorepo 而言,pnpm deploy 的價值在於把「workspace 內能跑」轉成「指定 app 能獨立搬移」。最小流程是:確認 workspace 依賴設定 → 先 build → 用 --filter 指定 app → 用 --prod deploy 建立 target → 檢查 files/ignore 與啟動檔 → 再複製進 runtime image。
常見問題
Q: pnpm deploy 和 pnpm pack 有什麼差別?
A: pnpm pack 主要是把 package 打成要發布或傳遞的 archive;pnpm deploy 是從 workspace 產生可攜式的部署目錄,並把該 app 與 workspace 依賴安裝到隔離的 node_modules。Docker production image 通常需要後者的目錄形狀。
Q: 為什麼 pnpm deploy 說需要 injectWorkspacePackages?
A: 新版 deploy 實作預設依賴 injected workspace packages,讓本機 package 能以可部署的方式被解析。可以在 pnpm-workspace.yaml 設定 injectWorkspacePackages: true;若專案仍在舊的相容路徑,暫時使用 --legacy,並把 migration 當成獨立工作處理。
Q: --prod 會把 build output 一起刪掉嗎?
A: --prod 主要控制不安裝 devDependencies,不代表它會自動刪除或保留所有 build output。dist/ 是否進入 deploy 目錄,仍取決於 build 是否先執行,以及 package 的 files、.npmignore、.gitignore 規則。
參考資料:
回報錯字、失效連結,或告訴我你想看的延伸主題。