我有一个包含 4 个节点包的单一存储库:
/
└── 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 镜像,但我不想包含该镜像实际上不需要的任何 node_modules,而且我也不想包含该镜像所不需要的任何其他目录或文件。不需要图像。
本质上我有两个问题:
目前,我只是将整个 mono-repo 复制到图像中:
FROM node:20.5.1-alpine3.18
RUN apk update && apk --no-cache add --virtual builds-deps build-base python3
ADD . /mono-repo
WORKDIR /mono-repo/apps/app2
我可以像这样列出所有依赖项吗?
npm ls -p -w ./apps/app2 | sed 1d
...然后以某种方式神奇地删除其他所有内容?
不确定是否相关,但我正在使用 GitLab CI,并且我有一个安装 node_modules 的作业,将它们放入缓存中,然后我有一个 docker-build 作业,它从缓存中检索 node_modules 以节省时间。因此,如果可能的话,我想继续使用该缓存以节省时间。每个 CI 作业都使用基于 alpine 的 Docker 镜像,我正在构建的镜像也基于 alpine。
编辑: 我现在在 GitLab 中使用正确的缓存(缓存存储在 ./.npm)。因此,我可以通过专门为一个工作区安装 node_modules 来解决问题之一:
npm ci --cache .npm --prefer-offline -w "$NODE_APP_DIR"
我还更新了 Dockerfile 以分两步运行(首先作为构建器安装依赖项并删除不必要的文件),然后我只需将剩下的所有内容复制到最终映像中:
# 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
不幸的是,这不太有效。具体来说,我尝试删除应用程序不需要的所有内容的部分:
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' '|')"
首先,我使用
npm ls -p -w "$NODE_APP_DIR"
获取应用程序具有的所有依赖项(递归地)的文件路径列表。然后我使用 find
递归地查找 mono-repo 中的每个文件。然后,我使用 grep 过滤掉以 npm ls 命令返回的路径之一开头的所有路径。为了使其正常工作,我必须首先使用 sed 清理依赖项列表,以便我可以将其与正则表达式一起使用。然后我在每行(正则表达式)的开头添加一个插入符号,然后用管道(正则表达式)替换所有换行符,以使 grep 与任何依赖项匹配。
但是看来我的想法行不通。尽管我专门只为一个应用程序安装了节点模块,但它似乎仍然返回了大量的node_modules。难道就没有更好的办法了吗?整个方法感觉太老套了......
据我所知,您的方法是正确的。您可以改进排除不需要的文件的方法,可以像下面这样进行优化。这将确保使用 npm 和 docker 的多阶段构建机制仅将所需的依赖项添加到最终的 docker 文件中。
参考下面代码中的
Docker
文件:
FROM node:20.5.1-alpine3.18 AS builder
RUN apk update && apk --no-cache add build-base python3
COPY . /mono-repo
WORKDIR /mono-repo
ARG NODE_APP_DIR
RUN npm ci --cache .npm --prefer-offline -w "$NODE_APP_DIR"
RUN mkdir /app && cp -R "$NODE_APP_DIR" /app && cd /app && npm prune --production
FROM node:20.5.1-alpine3.18
COPY --from=builder /app /app
WORKDIR /app
ARG NODE_APP_DIR
CMD npm start --workspace "$NODE_APP_DIR"
上面将复制整个 Mono 存储库,并使用
npm ci
安装依赖项,然后将文件复制到新目录 /app
,然后将相关应用程序代码复制到创建的目录。最后在 npm prune --production
目录中运行 /app
将删除不必要的node_modules。
然后仅将修剪后的
/app
目录从构建器复制到舞台到最终图像。
将工作目录设置为
/app
并确保设置 NODE_APP_DIR
在您的情况下它可能是 apps/app2
。
然后您可以使用以下命令为
app2
构建 docker 镜像:
docker build --build-arg NODE_APP_DIR="apps/app2" -t app2:test .
希望这有帮助。如果有任何问题请随时询问。