使用 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 进入容器并查看目录状态来进行调试。

参考链接

复制代码
相关推荐
xlsw_1 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹2 小时前
基于java的改良版超级玛丽小游戏
java
鸠摩智首席音效师2 小时前
Docker 中如何限制CPU和内存的使用 ?
docker·容器
燃先生._.2 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
Michaelwubo2 小时前
Docker dockerfile镜像编码 centos7
运维·docker·容器
jingyu飞鸟2 小时前
centos-stream9系统安装docker
linux·docker·centos
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭2 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
好像是个likun2 小时前
使用docker拉取镜像很慢或者总是超时的问题
运维·docker·容器
暮湫3 小时前
泛型(2)
java