有关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程序就行了,不需要编译时的编译工具,最后生成的大体积镜像就是一种浪费。

使用脉冲云的解决办法是将程序编译和镜像打包分开,使用脉冲云的编译构建服务,选择增加构Go语言构建工具,然后在构建步骤中编译。
这里写图片描述

最后将编译接口拷贝到镜像中就行了,那么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构建的报错日志,我详细解释了日志查看的方法相关配置。现在用户的新问题是关于构建镜像的具体步骤,需要结合之前的回答,但这次的重点是构建过程本身。 用户提供的站内引用中,引用1提到构建步骤是编写Dockerfile、docker build、docker run。引用2对比了docker commitDockerfile,提到Dockerfile底层也是分层构建。引用3讨论了基础镜像的选择,如CentOSAlpine的区别。这些信息需要整合到回答中,但用户现在更关注的是构建的步骤方法,所以应该以步骤为主,同时可能涉及分层结构、基础镜像选择等。 我需要确保回答结构清晰,分步骤讲解,同时涵盖关键概念。可能的结构是:先介绍Dockerfile的基本概念,然后详细步骤,包括编写、构建、运行,接着讲解分层构建原理,最后补充最佳实践常见问题。这样用户不仅能按步骤操作,还能理解背后的机制。 还要注意用户可能的潜在需求,比如为什么推荐Dockerfile而不是docker commit,或者如何优化镜像大小。在回答中需要自然地融入这些信息,比如在步骤中提及多阶段构建,或者在最佳实践中提到选择Alpine镜像以减少体积。同时,根据之前的回答,用户可能对错误处理感兴趣,所以在步骤中强调构建时的日志查看方法,比如使用--progress=plain参数,这样能帮助用户提前发现并解决问题。 需要确保使用正确的中文术语,避免技术术语错误,比如“镜像”而不是“图像”,“容器”而不是“容器实例”。同时,引用之前的引用内容,比如引用2提到的分层构建,可以在分层构建原理部分引用,增强回答的可信度。 最后,生成相关问题需要考虑用户可能进一步的问题,比如如何优化Dockerfile多阶段构建的具体方法,或者如何选择基础镜像。这些问题应该与当前主题紧密相关,帮助用户深入理解。</think>### 如何使用Dockerfile构建镜像? --- #### 一、核心步骤与流程 1. **编写Dockerfile文件** - 在项目根目录创建名为`Dockerfile`的文本文件(无扩展名) - 按顺序定义构建指令,例如: ```dockerfile # 选择基础镜像(如Alpine仅5MB[^3]) FROM alpine:3.18 # 设置工作目录 WORKDIR /app # 复制本地文件到镜像 COPY . . # 安装依赖(Alpine使用apk包管理器[^3]) RUN apk add --no-cache python3 py3-pip # 定义启动命令 CMD ["python3", "main.py"] ``` 2. **执行构建命令** 在Dockerfile所在目录运行: ```bash docker build -t your_image:tag . ``` - `-t`:指定镜像名称标签(如`myapp:v1`) - `.`:表示当前目录为构建上下文(Docker会将此目录所有文件发送给守护进程[^1]) 3. **验证与运行** - 查看镜像列表: ```bash docker images ``` - 启动容器: ```bash docker run -d --name my_container your_image:tag ``` --- #### 二、分层构建原理 1. **每一条指令生成一个镜像层** Docker会按顺序执行Dockerfile中的指令,每条指令(如`FROM`、`RUN`、`COPY`)都会创建一个新的只读层[^2]。例如: ```dockerfile FROM ubuntu:22.04 # 层1:基础操作系统 RUN apt-get update # 层2:更新包列表 RUN apt-get install nginx # 层3:安装软件 COPY nginx.conf /etc/ # 层4:配置文件 ``` 2. **缓存机制优化构建速度** - 如果某层及其之前的层未发生变更,Docker会直接复用缓存 - 强制跳过缓存:添加`--no-cache`参数 ```bash docker build --no-cache -t your_image:tag . ``` --- #### 三、关键方法与最佳实践 1. **多阶段构建(减少镜像体积)** 通过多阶段构建分离编译环境运行环境: ```dockerfile # 阶段1:构建环境 FROM golang:1.20 AS builder WORKDIR /src COPY . . RUN go build -o app # 阶段2:运行环境 FROM alpine:3.18 COPY --from=builder /src/app /app CMD ["/app"] ``` 2. **基础镜像选择策略** | 镜像类型 | 体积 | 适用场景 | 示例 | |----------------|--------|-------------------------|-------------------| | **完整OS镜像** | 较大 | 需要完整系统功能 | `ubuntu`、`centos` | | **精简镜像** | 极小 | 微服务/无依赖二进制 | `alpine`[^3] | | **语言专用** | 中等 | 特定语言环境 | `python:3.11-slim` | 3. **调试技巧** - 进入失败层调试: ```bash docker run -it --rm <失败层ID> sh ``` - 检查构建上下文文件: ```bash docker build --no-cache --progress=plain . 2>&1 | tee build.log ``` --- #### 四、常见问题与解决方案 | 问题现象 | 原因分析 | 解决方案 | |---------------------------|-------------------------|-----------------------------------| | **COPY/ADD指令文件丢失** | 文件不在构建上下文中 | 检查`.dockerignore`排除规则 | | **网络依赖下载失败** | 容器内DNS配置错误 | 在Dockerfile中设置`ENV GOPROXY=https://goproxy.cn` | | **权限拒绝错误** | 容器用户权限不足 | 添加`USER`指令或`chmod`操作 | --- ### 相关问题 1. 如何通过`.dockerignore`文件优化构建速度? 2. 多阶段构建时如何选择不同阶段的基础镜像? 3. 为什么Alpine镜像的软件包安装方式与其他Linux发行版不同? 通过掌握以上步骤原理,您可以高效构建符合业务需求的Docker镜像。首次构建建议使用`--progress=plain`参数观察完整流程[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值