继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

使用Docker打包Turborepo Remix应用指南

倚天杖
关注TA
已关注
手记 364
粉丝 47
获赞 187

将 Remix 应用程序 Docker 化

最近,我有机会接触到了一种特别让我印象深刻的科技:Turborepo。Turborepo 是一个专门为 JavaScript 和 TypeScript 优化的增量构建工具和打包系统,使用 Rust 语言编写。

在这个项目期间,我们被要求将仓库中的一个应用程序docker化,具体来说,我们要docker化的就是一个Remix应用程序。在这篇文章中,我将引导你完成实现此目标的过程,展示我们如何利用Turborepo和Docker一起工作,构建一个尽可能小的最终Docker镜像。

查看完整代码和更多细节,请浏览一下我的 GitHub:GitHub 仓库

前提条件
  • 确保你已经安装了 Turborepo CLI。如果没有安装的话,请参阅Turborepo 文档以获取详细的安装指南。
一个对 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 命令”部分提到的目录中。
第三个阶段:「Dev 安装程序」
    # 此步骤仅安装生产和开发依赖项  
    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"]
  1. 基础镜像:运行阶段从 base 镜像开始,这是一个仅包含必要运行时依赖的最小镜像。
  2. 工作目录:工作目录设置为 /app,应用程序代码和依赖项将存放在此目录。
  3. 非 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,以确保正确的权限设置。
  1. 最终工作目录为:工作目录是 /app/apps/web,主应用程序就位于这里。

  2. 启动应用的指令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 应用程序,但这里提到的工具和技巧也几乎可以原封不动地应用到任何其他框架中。

参考资料
关注我,获取更多精彩内容

如果你读到这一步,并且希望获取更多类似的内容,请确保在 MediumLinkedIn 上关注我。
[OOP]:面向对象程序设计
[CRUD]:增删查改
[JVM]:JVM
[SUT]:被测试系统

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP