制作一个超级精简的 Docker 镜像只需7步

文章来自黄庆兵老师的网易蜂巢《玩转 Docker 镜像》系列https://github.com/bingohuang/play-docker-images,原本分为上下两篇,这里由生态君整合成一篇便于阅读。希望能对大家有所帮助。

目录

介绍

镜像层(Layers)

制作步骤

lab-1:初始化构建 Redis 镜像

lab-2:优化基础镜像

lab-3:串联 Dockerfile 指令

lab-4:压缩你的镜像

lab-5:使用最精简的 base image

lab-6:提取动态链接的 .so 文件

lab-7:为 Go 应用构建精简镜像

总结

参考

作者简介:黄庆兵,毕业于浙大,工作于网易,从事云计算、Docker和Go相关开发及布道工作;喜欢开源,乐于分享,勤于布道,折腾过开源小工具,制作过Docker课程,分享过 Gopher Meetup。我的 Github 账号:https://github.com/bingohuang,欢迎一起来写 Go 玩 Docker!

介绍

前段时间网易蜂巢曾经推出蜂巢LogoT恤,用的正是 Docker 镜像制作,最神奇的是,它最终的镜像大小只有585字节。

$ docker images | grep hub.c.163.com/public/logo

REPOSITORY                      TAG           IMAGE ID         CREATED       SIZE

hub.c.163.com/public/logo       latest        6fbdd13cd204     11 days ago   585 B

有些镜像都不是我们自己来打包的(比如下载公共镜像),那是否有一些通用的精简 Docker 镜像的手段呢?答案是肯定的,甚至有的镜像可以精简 98%。精简镜像大小的好处不言而喻,既节省了存储空间,又能节省带宽,加快传输等。那好,接下来就请跟随我来学习怎么制作精简 Docker 镜像。

镜像层(Layers)

在开始制作镜像之前,首先了解下镜像的原理,而这其中最重要的概念就是镜像层(Layers)。镜像层依赖于一系列的底层技术,比如文件系统(filesystems)、写时复制(copy-on-write)、联合挂载(union mounts)等,幸运的是你可以在很多地方学习到这些技术[1],这里就不再赘述技术细节。

总的来说,你最需要记住这点:

在 Dockerfile 中, 每一条指令都会创建一个镜像层,继而会增加整体镜像的大小。

举例来说:

FROMbusybox

RUN mkdir /tmp/foo

RUNddif=/dev/zero of=/tmp/foo/bar bs=1048576 count=100

RUN rm /tmp/foo/bar

以上 Dockerfile 干了几件事:

基于一个官方的基础镜像 busybox(只有1M多)

创建一个文件夹(/tmp/foo)和一个文件(bar),该文件分配了100M大小

再把这个大文件删除

实际上它最终什么也没做,我们把它构建成镜像(构建可以参考一期[2]):

docker build -t busybox:test.

再让我们来对比下原生的 busybox 镜像大小和我们生成的镜像大小:

$ docker images | grep busybox

busyboxtest896c63dbdb96    2 seconds ago    106 MB

busybox    latest   2b8fd9751c4c    9 weeks ago      1.093 MB

出乎意料的是,却生成了 106 MB 的镜像。

多出了 100 M,这是为何?这点和 Git 类似(都用到了Copy-On-Write技术),我用 git 做了如下两次提交(添加了又删除),请问A_VERY_LARGE_FILE还在 git 仓库中吗?

$ git add  A_VERY_LARGE_FILE

$ git commit

$ git rm  A_VERY_LARGE_FILE

$ git commit

答案是:在的,并且会占用仓库的大小。Git 会保存每一次提交的文件版本,而 Dockerfile 中每一条指令都可能增加整体镜像的大小,即使它最终什么事情都没做。

制作步骤

了解了镜像层知识,有助于我们接下来制作精简镜像。这里开始,以最常用的开源缓存软件Redis为例,从一步步试验,来介绍如何制作更精简的 Docker 镜像。

lab-1:初始化构建 Redis 镜像

直接上Dockerfile:

FROMubuntu:trusty

ENVVER3.0.0

ENVTARBALL http://download.redis.io/releases/redis-$VER.tar.gz

# ==> Install curl and helper tools...

RUN apt-get update

RUN apt-get install -y  curl make gcc

# ==> Download, compile, and install...

RUNcurl -L$TARBALL| tar zxv

WORKDIRredis-$VER

RUN make

RUN make install

#...

# ==> Clean up...

WORKDIR /

RUN apt-get remove -y --auto-remove curl make gcc

RUN apt-get clean

RUNrm -rf /var/lib/apt/lists/*  /redis-$VER

#...

CMD["redis-server"]

结合注释,读起来并不困难,用到的都是常规的几个命令,简要介绍如下:

FROM:顶头写,指定一个基础镜像,此处基于ubuntu:trusty

ENV:设置环境变量,这里设置了VER和TARBALL两个环境变量

RUN:最常用的 Dockerfile 指令,用于运行各种命令,这里调用了 8 次 RUN 指令

WORKDIR:指定工作目录,相当于指令cd

CMD:指定镜像默认执行的命令,此处默认执行 redis-server 命令来启动 redis

执行构建:

$ docker build  -t redis:lab-1  .

注:国内网络,更新下载可能会较慢

查看大小:

LabiamgeBaseLang.red[*]Size (MB)   Memo

1redisubuntuCdyn347.3   base ubuntu

动辄就有 300多 M 的大小,不能忍,下面我们开始一步步优化。

lab-2:优化基础镜像

精简1:选用更小的基础镜像。

常用的 Linux 系统镜像一般有ubuntu、centos、debian,其中debian更轻量,而且够用,对比如下:

REPOSITORY          TAG        IMAGE ID         VIRTUAL SIZE

---------------     ------     ------------     ------------

centos              7          214a4932132a     215.7 MB

centos              6          f6808a3e4d9e     202.6 MB

ubuntu              trusty     d0955f21bf24     188.3 MB

ubuntu              precise    9c5e4be642b7     131.9 MB

debian              jessie     65688f7c61c4     122.8 MB

debian              wheezy     1265e16d0c28     84.96 MB

替换debian:jessie作为我们的基础镜像。

优化 Dockerfile:

FROMdebian:jessie

#...

执行构建:

$ docker build  -t redis:lab-2  .

查看大小:

LabimageBaseLang.red[*]Size (MB)   Memo

01redisubuntuCdyn347.3   base ubuntu

02redisdebianCdyn305.7   base debian

减少了42M,稍有成效,但并不明显。细心的同学应该发现,只有 122 MB 的debian基础镜像,构建后增加到了 305 MB,看来这里面肯定有优化的空间,如何优化就要用到我们开头说到的Image Layer知识了。

lab-3:串联 Dockerfile 指令

精简2:串联你的 Dockerfile 指令(一般是RUN指令)。

Dockerfile 中的 RUN 指令通过&&和/支持将命令串联在一起,有时能达到意想不到的精简效果。

优化 Dockerfile:

FROMdebian:jessie

ENVVER3.0.0

ENVTARBALL http://download.redis.io/releases/redis-$VER.tar.gz

RUNecho"==> Install curl and helper tools..."&& \

apt-get update                      && \

apt-get install -y  curl make gcc   && \

\

echo"==> Download, compile, and install..."&& \

curl -L$TARBALL| tar zxv  && \

cdredis-$VER&& \

make                        && \

make install                && \

...

echo"==> Clean up..."&& \

apt-get remove -y --auto-remove curl make gcc  && \

apt-get clean                                  && \

rm -rf /var/lib/apt/lists/*  /redis-$VER

#...

CMD["redis-server"]

构建:

$ docker build  -t redis:lab-3  .

查看大小:

LabImageBaseLang.red[*]Size (MB)   Memo

01redisubuntuCdyn347.3   base ubuntu

02redisdebianCdyn305.7   base debian

03redisdebianCdyn151.4   cmd chaining

哇!一下子减少了 50%,效果明显啊!这是最常用的一个精简手段了。

lab-4:压缩你的镜像

优化3:试着用命令或工具压缩你的镜像。

docker 自带的一些命令还能协助压缩镜像,比如export和import

$ docker run -d redis:lab-3

$ dockerexport71b1c0ad0a2b | docker import - redis:lab-4

但麻烦的是需要先将容器运行起来,而且这个过程中你会丢失镜像原有的一些信息,比如:导出端口,环境变量,默认指令。

所以一般通过命令行来精简镜像都是实验性的,那么这里再推荐一个小工具:docker-squash[3]。用起来更简单方便,并且不会丢失原有镜像的自带信息。

下载安装:

https://github.com/jwilder/docker-squash#installation

压缩操作:

$ docker save redis:lab-3 \

| sudo docker-squash -verbose -t redis:lab-4  \

| docker load

注:该工具在 Mac 下并不好使,请在 Linux 下使用

对比大小:

LabImageBasePL.red[*]Size (MB)   Memo

01redisubuntuCdyn347.3   base ubuntu

02redisdebianCdyn305.7   base debian

03redisdebianCdyn151.4   cmd chaining

04redisdebianCdyn151.4   docker-squash

好吧,从这里看起来并没有太大作用,所以我只能说试着,而不要报太大期望。

lab-5:使用最精简的 base image

使用scratch或者busybox作为基础镜像。

关于 scratch:

一个空镜像,只能用于构建镜像,通过FROM scratch

在构建一些基础镜像,比如debian、busybox,非常有用

用于构建超少镜像,比如构建一个包含所有库的二进制文件

关于busybox

只有 1~5M 的大小

包含了常用的 UNIX 工具

非常方便构建小镜像

这些超小的基础镜像,结合能生成静态原生 ELF 文件的编译语言,比如C/C++,比如 Go,特别方便构建超小的镜像。

cloudcomb-logo(C语言开发) 就是用到了该原理,才能构建出 585 字节的镜像。

redis同样使用 C语言 开发,看来也有很大的优化空间,下面这个实验,让我们介绍具体的操作方法。

lab-6:提取动态链接的 .so 文件

实验上下文:

$ cat /etc/os-release

NAME="Ubuntu"

VERSION="14.04.2 LTS, Trusty Tahr"

$ uname -a

Linux localhost 3.13.0-46-generic#77-Ubuntu SMP

Mon Mar 2 18:23:39 UTC 2015

x86_64 x86_64 x86_64 GNU/Linux

隆重推出 ldd:打印共享的依赖库

$ ldd  redis-3.0.0/src/redis-server

linux-vdso.so.1 =>  (0x00007fffde365000)

libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f307d5aa000)

libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f307d38c000)

libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f307cfc6000)

/lib64/ld-linux-x86-64.so.2 (0x00007f307d8b9000)

将所有需要的 .so 文件打包:

$ tar ztvf rootfs.tar.gz

4485167  2015-04-21 22:54  usr/local/bin/redis-server

1071552  2015-02-25 16:56  lib/x86_64-linux-gnu/libm.so.6

141574  2015-02-25 16:56  lib/x86_64-linux-gnu/libpthread.so.0

1840928  2015-02-25 16:56  lib/x86_64-linux-gnu/libc.so.6

149120  2015-02-25 16:56  lib64/ld-linux-x86-64.so.2

再制作成 Dockerfile:

FROMscratch

ADD  rootfs.tar.gz  /

COPY redis.conf     /etc/redis/redis.conf

EXPOSE6379

CMD["redis-server"]

执行构建:

$ docker build  -t redis-05  .

查看大小:

LabBasePL.red[*]Size (MB)   Memo

01redisubuntuCdyn347.3   base ubuntu

02redisdebianCdyn305.7   base debian

03redisdebianCdyn151.4   cmd chaining

04redisdebianCdyn151.4   docker-squash

05redisscratchCdyn7.73   rootfs: .so

哇!显著提高啦!

测试一下:

$ docker run -d --name redis-05 redis-05

$ redis-cli  -h  \

$(docker inspect -f'{{.NetworkSettings.IPAddress}}'redis-05)

$ redis-benchmark  -h  \

$(docker inspect -f'{{.NetworkSettings.IPAddress}}'redis-05)

总结一下:

用ldd查出所需的 .so 文件

将所有依赖压缩成rootfs.tar或rootfs.tar.gz,之后打进scratch基础镜像

lab-7:为 Go 应用构建精简镜像

Go 语言天生就方便用来构建精简镜像,得益于它能方便的打包成包含静态链接的二进制文件。

打个比方,你有一个 4 MB 大小的包含静态链接的 Go 二进制,并且将其打进 scratch 这样的基础镜像,你得到的镜像大小也只有区区的 4 MB。这可是包含同样功能的 Ruby 程序的百分之一啊。

这里再给大家介绍一个非常好用开源的 Go 编译工具:golang-builder,并给大家实际演示一个例子

程序代码:

packagemain// import "github.com/CenturyLinkLabs/hello"

import"fmt"

funcmain(){

fmt.Println("Hello World")

}

Dockerfile:

FROMscratch

COPY hello /

ENTRYPOINT["/hello"]

通过 golang-builder 打包成镜像:

docker run --rm \

-v $(pwd):/src \

-v /var/run/docker.sock:/var/run/docker.sock \

centurylink/golang-builder

查看镜像大小(Mac下测试):

$ docker images

REPOSITORY   TAG      IMAGE ID       CREATED          VIRTUAL SIZE

hello        latest   1a42948d3224   24 seconds ago   1.59 MB

哇!这么省力,就能创建几 M 大小的镜像,Go 简介就是为 Docker 镜像量身定做的!

总结

我们介绍了镜像层的知识,并且通过实验,介绍三种如何精简镜像的技巧。这里主要介绍了三种精简方法:选用更精小的镜像,串联 Dockerfile 运行指令,以及试着压缩你的镜像。通过这几个技巧,已经可以将 300M 大小的镜像压缩到 150M,压缩率50%到98%,效果还是不错。

优化基础镜像

串接 Dockerfile 命令:

压缩 Docker images

优化程序依赖

选用更合适的开发语言

参考

scratch in Docker Hub[4]

Make FROM scratch a special cased 'no-base' spec[5]

vDSO (virtual dynamic shared object)[6]

Small Docker Images For Go Apps[7](withgolang-builder[8])

Building Docker Images for Static Go Binaries[9]

Dockerfile Best Practices - take 2[10]- by Michael Crosby, 2014-03-09.

Optimizing Docker Images[11]- by Brian DeHamer, 2014-07-28.

Squashing Docker Images[12]- by Jason Wilder, 2014-08-19.

参考资料

[1] 这些技术:https://docs.docker.com/engine/userguide/storagedriver/imagesandcontainers/

[2] 一期:https://github.com/bingohuang/play-docker-images/tree/master/stage-01

[3] docker-squash:https://github.com/jwilder/docker-squash

[4] scratch in Docker Hub:https://registry.hub.docker.com/_/scratch/

[5] Make FROM scratch a special cased 'no-base' spec:https://github.com/docker/docker/pull/8827

[6] vDSO (virtual dynamic shared object):http://en.wikipedia.org/wiki/VDSO

[7] Small Docker Images For Go Apps:http://www.centurylinklabs.com/small-docker-images-for-go-apps/

[8] golang-builder:https://github.com/CenturyLinkLabs/golang-builder

[9] Building Docker Images for Static Go Binaries:https://medium.com/@kelseyhightower/optimizing-docker-images-for-static-binaries-b5696e26eb07

[10] Dockerfile Best Practices - take 2:http://crosbymichael.com/dockerfile-best-practices-take-2.html

[11] Optimizing Docker Images:http://www.centurylinklabs.com/optimizing-docker-images/

[12] Squashing Docker Images:http://jasonwilder.com/blog/2014/08/19/squashing-docker-images/

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

推荐阅读更多精彩内容