1594 字
8 分鐘

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 outapp 檔案、專屬 lockfile 與 production 依賴仍要驗證 workspace 設定與輸出檔案
Docker multi-stage + deploybuild 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: true

app 依賴共用 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

Terminal window
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

1.4
FROM node:22-alpine AS build
RUN corepack enable
WORKDIR /repo
COPY . .
RUN pnpm install --frozen-lockfile
RUN pnpm --filter @acme/web build
FROM build AS pruned
RUN pnpm --filter @acme/web --prod deploy /out
FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=pruned /out ./
EXPOSE 3000
CMD ["node", "dist/server.js"]

這個流程有三個容易弄錯的地方:

  1. 先 build,再 deploy:如果 dist/ 是 build script 產生的,部署前必須先建立它。
  2. --prod 放在 deploy 前:它告訴 pnpm 不要把 app 的 devDependencies 安裝到 target directory。
  3. 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 是產品入口」:

Terminal window
pnpm --filter @acme/web build
pnpm --filter @acme/web --prod deploy out

如果 deploy 顯示找不到 package,先跑:

Terminal window
pnpm list --depth -1 --recursive
pnpm --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 成功#

部署前至少檢查下面幾項:

Terminal window
rm -rf ./out
pnpm --filter @acme/web --prod deploy out
test -f out/package.json
test -f out/pnpm-lock.yaml
test -f out/dist/server.js
find 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 存在:

Terminal window
(
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 deploypnpm 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 規則。

參考資料:

pnpm Docs:pnpm deploy

pnpm Docs:Workspace 與 workspace protocol

pnpm Docs:Filtering

pnpm deploy 怎麼做?把 monorepo 的單一 app 裝成 production image
https://laplusda.com/posts/pnpm-deploy-monorepo-production-image/
作者
Zero
發佈於
2026-08-19
許可協議
CC BY-NC-SA 4.0
這篇文章有幫助嗎?

回報錯字、失效連結,或告訴我你想看的延伸主題。