TurboRepo Docker镜像优化指南:轻松打造轻量级镜像
最近往我的TurboRepo中添加了几个后端项目,然后使用Docker部署到服务器上,网上的TurboRepo的打包部署教程比较少,而且有些文章给的方法让Docker的镜像变得很大,所以我这里记录一下我是怎么减少镜像大小的。
本篇文章主要讲解在TurboRepo项目中,如何让Docker打包的镜像变得更小,然后分步逐行解释Dockerfile,帮助大家制定自己的Dockerfile。
背景
TurboRepo中打包Docker之所以复杂的原因大致有两点:
1.pnpm-lock.yaml文件只有一个:在根目录下,因为其管理着整个项目的依赖,而不是在每个项目的根目录下。 如果我们直接从根目录打包,或者复制lock文件到项目文件中打包,Docker镜像中就会包含所有的依赖,这样镜像会变得很大。
2.项目之间的依赖:TurboRepo中的项目之间是可以相互依赖的,这样就会导致项目之间的依赖会被打包到镜像中。
比如下面的项目结构:
├── Dockerfile
├── backend
│ ├── main-server
│ ├── pub-auth
│ └── worker-gateway
├── db
├── node_modules
├── package.json
├── packages
│ ├── share
│ └── ui
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json
在这个项目中,main-server依赖了share和pub-auth,但是因为在大仓中,我们直接运行pnpm install
,所有的依赖都会被安装到node_modules中,这样就会导致镜像变得很大。
比如我在我没优化之前,我的项目镜像大概有1.5G(前端的依赖太多了),优化之后,镜像大小只有200M。
解决方案
解决方案大致有三个:
1.使用turbo prune
turbo prune主要解决了项目之间的依赖问题,它会将项目之间的依赖删除,只保留项目自己的依赖。
比如在上面的结构中,我们可以运行turbo prune main-server --docker
,这样就会删除没有用到的依赖,只保留main-server自己的依赖放在out
文件夹中。
在out
目录在,其结构的规则如下:
- 根目录保留了一份pnpm-lock.yaml文件用于安装build时的依赖
- full目录下存放了main-server和其依赖项目的源码
- json目录下存放了main-server和其依赖项目的package.json文件
结构变成这样:
pnpm-lock.yaml
full/
├── backend/
│ ├── main-server/
│ └── pub-auth/
└── packages/
└── share/
json/
├── backend/
│ ├── main-server/
│ │ └── package.json
│ └── pub-auth/
│ └── package.json
└── packages/
└── share/
└── package.json
这样我们就可以在Dockerfile中使用这些文件来构建项目。
2.使用pnpm install —prod (pnpm prune)
在上面的动作中,我们已经删除了项目之间的依赖,但是还有一个问题,就是我们的项目中可能会有很多开发依赖,这些依赖在生产环境中是不需要的,所以我们需要删除这些依赖。
在文档中对pnpm prune
的描述如下:
Remove the packages specified in devDependencies.
这个命令会删除devDependencies
中的依赖 ( ⚠️ 注意,不要将运行时依赖放在devDependencies
中,否则会影响项目的运行)。
不过这个命令在TurboRepo中并不适用,因为我们的项目中可能会有很多子项目,这些子项目的依赖可能会被删除,导致项目无法运行。
但是我们可以复制项目中的package.json
到根目录,然后删除node_modules
,然后运行pnpm install --prod
,这样就可以删除开发依赖。
3.使用多阶段构建
多阶段构建是Dockerfile中的一个特性,可以让我们在一个Dockerfile中构建多个镜像,然后将最终的镜像复制到最终的镜像中。
这个Dockerfile主要分为四个阶段:
- 第一个阶段是在alpine镜像中安装pnpm和turbo
- 第二个阶段是在base镜像中,运行
turbo prune
,删除项目之间的依赖 - 第三个阶段是在builder镜像中,运行
pnpm install --prod
,删除开发依赖,然后运行turbo build
,构建项目 - 第四个阶段是在runner镜像中,运行项目
这里就不再赘述了,其目的是为了不将构建环境的依赖放到最终的镜像中,从而减小镜像的大小。 具体可以参考Docker多阶段构建。
最终的Dockerfile
这里的ARG PROJECT=main-server
是为了方便在构建时指定项目,比如docker build --build-arg PROJECT=main-server .
ARG NODE_VERSION=20
ARG PROJECT=main-server
# Alpine image
FROM node:${NODE_VERSION}-alpine AS alpine
RUN apk update
RUN apk add --no-cache libc6-compat
# Setup pnpm and turbo on the alpine base
FROM alpine as base
RUN npm install pnpm turbo --global
# Prune projects
FROM base AS pruner
ARG PROJECT
WORKDIR /app
COPY . .
RUN turbo prune ${PROJECT} --docker
# Build the project
FROM base AS builder
ARG PROJECT
WORKDIR /app
# Copy lockfile and package.json's of isolated subworkspace
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=pruner /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY --from=pruner /app/out/json/ .
# First install the dependencies (as they change less often)
RUN pnpm install --frozen-lockfile
# Copy source code of isolated subworkspace
COPY --from=pruner /app/out/full/ .
RUN turbo build --filter=${PROJECT}
# 复制一个package.json到根目录
COPY --from=pruner /app/out/json/backend/${PROJECT}/package.json ./package.json
# 删除node_modules
RUN rm -rf node_modules
RUN pnpm install --prod
# Final image
FROM alpine AS runner
ARG PROJECT
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nodejs
USER nodejs
WORKDIR /app
COPY --from=builder --chown=nodejs:nodejs /app .
WORKDIR /app/backend/${PROJECT}
ARG PORT=8080
ENV PORT=${PORT}
ENV NODE_ENV=production
EXPOSE ${PORT}
CMD node dist/index.js