使用Docker打包Turborepo Remix应用指南
将 Remix 应用程序 Docker 化
最近,我有机会接触到了一种特别让我印象深刻的科技:Turborepo。Turborepo 是一个专门为 JavaScript 和 TypeScript 优化的增量构建工具和打包系统,使用 Rust 语言编写。
在这个项目期间,我们被要求将仓库中的一个应用程序docker化,具体来说,我们要docker化的就是一个Remix应用程序。在这篇文章中,我将引导你完成实现此目标的过程,展示我们如何利用Turborepo和Docker一起工作,构建一个尽可能小的最终Docker镜像。
前提条件查看完整代码和更多细节,请浏览一下我的 GitHub:GitHub 仓库。
- 确保你已经安装了 Turborepo CLI。如果没有安装的话,请参阅Turborepo 文档以获取详细的安装指南。
在我们深入构建Docker镜像之前,了解典型Turborepo项目的结构非常重要。一个标准的项目会有如下高层次的结构:
turborepo-remix-app/
│
├── package.json
├── package-lock.json
├── turbo.json
├── apps/
│ ├── docs/
│ │ └── package.json
│ │ ... 等相关代码文件
│ └── web/ (一个 Remix 应用程序)
│ └── package.json
│ ... 等相关代码文件
└── packages/
└── ui/
└── package.json
... 等相关代码文件
有两个主要的目录需要注意:名为apps和packages的目录。
- apps : 此目录包含仓库中的应用。其中每个文件夹代表一个独立的应用,每个应用都有自己的
package.json
和相关文件。在我们的情况下,/web
目录存放了我们的 "Remix" 应用。 - packages : 此目录存放了我们在 Turborepo 中各项目共用的组件和工具。
理解这种结构非常重要,因为它能帮助我们高效地管理和构建使用Turborepo的项目。
瑞典语翻译应调整为更符合中文习惯的表达:“了解 Turborepo 命令”。同时确保专有名词“Turborepo”保持不变。 瑞典语翻译调整为:“了解 Turborepo 命令”保持专有名词“Turborepo”不变。 了解 Turborepo 命令在我们开始编写 Dockerfile 之前,有必要熟悉一下 Turborepo 提供的几个关键命令。我们将主要关注 prune
命令(注:prune
是清理和删除多余文件的命令)这部分内容。根据 Turborepo 的官方文档,该命令:
生成目标包的一个部分单代码库。输出将被放置在一个名为 out 的目录里,具体内容如下:
构建目标“app”所需的所有内部包的完整源代码。
精简后的锁定文件,仅包含构建目标“app”所需的原始锁定文件子集。
- 根 package.json 文件的副本。如果我们对之前提到的结构化项目运行命令
turbo prune web --docker
,则会在名为out
的文件夹中得到类似以下的输出。
例如,运行命令 turbo prune web --docker
在前面提到的项目结构下,将会在名为 out 的文件夹中生成类似以下的输出内容。
turborepo-remix-app/
├── out/
│ ├── package-lock.json
│ ├── full/
│ │ ├── apps/
│ │ │ └── web/
│ │ │ └── package.json
│ │ │ ... 其他相关代码文件
│ │ ├── package.json / (来自项目根目录)
│ │ ├── packages/
│ │ │ └── ui/
│ │ │ └── package.json
│ │ │ ... 其他相关代码文件
│ ├── JSON/
│ │ ├── apps/
│ │ │ └── web/
│ │ │ └── package.json
│ │ ├── package.json / (来自项目根目录)
│ │ ├── packages/
│ │ │ └── ui/
│ │ │ └── package.json
│ │ ... 其他相关代码文件
从上面的结构中,我们可以看出以下几点:
- 一个名为
json
的文件夹,包含精简工作区的package.json
文件。 - 一个名为
full
的文件夹,包含构建目标所需内部包的完整源代码。 - 一个精简后的锁定文件,包含构建目标所需原始锁定文件的子集。
理解这些输出结果对于利用 Turborepo 来管理和构建我们的应用程序非常重要。
🐳 让我们用 Docker 容器化它吧 🐳现在我们已经很好地理解了 Turborepo 的工作原理,是时候开始把我们的 Remix 应用程序打包进容器,并编写相应的 Dockerfile 了。需要注意的是,这种方法不仅仅是对 Remix 有用,还可以适用于任何其他 JavaScript 框架。
在 Turborepo 中,我们会在应用级别添加 Dockerfile。在我们的例子中,将在以下路径添加 Dockerfile:apps/web/dockerfile
。
另一个重要方面是使用多阶段构建步骤,这不仅能使代码更高效,还能显著减小最终的镜像大小。
第一阶段:基础阶段从 node:20-alpine3.19 作为基础镜像。
- 平台说明:Dockerfile 从指定基础节点版本
node:20-alpine3.19
开始,这确保了兼容性并提供了轻量级的环境。 - 标签:该阶段标记为
base
,方便后续阶段引用。
# 输出将被放置到名为 "out" 的目录下。生成目标包的单仓库部分。
FROM base AS prune
RUN apk update
RUN apk add --no-cache libc6-compat
WORKDIR /app
RUN npm install turbo --global
COPY . . # 复制当前目录到应用目录下
RUN turbo prune web --docker # 运行 turbo prune 命令修剪 web 目录,使用 --docker 参数
- 基础继承:
prune
阶段从base
阶段继承。 - 包更新及安装:它更新 Alpine 包索引并安装
libc6-compat
以确保与某些二进制文件的兼容性。 - 工作目录:设置工作目录为
/app
。 - 安装 Turbo:使用 npm 全局安装 Turbo。
- 复制文件夹:将整个项目目录复制到容器中。
- Turbo 精简:执行
turbo prune web --docker
来生成web
包的部分单仓库,将输出放到之前在“理解 Turborepo 命令”部分提到的目录中。
# 此步骤仅安装生产和开发依赖项
FROM base AS 安装程序
RUN apk update
RUN apk add --no-cache libc6-compat
WORKDIR /app
# /out/json 目录包含 package.json 文件,用于安装与 “web” 相关的包
COPY --from=prune /app/out/json/ .
RUN npm install
- 基础继承:
installer
阶段继承自base
阶段。 - 软件包更新:更新Alpine软件包索引。
- 包安装:安装
libc6-compat
而不进行缓存,以保持镜像大小较小,从而节省空间。 - 设定工作目录:将工作目录设定为
/app
。 - 复制文件:从
prune
阶段的输出目录复制package.json
文件到当前的工作目录。此文件包含了web
包所需的依赖项。 - 安装依赖:运行
npm install
来安装package.json
文件中指定的生产和开发依赖项。
# 此步骤仅安装生产环境所需的依赖项
FROM base AS installer-production
RUN apk update # 更新 apk
RUN apk add --no-cache libc6-compat # 添加必要的兼容库
WORKDIR /app # 设置工作目录为 /app
# /out/json 目录包含用于安装与 "web" 相关包的 package.json 文件
COPY --from=prune /app/out/json/* . # 复制 package.json 文件
RUN npm install --only=production # 安装生产环境所需的 npm 包
- 基类继承:
installer-production
阶段继承自base
阶段。 - 更新包:更新 Alpine 的软件包索引。
- 安装包:不启用缓存来安装
libc6-compat
,以保持镜像大小较小。 - 工作目录:设置工作目录为
/app
。 - 复制文件:从
prune
阶段的输出目录复制package.json
文件到当前工作目录。此文件包含web
包所需的依赖。 - 安装依赖:运行
npm install --production
来仅安装package.json
文件中指定的生产依赖。
可能有人会疑惑,我们为什么要有两个安装器(一个是开发版,一个是生产版)?
我们的目标是将这两个步骤分开以优化构建过程。开发安装程序在构建阶段使用,而生产安装程序则用于生成最终镜像。后续阶段会提供更多相关信息。
第五阶段:建设者 # 使用基镜像构建生产环境
FROM base AS builder
WORKDIR /app
设置工作目录为/app
COPY --from=installer /app/node_modules /app/node_modules
从installer镜像中复制/app/node_modules到当前的/app/node_modules目录
# /out/full目录包含了用于运行"web"包的代码文件
COPY --from=prune /app/out/full/ .
从prune镜像中复制/app/out/full/目录下的文件到当前目录
# 假设你的根目录下的 package.json 文件中有 'build' 命令
# --filter 参数用于指定要执行 'build' 命令的应用程序
RUN npx turbo build --filter=web
运行 npx turbo build --filter=web 命令
- 基础继承:
builder
阶段继承自base
阶段。 - 工作目录:设置工作目录为
/app
。 - 复制文件:将
prune
阶段输出目录中的开发包剪枝文件复制到当前工作目录。 - 构建命令:执行
npx turbo build
命令以构建web
包的生产版本。
# runner 将使用 builder 的输出,并且仅使用生产环境的 node_modules 以减小最终镜像的体积
FROM base AS runner
WORKDIR /app
# 不要以 root 用户运行生产环境
RUN addgroup --system --gid 1001 web-group
RUN adduser --system --uid 1001 web-user
USER web-user
# 将生产环境所需依赖项、构建文件和公共文件复制到相应目录
COPY --from=installer-production --chown=web-user:web-group /app/node_modules /app/node_modules
COPY --from=prune --chown=web-user:web-group /app/out/json/ .
COPY --from=builder --chown=web-user:web-group /app/apps/web/build /app/apps/web/build
COPY --from=builder --chown=web-user:web-group /app/apps/web/public /app/apps/web/public
WORKDIR /app/apps/web
cmd ["npm", "run", "start"]
- 基础镜像:运行阶段从
base
镜像开始,这是一个仅包含必要运行时依赖的最小镜像。 - 工作目录:工作目录设置为
/app
,应用程序代码和依赖项将存放在此目录。 - 非 root 用户的设置:
-
创建一个具有 GID 1001 的系统组
web-group
,使得表达更加自然。 -
创建一个具有 UID 1001 的系统用户
web-user
并将该用户添加到web-group
。 - 使用
USER
指令切换到web-user
账户,以避免以 root 用户身份运行应用程序,这样可以增强安全性。
4. 复制依赖项和构建输出 :
- 生产依赖项从
installer-production
阶段复制到/app/node_modules
。 - JSON 输出文件从
prune
阶段复制到当前目录。 - 构建目录和公共文件夹从
builder
阶段复制到/app/apps/web
目录中的相应位置。 - 所有复制的文件和目录的所有权被分配给
web-user:web-group
,以确保正确的权限设置。
-
最终工作目录为:工作目录是
/app/apps/web
,主应用程序就位于这里。 - 启动应用的指令 :
CMD
指令指定了启动应用的命令,即通过npm run start
。
现在我们已经详细讨论了每一步,是时候看看整个源代码了。
FROM node:20-alpine3.19 as base
# 生成特定包的部分单存储库,输出将放置在名为 "out" 的目录中
FROM base AS prune
RUN apk update
RUN apk add --no-cache libc6-compat
WORKDIR /app
RUN npm install turbo --global
COPY . .
RUN turbo prune web --docker
# 负责在 Dockerfile 和私有 GitHub 仓库之间创建身份验证
FROM base AS authentication
WORKDIR /app
# 此步骤仅安装生产和开发依赖项
FROM base AS installer
RUN apk update
RUN apk add --no-cache libc6-compat
WORKDIR /app
# /out/json 目录包含与 "web" 包相关的 package.json 文件
COPY --from=prune /app/out/json/ .
RUN npm install
# 此步骤仅安装生产依赖项
FROM base AS installer-production
RUN apk update
RUN apk add --no-cache libc6-compat
WORKDIR /app
# /out/json 目录包含与 "web" 包相关的 package.json 文件
COPY --from=prune /app/out/json/ .
RUN npm install --only=production
# 使用生产和开发依赖项来构建生产环境
FROM base AS builder
WORKDIR /app
COPY --from=installer /app/node_modules /app/node_modules
# /out/full 目录包含运行 "web" 包的代码文件
COPY --from=prune /app/out/full/ .
RUN npx turbo build --filter=web
# 运行器将使用构建器输出,并仅使用生产依赖项来减小最终镜像的大小
FROM base AS runner
WORKDIR /app
# 不要以 root 身份运行生产进程
RUN addgroup --system --gid 1001 web-group
RUN adduser --system --uid 1001 web-user
USER web-user
# 复制过来生产依赖项、构建和公共目录
COPY --from=installer-production --chown=web-user:web-group /app/node_modules /app/node_modules
COPY --from=prune --chown=web-user:web-group /app/out/json/ .
COPY --from=builder --chown=web-user:web-group /app/apps/web/build /app/apps/web/build
COPY --from=builder --chown=web-user:web-group /app/apps/web/public /app/apps/web/public
WORKDIR /app/apps/web
CMD ["npm", "run", "start"]
要构建此Docker镜像,请在Turborepo根目录下运行以下命令:
docker build -t <image-tag> -f ./apps/web/dockerfile .
此命令用于构建Docker镜像,标签为<image-tag>,使用位于./apps/web/dockerfile的Dockerfile文件。<image-tag>应替换为实际的标签名称。
最后:结论将任何应用程序容器化都可能是一项具有挑战性的任务。理解你的开发工作流程的主要方面以及它们如何运作至关重要。一旦你有了这种理解,你就可以开始构建一个符合仓库规则的Dockerfile。
在这篇文章中,我们带您了解了在 Turborepo 中将应用程序 docker 化应用的过程。虽然我们具体讲解的是一个 Remix 应用程序,但这里提到的工具和技巧也几乎可以原封不动地应用到任何其他框架中。
参考资料 关注我,获取更多精彩内容如果你读到这一步,并且希望获取更多类似的内容,请确保在 Medium 和 LinkedIn 上关注我。
[OOP]:面向对象程序设计
[CRUD]:增删查改
[JVM]:JVM
[SUT]:被测试系统
共同学习,写下你的评论
评论加载中...
作者其他优质文章