先说效果
把每次构建的镜像差异大小从2GB
优化到了20MB
,构建时间从8分钟
减少到2分钟
。
分析过程
用过Next.js的都知道,为了支持SSR,Next.js项目构建完成后的产物不仅仅是前端的静态资产,还包含可以使用node命令运行的js文件,以及node_modules
文件夹。
这个node_modules
文件夹正是镜像虚胖的罪魁祸首。那么优化大小的方向也很清晰了,就是尽量减少node_modules
的大小。
我在优化过程中也参考了很多人的思路,总体上有以下几种方法:
- 使用 alpine 版本的 node 镜像。
- 使用多阶段构建。将构建阶段分为development,build和production三个阶段。development阶段安装所有依赖包,然后执行duild脚本;在duild阶段仅安装production依赖包;production阶段仅复制生产运行所需的文件:
- package.json
- next.config.js
- .next/
- node_modules/
- public/
- build阶段安装依赖时使用
npm ci
或者yarn --production --frozen-lockfile
经过以上优化,镜像大小可以从2GB左右(你没看错!)减小到 500MB 左右。主要是因为去掉了devDependencies
包以及安装依赖时包管理器缓存的包。
但是如果每次做一些小调整就要构建出一个 500MB 的镜像还是太大了点。在开发过程中,只有依赖包发生变化的时候,node_modules/
文件夹才会发生变化,理想情况下,利用镜像文件的分层特性,我们是可以复用包含node_modules/
文件夹的那一层的,在这一层的基础上复制public/
和.next/
文件夹,这样在代码改动不涉及到依赖变更的时候,镜像的变化就会很小了。
那为什么在依赖没有发生变化的情况下,我们没有办法复用包含node_modules/
的那一层镜像呢?这是因为某些依赖包在安装后会有postinstall的过程,导致每次安装后node_modules/
总会产生一些小变化。
知道这个原因后,解决办法也很简单了:当lockfile发生变化时构建包含node_modules/
文件夹的基础镜像,生产镜像基于这些基础镜像来构建,这样就不需要重复安装依赖包了。
另外分析.next/
文件夹,发现其中有个cache/
文件夹也是运行时所不需要的,应该在执行完build脚本后删掉。
上代码
在依赖包发生变化后,执行以下脚本构建基础镜像。
#!/bin/bash
sha1sum -c ./yarn.lock.sha1
if [ $? == 0 ]; then
echo 'yarn.lock file not changed'
else
echo 'yarn.lock file changed'
IMAGE_TAG=`git describe --tags --dirty --always`
docker build -f ./builder-image.Dockerfile -t web-ui-builder:${IMAGE_TAG} .
docker build -f ./runner-image.Dockerfile -t web-ui-runner:${IMAGE_TAG} .
echo $IMAGE_TAG > ./base-image-tag
sha1sum ./yarn.lock > ./yarn.lock.sha1
fi
然后再构建生产镜像。
#!/bin/bash
IMAGE_TAG=`git describe --tags --dirty --always`
BASE_IMAGE_TAG=`cat ./hack/base-image-tag | sed -e 's/^[[:space:]]*//'`
docker build \
--build-arg BASE_IMAGE_TAG=${BASE_IMAGE_TAG} \
-f ./production-image.Dockerfile \
-t web-ui:${IMAGE_TAG} .
builder-image.Dockerfile
# Install dependencies only when needed
FROM node:18.14-alpine as builder
# 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
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
FROM node:18.14-alpine
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY --from=builder /app ./
runner-image.Dockerfile
# Install dependencies only when needed
FROM node:18.14-alpine as builder
# 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
COPY package.json yarn.lock ./
RUN yarn install --production --ignore-scripts --prefer-offline --frozen-lockfile
FROM node:18.14-alpine
WORKDIR /app
COPY --from=builder /app ./
production-image.Dockerfile
ARG BASE_IMAGE_TAG=latest
FROM web-ui-builder:$BASE_IMAGE_TAG AS builder
WORKDIR /app
COPY . .
RUN yarn build
RUN rm -rf ./.next/cache
FROM web-ui-runner:$BASE_IMAGE_TAG AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
# You only need to copy next.config.js if you are NOT using the default configuration
COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder /app/package.json ./package.json
USER nextjs
EXPOSE 3000
# 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.
ENV NEXT_TELEMETRY_DISABLED 1
CMD ["yarn", "start"]