docker系列—Dockerfile 多阶段构建原理和使用场景

Docker 17.05版本以后,新增了Dockerfile多阶段构建。所谓多阶段构建,实际上是允许一个Dockerfile 中出现多个FROM 指令。这样做有什么意义呢?

老版本Docker中为什么不支持多个 FROM 指令

在17.05版本之前的Docker,只允许Dockerfile中出现一个FROM指令,这得从镜像的本质说起。

Docker概念简介 中我们提到,你可以简单理解Docker的镜像是一个压缩文件,其中包含了你需要的程序和一个文件系统。其实这样说是不严谨的,Docker镜像并非只是一个文件,而是由一堆文件组成,最主要的文件是 层。

Dockerfile 中,大多数指令会生成一个层,比如下方的两个例子:

# 示例一,foo 镜像的Dockerfile
# 基础镜像中已经存在若干个层了
FROM ubuntu:16.04

# RUN指令会增加一层,在这一层中,安装了 git 软件
RUN apt-get update \
  && apt-get install -y --no-install-recommends git \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/*
# 示例二,bar 镜像的Dockerfile
FROM foo

# RUN指令会增加一层,在这一层中,安装了 nginx
RUN apt-get update \
  && apt-get install -y --no-install-recommends nginx \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/*

假设基础镜像 ubuntu:16.04已经存在5层,使用第一个Dockerfile打包成镜像 foo,则foo有6层,又使用第二个Dockerfile打包成镜像bar,则bar中有7层。

如果ubuntu:16.04 等其他镜像不算,如果系统中只存在 foo 和 bar 两个镜像,那么系统中一共保存了多少层呢?

是7层,并非13层,这是因为,foo和bar共享了6层。层的共享机制可以节约大量的磁盘空间和传输带宽,比如你本地已经有了foo镜像,又从镜像仓库中拉取bar镜像时,只拉取本地所没有的最后一层就可以了,不需要把整个bar镜像连根拉一遍。但是层共享是怎样实现的呢?

原来,Docker镜像的每一层只记录文件变更,在容器启动时,Docker会将镜像的各个层进行计算,最后生成一个文件系统,这个被称为 联合挂载。对此感兴趣的话可以进入了解一下 AUFS

Docker的各个层是有相关性的,在联合挂载的过程中,系统需要知道在什么样的基础上再增加新的文件。那么这就要求一个Docker镜像只能有一个起始层,只能有一个根。所以,Dockerfile中,就只允许一个 FROM指令。因为多个FROM 指令会造成多根,则是无法实现的。但为什么 Docker 17.05 版本以后允许 Dockerfile支持多个 FROM 指令了呢,莫非已经支持了多根?

多个 FROM 指令的意义

多个 FROM 指令并不是为了生成多根的层关系,最后生成的镜像,仍以最后一条 FROM 为准,之前的 FROM 会被抛弃,那么之前的FROM 又有什么意义呢?

每一条 FROM 指令都是一个构建阶段,多条 FROM 就是多阶段构建,虽然最后生成的镜像只能是最后一个阶段的结果,但是,能够将前置阶段中的文件拷贝到后边的阶段中,这就是多阶段构建的最大意义。

最大的使用场景是将编译环境和运行环境分离,比如,之前我们需要构建一个Go语言程序,那么就需要用到go命令等编译环境,我们的Dockerfile可能是这样的:

# Go语言环境基础镜像
FROM golang:1.10.3

# 将源码拷贝到镜像中
COPY server.go /build/

# 指定工作目录
WORKDIR /build

# 编译镜像时,运行 go build 编译生成 server 程序
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOARM=6 go build -ldflags '-w -s' -o server

# 指定容器运行时入口程序 server
ENTRYPOINT ["/build/server"]

基础镜像golang:1.10.3是非常庞大的,因为其中包含了所有的Go语言编译工具和库,而运行时候我们仅仅需要编译后的server程序就行了,不需要编译时的编译工具,最后生成的大体积镜像就是一种浪费。

最后将编译接口拷贝到镜像中就行了,那么Dockerfile的基础镜像并不需要包含Go编译环境:

# 不需要Go语言编译环境
FROM scratch

# 将编译结果拷贝到容器中
COPY server /server

# 指定容器运行时入口程序 server
ENTRYPOINT ["/server"]

提示:scratch 是内置关键词,并不是一个真实存在的镜像。 FROM scratch 会使用一个完全干净的文件系统,不包含任何文件。因为Go语言编译后不需要运行时,也就不需要安装任何的运行库。FROM scratch可以使得最后生成的镜像最小化,其中只包含了 server程序。

在 Docker 17.05版本以后,就有了新的解决方案,直接一个Dockerfile就可以解决:

# 编译阶段
FROM golang:1.10.3

COPY server.go /build/

WORKDIR /build

RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOARM=6 go build -ldflags '-w -s' -o server

# 运行阶段
 FROM scratch

# 从编译阶段的中拷贝编译结果到当前镜像中
COPY --from=0 /build/server /

ENTRYPOINT ["/server"]

这个 Dockerfile 的玄妙之处就在于 COPY 指令的–from=0 参数,从前边的阶段中拷贝文件到当前阶段中,多个FROM语句时,0代表第一个阶段。除了使用数字,我们还可以给阶段命名,比如:

# 编译阶段 命名为 builder
FROM golang:1.10.3 as builder

# ... 省略

# 运行阶段
FROM scratch

# 从编译阶段的中拷贝编译结果到当前镜像中
COPY --from=builder /build/server /

更为强大的是,COPY --from 不但可以从前置阶段中拷贝,还可以直接从一个已经存在的镜像中拷贝。比如,

FROM ubuntu:16.04

COPY --from=quay.io/coreos/etcd:v3.3.9 /usr/local/bin/etcd /usr/local/bin/

我们直接将etcd镜像中的程序拷贝到了我们的镜像中,这样,在生成我们的程序镜像时,就不需要源码编译etcd了,直接将官方编译好的程序文件拿过来就行了。

有些程序要么没有apt源,要么apt源中的版本太老,要么干脆只提供源码需要自己编译,使用这些程序时,我们可以方便地使用已经存在的Docker镜像作为我们的基础镜像。但是我们的软件有时候可能需要依赖多个这种文件,我们并不能同时将 nginx 和 etcd 的镜像同时作为我们的基础镜像(不支持多根),这种情况下,使用 COPY --from 就非常方便实用了。

<think>好的,我现在需要回答用户关于Dockerfile多阶段构建使用教程示例的问题。首先,我要确保自己正确理解多阶段构建的概念。根据引用中的信息,多阶段构建允许一个Dockerfile中出现多个FROM指令,这样可以分离构建环境运行环境,减少最终镜像的大小。这一点在Docker 17.05之后才支持,用户可能需要知道版本要求。 接下来,我需要整理多阶段构建的基本结构。通常分为构建阶段运行阶段。构建阶段安装所有必要的依赖并编译应用,而运行阶段只复制必要的文件并使用更小的基础镜像,比如Alpine。这样能有效减小镜像体积,提升安全性,因为运行环境不包含构建工具。 然后,我需要考虑用户可能需要具体的示例来理解如何操作。根据引用中的例子,可以展示一个两阶段的Dockerfile,比如第一阶段用Golang构建,第二阶段复制二进制文件到Alpine镜像。同时,用户可能想知道如何命名阶段,使用AS关键字,以及如何通过--target参数来指定构建某个阶段,这在调试时很有用,如引用3提到的。 另外,用户可能关心如何从外部镜像复制文件,比如使用COPY --from指令。这可能涉及到多阶段构建中的不同来源,比如其他镜像或之前的阶段。这部分需要明确说明语法用法。 还要提到最佳实践,比如选择合适的基础镜像、清理不必要的文件、利用缓存加速构建等。这些内容在引用4中有所提及,例如构建阶段运行阶段的分离,避免携带不必要的依赖。 用户可能遇到的问题包括如何处理多个阶段的依赖关系,或者如何优化构建过程。需要确保示例清晰,步骤明确,避免混淆。同时,注意使用中文回答,并正确应用LaTeX格式,虽然在这个问题中数学公式可能不多,但如果有变量或代码结构,需正确使用$...$或$$...$$。 最后,生成相关问题时要围绕多阶段构建的常见疑问,比如与单阶段的对比、版本兼容性、调试技巧等。确保问题覆盖用户可能进一步探索的方向。</think>### Dockerfile多阶段构建教程与示例 #### 一、多阶段构建的核心作用 多阶段构建通过多个`FROM`指令将构建过程分为多个独立阶段,核心价值体现在: 1. **减少镜像体积**:仅保留运行时必需的文件,丢弃构建工具中间产物,例如最终镜像可缩小80%(如从1.2GB降至200MB)[^4] 2. **提升安全性**:运行环境不包含编译器、调试工具等潜在攻击面 3. **简化流水线**:无需维护多个Dockerfile或手动文件拷贝操作[^2] #### 二、基础语法结构 ```dockerfile # 第一阶段:构建环境 FROM golang:1.19 AS builder WORKDIR /app COPY . . RUN go build -o myapp . # 第二阶段:运行环境 FROM alpine:3.14 WORKDIR /app COPY --from=builder /app/myapp . CMD ["./myapp"] ``` #### 三、关键操作技巧 1. **阶段命名**:使用`AS`定义阶段别名(如`builder`),方便后续引用 2. **文件跨阶段复制**:`COPY --from=阶段名` 支持: - 从指定构建阶段复制(`--from=builder`) - 从外部镜像复制(`--from=nginx:latest`)[^3] 3. **选择性构建**:通过`docker build --target builder -t dev-image .`仅构建指定阶段,适合调试 #### 四、最佳实践示例 ```dockerfile # 阶段1:依赖安装 FROM node:16 AS deps WORKDIR /app COPY package*.json ./ RUN npm ci # 阶段2:构建应用 FROM node:16 AS build WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build # 阶段3:生产环境 FROM nginx:1.23-alpine COPY --from=build /app/dist /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] ``` 该示例实现: 1. 依赖安装与代码构建分离 2. 使用轻量级nginx镜像作为运行基础 3. 通过层级复制避免携带开发依赖 #### 五、进阶使用场景 1. **二进制文件提取**:当构建产物为独立可执行文件时: ```dockerfile FROM scratch AS runtime COPY --from=builder /app/binary / ENTRYPOINT ["/binary"] ``` 2. **多架构构建**:结合`--platform`参数实现跨平台构建 3. **构建缓存优化**:通过`--cache-from`复用缓存层加速构建 #### 六、版本兼容性注意 1. 需Docker 17.05+版本(2017年后发布的主流版本均支持) 2. 旧版本替代方案需手动文件传输多个Dockerfile管理[^2]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值