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

使用脉冲云的解决办法是将程序编译和镜像打包分开,使用脉冲云的编译构建服务,选择增加构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.3as 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 就非常方便实用了。

文章分享转载自:脉冲云

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容

  • Docker — 云时代的程序分发方式 要说最近一年云计算业界有什么大事件?Google Compute Engi...
    ahohoho阅读 15,508评论 15 147
  • docker基本概念 1. Image Definition 镜像 Image 就是一堆只读层 read-only...
    慢清尘阅读 8,725评论 1 21
  • 转载自 http://blog.opskumu.com/docker.html 一、Docker 简介 Docke...
    极客圈阅读 10,473评论 0 120
  • 想达到的效果: 方案其一 实现方法众多,这是其中一种。 环境: Ubuntu 14.4 Nodejs 6.2.0 ...
    By_syk阅读 280评论 0 1
  • 前言:年前农历廿七一家三口回老公老家过年,呆在老家的那些天脑子里一篇一篇,却没有时间写。前天晚上回到自己的小家,今...
    梦姐阅读 255评论 12 11