Dockerfile 是一个用来构建镜像的文本文件,它包含了一条条指令,大部分指令都对应构建一层镜像。并且,我们构建镜像时,都需要基于一个基础镜像,通过 FROM 指令来引入。

多阶段构建概念

当我们的镜像构建过程需要依赖多个基础镜像时, 我们没办法在 FROM 指令中加入多个基础镜像来实现,事实上也没必要在我们构建的镜像中包含多个基础镜像,因为除了运行时环境基础镜像,其它都只是为编译、打包之类中间过程服务,在运行时不再需要,没必要添加在镜像中增大镜像的大小。此时我们可以使用 Dockerfile 的 多阶段构建 来实现。

所谓多阶段构建,就是允许一个 Dockerfile 中出现多条 FROM 指令,但只有最后一条 FROM 指令中的基础镜像作为本次构建镜像的基础镜像,其它均只为中间过程服务。

每一条 FROM 指令都表示一个构建阶段,多条 FROM 指令就表示多阶段构建,后面的构建阶段可以拷贝前面构建阶段产生的文件 —— 这也是多阶段构建的意义所在。

多阶段构建适用于编译、打包与运行环境分离的场景。事实上,在前文 使用YApi来统一管理项目API文档(Docker部署) 中, 我们已经使用了这一技术。以下即以此为例进行说明。

多阶段构建实践

我们构建一个 YApi 镜像包括几步:

  1. 下载 YApi 源码
  2. 安装依赖,编译
  3. 将编译好的文件夹拷贝到镜像目录

在 1,2 步中,我们需要 wget,python,make 等工具来完成,这些工具对 YApi 程序的运行是非必需的。所以我们有两种选择,一是先在宿主机中把 1,2 完成,然后只在 Dockerfile 中执行 3, 二是将这 1,2,3 都放在 Dockerfile 中执行,在编译完成后,再执行安装工具的清理工作。

两个选择对应两个问题:

  1. 宿主机中可能不存在需要的工具或依赖,安装起来麻烦,且需要手动(或配置自动化程序)执行
  2. 程序运行的环境可能跟编译打包的环境要求完全不一样,如 go 语言程序, 编译需要 golang 环境,而运行则不需要

我们采用多阶段构建则可以比较完美地解决以上问题。构建 YApi 镜像的 Dockerfile 如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FROM node:12-alpine as builder
WORKDIR /yapi
RUN apk add --no-cache wget python make
ENV VERSION=1.9.2
RUN wget https://github.com/YMFE/yapi/archive/refs/tags/v${VERSION}.zip
RUN unzip v${VERSION}.zip && mv yapi-${VERSION} vendors
RUN cd /yapi/vendors && npm install --production --registry https://registry.npm.taobao.org

FROM node:12-alpine
MAINTAINER ronwxy
ENV TZ="Asia/Shanghai"
WORKDIR /yapi/vendors
COPY --from=builder /yapi/vendors /yapi/vendors
RUN mv /yapi/vendors/config_example.json /yapi/config.json
EXPOSE 3000
ENTRYPOINT ["node"]

在该 Dockerfile 中, 两条 FROM 指令代表两个阶段的构建,第一阶段完成步骤 1,2, 在第二阶段中可以直接使用第一阶段生成的内容。
指令 COPY --from=builder /yapi/vendors /yapi/vendors 就表示从命名为 builder 的阶段中拷贝 /yapi/vendors 到当前阶段中

也可以使用 --from=n 来表示从第几个阶段中拷贝内容,n从0开始计

COPY --from 除了能从前面的构建阶段中拷贝内容,还能直接从已经存在的镜像中拷贝,这在我们需要依赖其它已存在镜像中的某些内容时非常方便实用。

评论