使用 pnpm workspace 和 standalone 模式构建 Next.js 的 Docker 镜像

引言

本文将探讨如何利用 pnpm workspace 和 standalone 模式来构建 Next.js 应用程序的轻量级 Docker 镜像。这种方法通过仅在 node_modules 目录中包含必要的文件,显著减少了最终 Docker 镜像的大小。

Standalone 模式简介

通常情况下,所有在 dependencies 中列出的包都会被放置在 node_modules 目录中,这会导致镜像体积增大。而在 standalone 模式下构建时,可以从 node_modules 中仅复制必需的文件到一个特定的目录。这样一来,node_modules 将只包含应用程序运行所需的文件,从而大幅减少镜像的大小!

要启用 standalone 模式,请在 next.config.js 文件中添加如下配置:

javascript 复制代码
module.exports = {
  output: 'standalone',
};

目录结构

我们采用了类似于 Turborepo 结构的目录布局:

plaintext 复制代码
.
├── apps
│   └── my-app
│       ├── package.json
│       └── ...
├── Dockerfile
├── packages
│   └── other-app
│       └── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── Dockerfile
└── .npmrc

apps 目录包含了作为服务器启动的应用程序,而 packages 目录则包含了 apps 中使用的共享包。

Dockerfile 概览

Dockerfile 设计用于维护每个应用程序的独立性,并将其放置在 /apps/my-app 目录下。下面是 Dockerfile 的概览:

多阶段构建

本 Dockerfile 采用了多阶段构建的方法来分离关注点并最小化最终镜像的大小。

第一阶段:基础环境(base)

Dockerfile 复制代码
FROM node:20.12.0-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"

# 启用 pnpm
RUN corepack enable

这一阶段设置了 pnpm 的环境。

第二阶段:依赖安装(deps)

Dockerfile 复制代码
FROM base AS deps

WORKDIR /app

# 复制 `pnpm install` 所需的文件
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
COPY ./apps/my-app/package.json /app/apps/my-app/package.json
COPY ./packages/ /app/packages/

RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
    pnpm install --frozen-lockfile

这一阶段安装了所需的依赖项。

第三阶段:构建器(builder)

Dockerfile 复制代码
FROM base AS builder

WORKDIR /app

# 复制已安装的 `node_modules` 从 deps 阶段
COPY --from=deps /app/node_modules ./node_modules
# 复制构建目标的目录
COPY ./apps/my-app ./apps/my-app
COPY --from=deps /app/packages ./packages
COPY --from=deps /app/.npmrc ./

# 展开符号链接并复制到 pruned 目录
RUN pnpm --filter=@monorepo/my-app deploy /pruned

WORKDIR /pruned

# 构建应用程序
RUN pnpm --filter=@monorepo/my-app build

这一阶段构建了应用程序并准备部署。

第四阶段:运行器(runner)

Dockerfile 复制代码
FROM base AS runner

WORKDIR /app

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

# 复制已构建的文件从 builder 阶段
COPY --from=builder /app/apps/my-app/public ./public
COPY --from=builder /pruned/.next/standalone .
COPY --from=builder /pruned/.next/static ./.next/static

CMD ["node", "server.js"]

这一阶段是启动应用程序的实际阶段。

构建 Docker 镜像

使用以下命令来构建 Docker 镜像并检查其大小:

bash 复制代码
$ docker build -f apps/my-app/Dockerfile --no-cache --target runner --tag myapp:latest --progress=plain

构建后检查镜像:

bash 复制代码
$ docker images

您将看到一个准备就绪且体积轻量的镜像。

结论

尽管在过程中遇到了一些挑战,比如理解 pnpm deploy 命令及管理多个应用程序在单一 workspace 中的过程,但最终还是成功地构建了一个高效且轻量的 Docker 镜像。

遇到的问题

  • 管理多个应用程序的复杂性 :尝试在一个 Dockerfile 中处理所有应用程序目录下的应用,导致 --build-arg 参数增加,使得 Dockerfile 变得繁琐。
    • 解决方案 :通过在每个 apps 目录中配置 Dockerfile 来避免这种情况。这样还可以针对每个应用程序单独进行 Node.js 的更新等操作。
  • pnpm deploy -> build 流程的理解不足 :对 pnpm 通过符号链接工作的方式以及在 deploy 后将实际文件复制的行为理解不够深入,导致应用程序启动失败。
    • 解决方案 :将 docker build--target 参数更改为 builder,然后通过 docker run -it my-app /bin/bash 进入容器并查看目录状态来进行调试。

参考链接

复制代码
相关推荐
漂流瓶jz5 小时前
总结CSS组件化演进之路:命名规范/CSS Modules/CSS in JS/原子化CSS
前端·javascript·css
budingxiaomoli5 小时前
Spring IoC &DI
java·spring·ioc·di
Spider Cat 蜘蛛猫6 小时前
Springboot SSO系统设计文档
java·spring boot·后端
未若君雅裁6 小时前
MySQL高可用与扩展-主从复制读写分离分库分表
java·数据库·mysql
学习中.........6 小时前
从扰动函数的变化,感受红黑树带来的性能提升
java
steven~~~6 小时前
为什么mq报错
javascript
修己xj6 小时前
别再让Docker占满你的硬盘!一篇搞定docker system所有命令
docker
计算机安禾6 小时前
【c++面向对象编程】第24篇:类型转换运算符:自定义隐式转换与explicit
java·c++·算法
weixin199701080167 小时前
【保姆级教程】淘宝/天猫商品详情 API(item_get)接入指南:Python/Java/PHP 调用示例与 JSON 返回值解析
java·python·php
环流_7 小时前
redis核心数据类型在java中的操作
java·数据库·redis