如果使用最基础的方式部署 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.config.js
如果有使用 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 # 复制环境配置文件
.scripts/build.sh
如果应用是 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
.scripts/build.sh
在项目根目录添加 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"]
Dockerfile
编译镜像:
docker build . -t next_app --build-arg ENV=staging
镜像大小:
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
.scripts/build.sh
在应用根目录添加如下的 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"]
Dockerfile
编译镜像:
docker build . -t nest_app --build-arg ENV=staging
镜像大小:
如果 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 {}
src/app.module.ts