Skip to content
ABFree Studio

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

© 2024 ABFree Co. All rights reserved.