cover

容器化部署 Next.js 和 Nest.js 应用

本文介绍了使用容器部署 Next.js 和 Nest.js 应用的方法

2023-10-29

如果使用最基础的方式部署 Next.js 和 Nest.js 应用,会导致镜像体积过大进而影响到镜像构建的效率。

通过将 Next.js 和 Nest.js 应用打包、移除无关依赖并使用体积更小的基础镜像作为运行环境,能有效地减少镜像体积,提高镜像构建效率。

Next.js 应用的镜像文件

Next.js@12 提供了一个 standalone 模式,通过在 next.config.js 中设置该选项,在执行 next build 指令后可以自动创建一个独立文件夹,只复制生产部署所需的必要文件,大幅减少应用体积。

首先修改 next.config.js 配置:

const nextConfig = {
  // ...
  output: "standalone",
  // ...
}
module.exports = nextConfig

如果有使用 next/image 的需求,需要显式地将 sharp 添加为应用的依赖:

npm_config_sharp_binary_host="https://registry.npmmirror.com/-/binary/sharp" \
npm_config_sharp_libvips_binary_host="https://registry.npmmirror.com/-/binary/sharp-libvips" \
npm install sharp --registry=https://registry.npmmirror.com

添加一个编译脚本:

#!/bin/sh
npx next build
cp -r public .next/standalone/public               # 复制 public 文件夹
cp -r .next/static .next/standalone/.next/static   # 复制 static 文件夹
cp .env .next/standalone/.env                      # 复制环境配置文件

如果应用是 MonoRepo 的一部分,需要配置 outputFileTracingRoot,并调整 .scripts/build.sh:

npx next build
cp -r public .next/standalone/<root/packages/app>/public
cp -r .artifacts .next/standalone/<root/packages/app>/.artifacts
cp -r .next/standalone/node_modules .next/standalone/<root/packages/app>/node_modules
cp -r .next/static .next/standalone/<root/packages/app>/.next/static
cp .env .next/standalone/<root/packages/app>/.env

在项目根目录添加 Dockerfile

# 使用标准的基础镜像用于编译
FROM node:18 AS builder
ARG ENV
WORKDIR /app

COPY package.json package-lock.json /app # 或者 yarn.lock

RUN --mount=type=cache,target=/root/.npm,id=npm_cache,sharing=locked \
    npm install --registry="https://registry.npmmirror.com"

COPY . /app
COPY .artifacts/.env.${ENV} /app/.env    # 如果有 build time 配置,需要复制 .env 文件

RUN bash .scripts/build.sh               # 执行编译

# 使用 alpine 镜像作为 Runtime 镜像
FROM node:18-alpine                      

EXPOSE 3000
ENV PORT=3000
WORKDIR /app

RUN --mount=type=cache,target=/tmp/dist,from=builder,source=/app/.next \
    cp -r /tmp/dist/standalone/. /app

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

编译镜像:

docker build . -t next_app --build-arg ENV=staging

镜像大小:

Next.js 应用镜像大小

Nest.js 应用的镜像文件

虽然 Nest.js 官方并没有提供类似 Next.js 的 standalone 的编译模式,但是可以通过 @vercel/ncc 实现类似的结果。

ncc 使用起来非常简单,并且不需要额外的配置就可以实现应用的打包和移除无关依赖。

首先在 Nest.js 应用中添加 @vercel/ncc 依赖:

npm install -D @vercel/ncc

添加编译脚本:

npx ncc build src/main.ts -o dist
cp .env dist/.env

在应用根目录添加如下的 Dockerfile:

FROM node:18 as builder

WORKDIR /app
ARG ENV

COPY package.json package-lock.json /app

RUN --mount=type=cache,target=/root/.npm,id=npm_cache \
    npm install --registry="https://registry.npmmirror.com"

COPY . /app
COPY .artifacts/.env.api.${ENV} /app/packages/api/.env

# build references if necessary
RUN bash .scripts/build.sh

FROM node:18-alpine
EXPOSE 3000
WORKDIR /app

RUN --mount=type=cache,target=/tmp/dist,from=builder,source=/app/dist \
    cp -r /tmp/dist/. /app

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

编译镜像:

docker build . -t nest_app --build-arg ENV=staging

镜像大小:

Nest.js 应用镜像大小

如果 Nest.js 应用是 MonoRepo,则在执行 ncc build 之前,应当将其在该 MonoRepo 下的其他依赖先编译完成(如果使用 lerna 管理 MonoRepo,则可以先使用 lerna run build --scope ... --parallel 将依赖先编译)。

如果在应用中使用了 TypeORM,并且遇到 no metadata for entity was found 错误。可能的原因是在注入 TypeORM 时的 entities 字段配置有误,确保将 autoLoadEntities 字段设置为 true

@Module({
  imports: [
    // ...
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (cs: ConfigService) => {
        const config = {
          type: cs.get("DB_TYPE"),
          host: cs.get("DB_HOST"),
          port: cs.get<number>("DB_PORT"),
          username: cs.get("DB_USERNAME"),
          password: cs.get("DB_PASSWORD"),
          database: cs.get("DB_NAME"),
          // 自动加载 Entites
          autoLoadEntities: true,
          // 指定定义 Entity 的文件夹相对 src 的路径,或将 Entity Class 逐个加入其中
          entities: ["schema/*{.ts,.js}"], 
          synchronize: true,
        } as any;
        return config;
      },
      inject: [ConfigService],
    }),
    // ...
  ]
}
export class AppModule {}