本文将探讨如何利用 pnpm workspace 和 standalone 模式来构建 Next.js 应用程序的轻量级 Docker 镜像。这种方法通过仅在 node_modules
目录中包含必要的文件,显著减少了最终 Docker 镜像的大小。
通常情况下,所有在 dependencies
中列出的包都会被放置在 node_modules
目录中,这会导致镜像体积增大。而在 standalone 模式下构建时,可以从 node_modules
中仅复制必需的文件到一个特定的目录。这样一来,node_modules
将只包含应用程序运行所需的文件,从而大幅减少镜像的大小!
要启用 standalone 模式,请在 next.config.js
文件中添加如下配置:
module.exports = {
output: 'standalone',
};
我们采用了类似于 Turborepo 结构的目录布局:
.
├── apps
│ └── my-app
│ ├── package.json
│ └── ...
├── Dockerfile
├── packages
│ └── other-app
│ └── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── Dockerfile
└── .npmrc
apps
目录包含了作为服务器启动的应用程序,而 packages
目录则包含了 apps
中使用的共享包。
Dockerfile
设计用于维护每个应用程序的独立性,并将其放置在 /apps/my-app
目录下。下面是 Dockerfile
的概览:
本 Dockerfile 采用了多阶段构建的方法来分离关注点并最小化最终镜像的大小。
FROM node:20.12.0-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
# 启用 pnpm
RUN corepack enable
这一阶段设置了 pnpm 的环境。
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
这一阶段安装了所需的依赖项。
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
这一阶段构建了应用程序并准备部署。
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 build -f apps/my-app/Dockerfile --no-cache --target runner --tag myapp:latest --progress=plain
构建后检查镜像:
$ 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
进入容器并查看目录状态来进行调试。