You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为npm workspaces单仓中的单个Node.js应用构建精简Docker镜像

解决Monorepo下Node.js应用的Docker镜像瘦身问题

背景

我有一个包含4个Node.js包的monorepo,结构如下:

/
└── apps
    ├── app1 ( "dependencies": { "lib1": "../../libs/lib1" } )
    └── app2 ( "dependencies": { "lib2": "../../libs/lib2", "express": "^4.18.2" } )
└── libs
    └── lib1  ( "dependencies": { "lib2": "../lib2", "puppeteer": "^21.1.1" } )
    └── lib2  ( "dependencies": { "node-gyp": "^9.4.0" } )
└── node_modules
└── package.json ( { ... "workspaces": ["apps/app*", "libs/lib*"], ... } )

核心需求:

  • app1不需要express
  • app2不需要lib1和puppeteer

要为app1和app2构建Docker镜像,要求:

  1. 镜像仅包含应用实际需要的node_modules
  2. 解析内部依赖的软链接,将实际代码纳入镜像而非软链接

当前尝试的方案及问题

我用GitLab CI缓存.npm目录,通过npm ci --cache .npm --prefer-offline -w "$NODE_APP_DIR"安装单个工作区的依赖,同时采用多阶段构建:

# Stage 1: Create an intermediate image for npm install
FROM node:20.5.1-alpine3.18 as builder

# Install build dependencies
RUN apk update && apk --no-cache add build-base python3

# Copy the application code into the builder image
COPY . /mono-repo

# Set the working directory
WORKDIR /mono-repo

# Set the NODE_APP_DIR ARG
ARG NODE_APP_DIR

# Run npm install in the workspace
RUN npm ci --cache .npm --prefer-offline -w "$NODE_APP_DIR"

# Remove everything that the app doesn't need
RUN export EXCLUDE_PATHS="$(npm ls -p -w "$NODE_APP_DIR" | sed 1d)" && echo "DEPENDENCIES: $EXCLUDE_PATHS" && find "$PWD" -name "*" | grep -vE "$(echo -en "$EXCLUDE_PATHS" | sed 's/[]\/$*.^|[]/\\&/g' | sed 's/^/^/' | tr '\n' '|')"

# Stage 2: Create the final production image
FROM node:20.5.1-alpine3.18

# Copy everything that's left from the builder image
COPY --from=builder /mono-repo /mono-repo

# Set the working directory
WORKDIR /mono-repo

# Set the NODE_APP_DIR ARG
ARG NODE_APP_DIR

# CMD for the final image
CMD npm start --workspace "$NODE_APP_DIR"

构建命令:

docker build --build-args NODE_APP_DIR="apps/app2" -t app2:test

但删除不必要文件的步骤未生效,镜像仍包含大量冗余node_modules。问题出在:npm ls -p输出的路径格式和find结果不匹配,正则处理复杂且容易出错,无法准确筛选需要保留的文件。

优化方案

方案1:用npm pack处理内部依赖,复制必要文件到独立目录

核心思路是为目标应用创建独立目录,仅复制应用代码和所有依赖(包括内部lib的打包文件),避免复制整个monorepo。

修改builder阶段:

# Stage 1: Build and package dependencies
FROM node:20.5.1-alpine3.18 as builder

RUN apk update && apk --no-cache add build-base python3 jq

COPY . /mono-repo
WORKDIR /mono-repo

ARG NODE_APP_DIR

# 安装目标工作区的依赖
RUN npm ci --cache .npm --prefer-offline -w "$NODE_APP_DIR"

# 创建目标目录
RUN mkdir -p /app

# 复制应用代码到目标目录
COPY "$NODE_APP_DIR"/ /app/

# 初始化app的package.json和node_modules
RUN cp "$NODE_APP_DIR/package.json" /app/
RUN mkdir -p /app/node_modules

# 处理内部依赖:将libs下的依赖打包后解压到app的node_modules
RUN for lib in $(jq -r '.dependencies | keys[]' "/mono-repo/$NODE_APP_DIR/package.json" | grep -E '^lib'); do \
    cd "/mono-repo/libs/$lib" && npm pack; \
    tar -xzf "$lib"*.tgz -C /app/node_modules; \
    mv /app/node_modules/package /app/node_modules/"$lib"; \
    rm "$lib"*.tgz; \
done

# 复制外部依赖到app的node_modules
RUN cp -R /mono-repo/node_modules/* /app/node_modules/

# 清理外部依赖中不需要的包
RUN npm prune --production --prefix /app

# Stage 2: 最终镜像
FROM node:20.5.1-alpine3.18

COPY --from=builder /app /app
WORKDIR /app

CMD ["npm", "start"]

方案2:用npm list生成依赖列表,结合rsync复制必要文件

避免删除操作,直接复制需要的文件到目标目录:

# Stage 1: 准备依赖列表和代码
FROM node:20.5.1-alpine3.18 as builder

RUN apk update && apk --no-cache add build-base python3 rsync

COPY . /mono-repo
WORKDIR /mono-repo

ARG NODE_APP_DIR

RUN npm ci --cache .npm --prefer-offline -w "$NODE_APP_DIR"

# 生成需要保留的路径列表:应用代码 + 所有依赖
RUN echo "$NODE_APP_DIR" > /keep-list.txt
RUN npm ls -p -w "$NODE_APP_DIR" | sed 1d >> /keep-list.txt

# 创建目标目录并复制需要的内容
RUN mkdir -p /app
RUN rsync -av --files-from=/keep-list.txt /mono-repo/ /app/

# 解析内部软链接:将libs的实际内容复制到node_modules对应的位置
RUN find /app/node_modules -type l -name 'lib*' | while read link; do \
    target=$(readlink "$link"); \
    rm "$link"; \
    cp -R "/mono-repo/$target" "$link"; \
done

# Stage 2: 最终镜像
FROM node:20.5.1-alpine3.18

COPY --from=builder /app /app
WORKDIR /app

CMD ["npm", "start"]

方案3:使用pnpm替代npm(若可切换包管理器)

pnpm的monorepo支持更高效,默认创建硬链接而非软链接,且可通过pnpm deploy直接生成仅含生产依赖的目录:

# Stage 1: 部署应用
FROM node:20.5.1-alpine3.18 as builder

RUN apk update && apk --no-cache add build-base python3
RUN npm install -g pnpm

COPY . /mono-repo
WORKDIR /mono-repo

ARG NODE_APP_DIR

RUN pnpm install --store=.pnpm-store --filter "$NODE_APP_DIR"...
RUN pnpm deploy --filter "$NODE_APP_DIR" /app

# Stage 2: 最终镜像
FROM node:20.5.1-alpine3.18

COPY --from=builder /app /app
WORKDIR /app

CMD ["node", "index.js"] # 根据实际启动文件调整

这些方案都能避免复制整个monorepo,确保镜像仅包含应用需要的内容,同时处理内部依赖的链接问题。

内容的提问来源于stack exchange,提问作者Forivin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 10:19:17