抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

目前项目中有个服务使用的是 Next.js 框架,在生产部署的时候,因为它build 之后不是一个纯静态的东西,并且需要依赖node_modules中的一些依赖,传统的部署方式导致整个包特别大。之前有想过如何将只用到的依赖拿出来,然后打成镜像部署,但是因为自己水平有限,身为后端对这块不太清楚,最终以失败告终。但是现在因为某些原因,决定重新将解决这个问题。

现在要考虑的问题

  1. 如何将前端项目打成镜像
  2. 如何将必须的东西打进镜像
  3. 如何最小化镜像

如何将前端项目打成镜像

这个问题很简单,只需要按照项目正常构建过程执行就行,只不过是在Dockerfile 流程化完成了这一步

查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 使用官方的Node.js slim镜像作为基础镜像
FROM node:16-alpine

# 设置工作目录
WORKDIR /home/app

# 将当前目录的内容复制到工作目录中
COPY . .

# 安装项目依赖
RUN npm install

# 构建项目
RUN npm run build

# 替换默认的nginx配置文件
# 这一步是可选的,如果你打算使用nginx作为反向代理服务器
# COPY nginx.conf /etc/nginx/nginx.conf

# 指定容器运行时的命令
CMD ["npm", "start"]

# 暴露应用的端口
EXPOSE 3000

如何将必须的东西打进镜像

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

因为node_modules 是必须的,但其中的依赖不是所有都需要,现在需要考虑如何移除node_modules中的非必要依赖。

通过查阅资料,Next.js|output,发现官方文档中有一栏专门解决这个问题。

output

During a build, Next.js will automatically trace each page and its dependencies to determine all of the files that are needed for deploying a production version of your application.

This feature helps reduce the size of deployments drastically. Previously, when deploying with Docker you would need to have all files from your package’s dependencies installed to run next start. Starting with Next.js 12, you can leverage Output File Tracing in the .next/ directory to only include the necessary files.

Furthermore, this removes the need for the deprecated serverless target which can cause various issues and also creates unnecessary duplication.

翻译如下

在生成过程中,Next.js 将自动跟踪每个页面及其依赖项,以确定部署应用程序的生产版本所需的所有文件。

此功能有助于大幅减小部署规模。以前,使用 Docker 进行部署时,需要 dependencies 安装包中的所有文件才能运行 next start 。从Next.js 12 开始,您可以利用 .next/ 目录中的输出文件跟踪仅包含必要的文件。

此外,这消除了对已 serverless 弃用目标的需求,这可能会导致各种问题,并产生不必要的重复。

具体原理详见https://nextjs.org/docs/pages/api-reference/next-config-js/output

总结来说:

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

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

1
2
3
4
5
6
7
const nextConfig = {
// ...
output: "standalone",
// ...
}
module.exports = nextConfig

在执行 npm build 之后,会生成如下结构的目录

2024-06-26_10-14-26

这将创建一个文件夹 .next/standalone ,然后可以在该文件夹中自行部署,而无需安装 node_modules

此外,还输出了一个最小的 server.js 文件,可以使用它来代替 next start .默认情况下,此最小服务器不会复制 public or .next/static 文件夹,因为理想情况下,这些文件夹应由 CDN 处理,尽管这些文件夹可以手动复制到 standalone/public and standalone/.next/static 文件夹,之后 server.js 文件将自动提供这些文件夹。

这个过程其实就可以大大缩减镜像的大小了,因为去除了无必须要的node_modules 依赖。

如何最小化镜像

最后,如何将镜像最小化构建是我们的最终目标。

遵循 Dockerfile 的最佳实践

  • 尽量使用官方的基础镜像,Docker推荐使用Alpine的镜像。
  • 使用多阶段构建
  • 使用.dockerignore去除无关的文件
  • 创建临时容器
  • 不要安装不用的包
  • 解耦应用程序
  • 利用缓存构建镜像
  • 拆分复杂的RUN命令为多行,并用 / 分割

更多详细的最佳实践可以参考官方文档:https://docs.docker.com/develop/develop-images/instructions/

我们这次只考虑 使用多阶段构建 后续再专门研究

编写的Dockerfile 如下(不是最终版)

查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 设置基础依赖层,使用Node 16并安装必要兼容库
FROM node:16-alpine AS dependencies

WORKDIR /home/app
COPY package.json package-lock.json ./

# 安装依赖,仅当package.json或package-lock.json有变动时重新安装
# 在构建镜像的过程中临时安装一些构建依赖,然后在安装完成后立即删除它们,以此来减小最终生成的镜像体积
RUN apk add --no-cache --virtual .build-deps libc6-compat \
&& npm ci --only=prod \
&& apk del .build-deps

# 设置构建层,基于依赖层并执行构建
FROM dependencies AS builder

ARG NODE_ENV
ENV NODE_ENV=$NODE_ENV
ENV NEXT_TELEMETRY_DISABLED=1

COPY . .
RUN npm run build

# 创建生产运行层,使用轻量级Node镜像并最小化体积
FROM node:16-alpine AS runner

WORKDIR /home/app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

# 只复制生产构建产物和公共静态资源
COPY --from=builder /home/app/.next/standalone ./standalone
COPY --from=builder /home/app/public ./standalone/public
COPY --from=builder /home/app/static ./standalone/static
COPY --from=builder /home/app/.next/static /home/app/standalone/.next/static

# 暴露端口与启动命令
EXPOSE 3000
ENV PORT 3000
CMD ["node", "./standalone/server.js"]

如上最终将部署的650M 的服务(有点扯蛋),改成了150M左右,结果还是不错的,但是任重而道远,还有许多地方要优化,一步一步来吧。

  • 如何进一步减小镜像大小
  • 如何缩短构建的时间

Next.js 官方 Dockerfile 样例

https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile

查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
FROM node:18-alpine AS base

# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi


# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1

RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000

# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD HOSTNAME="0.0.0.0" node server.js

参考文章

评论