多阶段构建之前的方案
构建镜像最有挑战性之一的就是使镜像尽可能小。Dockerfile
中的每一个指令都会向镜像添加新的层,在移动到下一个图层之前,你需要清理不再需要的历史遗留。要编写一个非常高效的Dockerfile
,传统方式是采用shell
或其它办法使层尽可能小,并确保每个层都能从上一层拿到需要的数据,并且不会多拿。
在 Docker v17.05 版本之前,我们在构建 Docker 镜像时,通常会采用两种方式:
一、全部放入一个 Dockerfile
一种方式是将所有的构建过程包含在一个Dockerfile
中,包括项目及其依赖库的编译、测试、打包等流程,这里可能会带来一些问题:
这里我们以Nest.js
程序为例进行构建。
FROM node:18-slim
WORKDIR /app
COPY package.json pnpm-lock.json ./
RUN pnpm corepack && pnpm install
COPY . .
RUN pnpm build && rm -rf /app/src && rm -rf /app/test
ENTRYPOINT ["pnpm", "start"]
构建镜像
docker build --target builder -t username/imagename:tag
我们不难发现,在Dockerfile
中我们还要执行清理工作,才能构建出符合生产的镜像。
REPOSITORY TAG IMAGE ID CREATED SIZE
mynest 1.0 96fb19125f08 7 seconds ago 534MB
二、分散到多个 Dockerfile
另一种方式,就是我们事先在一个Dockerfile将项目及其依赖库编译测试打包好后,再将其拷贝到运行环境中,这种方式需要我们编写两个Dockerfile和一些额编译脚本才能将两个阶段自动整合起来,这种方式虽然可以规避第一种方式存在的分险,但是部署过程比较繁杂。
多阶段构建方案
为了解决以上问题,Docker v17.05 开始推出了多阶段构建(multistage builds
)。使用多阶段构建我们就可以很容易的解决前面提到的问题,并且只需要一个Dockerfile
就能完成所有工作。
在多阶段构建下,你可以在Dockerfile
中使用多个FROM
声明,每个FROM
声明可以使用不同的基础镜像,并且每个FROM
都使用一个全新的构建阶段。你可以选择性的将一个构建阶段的文件复制到另一个构建阶段,并删除你不想保留在最终镜像中的一切。例如:
# 第一阶段
FROM node:12.16.1-alpine3.11 as builder
WORKDIR /app
COPY package.json pnpm-lock.json ./
RUN pnpm corepack && pnpm install
COPY . .
RUN pnpm build
# 第二阶段
FROM node:12.16.1-alpine3.11
WORKDIR /app
COPY --from=0 /app/package.json /app/pnpm-lock.json ./
RUN pnpm corepack && pnpm install --prod
COPY --from=0 /app/dist ./dist
CMD ["node", "dist/index.js"]
上述Dockerfile示例中,采用了多阶段构建的写法,在一个Dockerfile中,完成了应用程序的编译以及构建,最终只保留了第二阶段的构建产物。
COPY --from=0 /app/package.json /app/package-lock.json ./
表示从第一阶段的构建产物中拷贝文件到当前阶段。
现在只需要一个Dockerfile
文件,就可以解决上述所有问题,我们只需要运行docker build
来构建镜像即可。
docker build -t username/imagename:1.0 .
为构建阶段命名
默认情况下,构建阶段没有命名,使用它们的编号引用来它们,从第一个FROM
以0
开始计数。你也可以为每个构建阶段指定一个名称,给FROM
指令添加as <NAME>
为其构建阶段命名。
FROM node:12.16.1-alpine3.11
# ......
COPY /app/dist ./dist
# 第一阶段
FROM node:12.16.1-alpine3.11
WORKDIR /app
COPY package.json package-lock.json ./
RUN pnpm corepack && pnpm install
COPY . .
RUN pnpm build
# 第二阶段
FROM node:12.16.1-alpine3.11
WORKDIR /app
RUN pnpm corepack && pnpm install --prod
CMD ["node", "dist/index.js"]
# 第一阶段
FROM node:12.16.1-alpine3.11
WORKDIR /app
COPY package.json package-lock.json ./
RUN pnpm corepack && pnpm install
COPY . .
RUN pnpm build
# 第二阶段
FROM node:12.16.1-alpine3.11
WORKDIR /app
RUN pnpm corepack && pnpm install --prod
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]
只构建某一阶段的镜像
我们可以使用as
来为某一阶段命名,例如:
FROM node:18 as builder
当我们只想构建builder阶段的镜像时,增加--target=builder参数即可。
docker build --target builder -t username/imagename:tag