BackIcon加速多架构 Next.js 镜像构建

2024年10月25日

Openai logomark

Hamster1963

本篇博客藏了一枚 Follow 邀请码,欢迎大家寻宝👏。

前言

近期,开源项目 nezha-dash 引入了 Docker 部署方式。为了兼容 linux-arm64 架构,项目被打包成多架构镜像并发布到了 Docker Hub。

在此过程中,通过不断改进配置和工具链,显著提升了打包 Next.js 多架构镜像的效率。本文将简单分享这一优化过程。

Next.js 打包

在 Next.js 中,在配置文件(例如:next.config.mjs )中,将 output 配置项设置为 standalone ,即可打包成可使用 node 进行启动的独立前端服务。

ray-so-export.png

因此,将 Next.js 打包成镜像的步骤也十分简单,只需要将 Next.js 构建后的文件放到包含 node 环境的镜像内,设置好启动命令即可完成镜像打包。

官方示例

在 Next.js 的官方文档中,提供了一个 Dockerfile 示例。

alt: 由于步骤较为繁琐冗长,因此进行了模糊化处理。

由于步骤较为繁琐冗长,因此进行了模糊化处理。

在官方的示例中,采用了分布构建的方式,将构建分为四步:

  1. 安装构建所需系统内核库
  2. 安装项目依赖
  3. 构建项目
  4. 将构建后的文件与静态文件放入运行镜像中,配置启动命令。

官方示例中采用 node:18-alpine 作为基础镜像,分别进行上述的四个步骤,而其中对于构建性能的优化并没有特别完善。

  • 由于 alpine 的精简化,在构建前需要安装系统依赖库
  • 在包安装上,也许会花费许多时间

首先,我们针对包管理器与基础镜像这两个模块进行优化。

使用Bun进行加速

在项目中,采用 Bun 作为包管理器,关于 Bun 的介绍,可查看:

Bun — A fast all-in-one JavaScript runtime

通过 Bun 来安装与管理项目中的包,不仅可以以极快的速度安装项目依赖,还可以配合官方的 oven/bun 镜像来加速 Docker 构建。

以下将采用 Bun 作为工具,逐步优化镜像打包性能与镜像大小。

1. 使用 Bun 作为包管理器与基础容器

FROM oven/bun:1 AS base

# Stage 1: Install dependencies
FROM base AS deps
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile

# Stage 2: Build the application
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN bun run build

# Stage 3: Production image
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production

COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

EXPOSE 3000
CMD ["bun", "run", "server.js"]

在 Dockerfile 中,将 oven/bun:1 作为我们的基础镜像,分布进行构建任务:

  1. 使用 Bun 安装依赖
  2. 使用 bun run build 指令(定义在 package.json 中)构建 Next.js 服务
  3. 将构建好的文件与静态文件放入运行镜像,定义启动命令

相对于官方的示例,不仅步骤简洁了不少,无需手动安装额外的系统依赖。

无标题-2024-10-23-2226.png

图上方是pnpm安装包所需时间,下方是bun安装包所需时间,可以看到,使用Bun作为包管理器后,在包安装的速度上也有很大提升。

2.优化最终镜像大小

在使用 Bun 打包镜像后,对于先前的镜像,我们会发现镜像大小显著地增加了,从先前的 67M 膨胀到了目前的 109 M,因此,我们需要对最终的运行镜像大小进行优化。

CleanShot 2024-10-25 at 09.18.11@2x.png

在官方的示例中,采用了 alpine 系统作为基础镜像,同样的,我们可以保留 oven/bun 作为依赖安装与构建镜像,而将运行镜像改为更加精简的 oven/bun:alpine 。

FROM oven/bun:1 AS base

# Stage 1: Install dependencies
FROM base AS deps
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile

# Stage 2: Build the application
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN bun run build

# Stage 3: Production image
FROM oven/bun:1-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

EXPOSE 3000
CMD ["bun", "run", "server.js"]

可以看到,重新构建后,镜像大小回到了先前最初的大小。

CleanShot 2024-10-25 at 09.19.06@2x.png

使用统一构建进行加速多架构镜像

多架构镜像

随着 Arm 设备近几年的不断发展,多架构支持的镜像也是十分必要的,在 Arm 设备上虽然也可以运行 Amd64 架构的镜像,但难免在性能上会收到影响。

因此,在发布打包镜像时,可以将 Amd64 与 Amd64 架构放在同一个镜像仓库中,作为多架构镜像进行分发。

在项目中采用 GitHub Actions,通过 git tag 的方式进行触发打包。

name: Build and push Docker image

on:
  push:
    tags:
      - 'v*'

env:
  REGISTRY_IMAGE: hamster1963/nezha-dash
  ALIYUN_REGISTRY_IMAGE: registry.cn-guangzhou.aliyuncs.com/hamster-home/nezha-dash

jobs:
  build-and-push:
    name: Build and push Docker image
    runs-on: ubuntu-latest
    environment: Production
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
        with:
          driver-opts: network=host

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Login to AliYun Container Registry
        uses: docker/login-action@v3
        with:
          registry: registry.cn-guangzhou.aliyuncs.com
          username: ${{ secrets.ALI_USERNAME }}
          password: ${{ secrets.ALI_TOKEN }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: |
            ${{ env.REGISTRY_IMAGE }}
            ${{ env.ALIYUN_REGISTRY_IMAGE }}
          tags: |
            type=raw,value=latest
            type=ref,event=tag

      - name: Build and push Docker image
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

在 Build and push Docker image 任务中,我们使用 docker/build-push-action@v6 作为工作流工具,在 platforms 参数中,可以很方便地将目标架构传入,Docker 会为我们自动构建这些架构的镜像。

构建速度问题

在首次构建后,会发现,在构建 linux/amd64 架构的镜像时,速度很快,大约 50 秒就可以完成构建,但是构建 linux/arm64 架构镜像时却花费了惊人的 10 分钟,这其中一定发生了什么。

CleanShot 2024-10-25 at 09.30.19@2x.png

实际上,linux/arm64 构建速度如此慢的原因是由于仿真模拟的性能损耗。

仿真模拟与交叉编译

image.png

在目前我们编写的 Dockerfile 下,在为 linux/arm64 架构构建镜像时,是完全采用仿真编译的方式进行的,意味着构建过程需要在架构之间不断转换它们的指令,因此构建性能会收到很大的影响。

因此我们可以朝着交叉编译的方向进行优化,将全部的构建过程放在宿主机的架构上,这样子无需仿真,性能也不会受到太大的影响。

Next.js 交叉编译

对于 Next.js 而言,编译的过程实际上是一系列优化和转译,如果项目使用了 Typescript,则转换为最终的 Javascript ,并进行各种的混淆,压缩。

对于最终产物,由于 Javascript 语言的特性,只需要有 node,即可启动 Next.js 服务,也就是说:

Next.js 的编译产物与架构无关

image.png

了解这一点后,Next.js 的交叉编译无非就是将编译产物分发到不同架构镜像中的过程。

统一构建

最终,我们将构建的过程在宿主机架构的镜像中进行,并将其分发到不同架构的运行镜像中,而改造过程也十分简单,只需将我们的 base 容器与架构关联起来。

FROM oven/bun:1 AS base

变成

FROM --platform=$BUILDPLATFORM oven/bun:1 AS base

编译后,一样地,从 builder 中分发到不同架构的runner中:

FROM oven/bun:1-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

需要注意⚠️的是:

FROM oven/bun:1-alpine AS runner

默认会使用--target=$TARGETPLATFORM 的镜像,也就是目标架构的镜像,因此我们可以省略这一参数。

让我们开始构建吧!

FROM --platform=$BUILDPLATFORM oven/bun:1 AS base

# Stage 1: Install dependencies
FROM base AS deps
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile

# Stage 2: Build the application
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN bun run build

# Stage 3: Production image
FROM oven/bun:1-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

EXPOSE 3000
CMD ["bun", "run", "server.js"]

最终,通过一系列的优化,构建多架构镜像的时间:

CleanShot 2024-10-25 at 10.04.44@2x.png

15m 25s → 2m 14s

CleanShot 2024-10-25 at 10.03.32@2x.png

完整的 GitHub Actions 文件、Dockerfile 与构建记录,可在我的开源项目中查看:

https://github.com/hamster1963/nezha-dash