首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX  ›  程序员

高效编写 Dockerfile 的几条准则

  •  1
     
  •   hansonwang99 · 9 天前 · 2333 次点击

    Profile


    概述

    • Dockerfile 是专门用来进行自动化构建镜像的编排文件(就像 Jenkins 2.0 时代的 Jenkinsfile 是对 Jenkins 的 Job 和 Stage 的编排一样),我们可以通过 docker build 命令来自动化地从 Dockerfile 所描述的步骤来构建自定义的 Docker 镜像,这比我们去命令行一条条指令执行的方式构建高效得多。

    • 另一方面,由于 Dockerfile 提供了统一的配置语法,因此通过这样一份配置文件,我们可以在各种不同的平台上进行分发,需要时通过 Dockerfile 构建一下就能得到所需的镜像。

    • 最后一个必须提的优点便是:Dockerfile 通过与镜像配合使用,使得 Docker 镜像构建之时可以充分利用 “镜像的缓存功能”,因此也提效不少!

    然而写 Dockerfile 也像写代码一样,一份精心设计、Clean Code 的 Dockerfile 能在提高可读性的同时也大大提升 Docker 的使用效率

    因此下面就结合实践来讲几条 Dockerfile 的实践心得!



    基础镜像的选择有讲究

    在我的文章 《利用 K8S 技术栈打造个人私有云(连载之:基础镜像制作与实验)》 中,我们是基于某个 Linux 基础镜像作为底包,然后打包进我需要的功能从而形成自己的镜像。

    这里选择基础镜像时是有讲究的:

    • 一是 应当尽量选择官方镜像库里的基础镜像;
    • 二是 应当选择轻量级的镜像做底包

    就典型的 Linux 基础镜像来说,大小关系如下:

    Ubuntu > CentOS > Debian
    

    因此相比 Ubuntu,其实更推荐使用最轻量级的 Debian 镜像,而且它也是一个完整的 Release 版,可以放心使用



    多使用标签 Tag 有好处

    • 构建镜像时,给其打上一个易读的镜像标签有助于帮助了解镜像的功能,比如:
    docker build -t=“ centos:wordpress" .
    

    例如上面的这个 centos 镜像是用来做 wordpress 用的,所以已经集成了 wordpress 功能,这一看就很清晰明了

    • 再者,我们也应该在 Dockerfile 的 FROM 指令中明确指明标签 Tag,不要再让 Docker daemon 去猜,如
    FROM debian:codesheep
    


    充分利用镜像缓存

    什么是镜像缓存?

    由 Dockerfile 最终构建出来的镜像是在基础镜像之上一层层叠加而得,因此在过程中会产生一个个新的 镜像层。Docker daemon 在构建镜像的过程中会缓存一系列中间镜像。

    docker build 镜像时,会顺序执行 Dockerfile 中的指令,并同时比较当前指令和其基础镜像的所有子镜像,若发现有一个子镜像也是由相同的指令生成,则 命中缓存,同时可以直接使用该子镜像而避免再去重新生成了。

    为了有效地使用缓存,需要保证 Dockerfile 中指令的 连续一致,尽量将相同指令的部分放在前面,而将有差异性的指令放在后面

    **举例:**假如我想用 Dockerfile 方式 基于最基本的 CentOS 镜像来构建两个不同的镜像时,两个 Dockerfile 的开头可以相同:

    FROM centos:latest
    
    # 下面安装两个常用的工具
    RUN yum install -y net-tools.x86_64
    
    RUN yum install lrzsz
    
    ######## 上面为两个 Dockerfile 文件中相同的部分######
    
    ######## 下面为两个 Dockerfile 文件中不同的部分######
    
    ......
    
    


    ADD 与 COPY 指令的正确使用

    虽然两者都可以添加文件到镜像中,但在一般用法中,还是推荐以 COPY 指令为首选,原因在于 ADD 指令并没有 COPY 指令来的纯粹,ADD 会添加一些额外功能,典型的如下 ADD 一个压缩包时,其不仅会复制,还会自动解压,而有时我们并不需要这种额外的功能。

    ADD codesheep.tar.gz /path
    

    除此之外,在需要添加多个文件到镜像中的时候,不要一次性集中添加,而是选择 按需 在必要时 逐个 添加即可,因为这样有利于利用镜像缓存



    ##尽量使用 docker volume

    虽然上面一条原则说推荐通过 COPY 命令来向镜像中添加多个文件,然而实际情况中,若文件 大而多 的时候还是应该优先用 docker -v 命令来挂载文件,而不是依赖于 ADD 或者 COPY



    CMD 和 ENTRYPOINT 指令 的正确理解使用

    Dockerfile 制作镜像时,会组合 CMD 和 ENTRYPOINT 指令来作为容器运行时的默认命令:即 CMD + ENTRYPOINT。此时的默认命令组成中:

    • ENTRYPOINT 指令部分固定不变,容器运行时是无法修改的
    • 而 CMD 部分的指令也可以改变,表现在运行容器时,docker run 命令中提供的参数会覆盖 CMD 的指令内容。

    举个例子:

    FROM debian:latest
    
    MAINTAINER codesheep@163.com
    
    ENTRYPOINT [ "ls", "-l"]
    CMD ["-a"]
    

    若以默认命令运行容器,可以发现,执行的是 ls -a -l 命令:

    ls -l -a

    docker run 中增加参数 -t

    docker run -it --rm --name test debian:codesheep -t
    

    也可以发现执行的是 ls -l -t,即 Dockerfile 中的 CMD 原参数被覆盖了:

    ls -l -t

    因此推荐的使用方式是:

    • 使用 exec 格式的 ENTRYPOINT 指令 设置固定的默认命令和参数

    • 使用 CMD 指令 设置可变的参数



    不推荐在 Dockerfile 中 做端口映射

    Dockerfile 可以通过 EXPOSE 指令 将容器端口映射到主机端口上,但这样会导致镜像在一台主机上仅能启动一个容器!

    所以应该在 docker run 命令中来用 -p 参数来指定端口映射,而不要将该工作置于 Dockerfile 之中:

    #尽量避免这种方式
    EXPOSE 8080:8899
    
    #选择仅仅暴露端口即可,端口映射的任务交给 docker run 去做
    EXPOSE 8080
    


    使用 Dockerfile 来共享镜像

    推荐通过共享 Dockerfile 的方式来共享镜像,优点多多:

    • 通过 Dockerfile 构建的镜像用户可以清楚地看到构建的过程

    • 就像 Jenkinsfile 可以加入版本控制从而追踪 CI 系统的变迁和步骤的回滚一样,Dockerfile 作为一个编排文件同样可以入库做版本控制,这样也可以回溯

    • 使用 Dockerfile 构建的镜像具有确定性,没有玄学的成分



    后记

    如果有兴趣,也可以抽点时间看看作者一些关于容器化、微服务化方面的文章:

    作者相关的 SpringBt 实践文章在此:



    36 回复  |  直到 2018-07-12 11:54:02 +08:00
        1
    wl2358   9 天前 via Android
    最近在学 dicker, mark
        2
    mason961125   9 天前
    所以,开头放张图的意义是什么?
        3
    ngg0707   9 天前 via iPhone
    很有帮助
        4
    AlphaTr   9 天前 via iPhone
    有一点不太认同,希望能讨论讨论:tag 那个,我更倾向于 wordpress:v4.3.3 这样的,对于用户来说,不需要关心底层镜像是 centos 还是 ubuntu,关心的是 wordpress 的版本或者其他信息;私以为这里的 tag 类似于 git 的 tag,多用于版本
        5
    hansonwang99   9 天前
    我觉得楼上的理解也挺好呢
        6
    lizheming   9 天前 via iPhone
    @AlphaTr 我觉得只是他的例子举的不太好,意思都是差不多,作者想说的应该和你的意思一样。
        7
    ofnh   9 天前 via Android
    是 ls -l -a 吧
        8
    mritd   9 天前 via iPhone
    @lizheming 无版本号 / 时间戳 tag 等同于 latest
        9
    yuanfnadi   9 天前
    很重要的多部构建没有提到。可以极大的缩小镜像体积。
        10
    shiny   9 天前
    不谈谈 alpine linux 吗
        11
    billwsy   9 天前 via iPhone
    Tag 成 centos:wordpress 真的好吗?不怕冲突?以后上传到 registry 也不方便啊
        12
    billwsy   9 天前 via iPhone
    另外 Entrypoint 运行时也是可以改的

    至于所说的 layer,倒不如造一个自己的 base,所有其他 image 从中继承;我们系统里的 57 个 image 就组织成了这样的树状,然后一个脚本自动解析依赖关系
        13
    blless   9 天前 via iPhone
    感觉只是基础的构建…官方构建一般不会有两个 yum install,而且推荐安装跟清理一起做了 不然下一层清理体积并不会减小
        14
    blless   9 天前 via iPhone
    添加文件按需逐个添加效率也太低了…而且如果第二个文件就不一样 后面缓存也全没用了…
        15
    blless   9 天前 via iPhone   ♥ 2
    ……楼主你这文章就不要用"准则"了,明显误导别人
        16
    ysicing   8 天前 via Android
    基础镜像很少用 centos 的吧,不应该是最常用 alpine 或 debian😉
        17
    sdrzlyz   8 天前 via Android
    确定 Expose 是这个意思?这个“准则”有点误认啊。。。基础镜像不提 alpine ?
        18
    naiba   8 天前 via Android
    层数越多占用空间越大…
        19
    hxsf   8 天前 via iPhone
    @blless +1
    to 楼主 layer 越多越容易缓存也越大,
    有 run 的时候基本缓存失效 不如一把梭,layer 少点还小
    entrypoint 也是能改的
    推荐用-v 挂文件什么意思 有些东西要持久化才挂载 其他作为容器一部分就好了,不然分发的时候除了 image 还得分发其他文件。

    另外:标题确实不太好
        20
    0312birdzhang   8 天前 via iPhone
    @blless +1 不认同楼主的“镜像缓存”,这样只是方便你本地下次打镜像更快一点而已,但是却浪费了空间,使镜像多了一层。
        21
    beginor   8 天前 via Android
    为了达到最小化体积,将所有的动作都放到一个 install.sh , 然后 dockerfile 只有一句 exec,这种做法是否可取呢?

    https://github.com/beginor/docker-geoserver
        22
    jhsunnyshine   8 天前 via Android
    好评,给赞
        23
    wenzhoou   8 天前 via Android
    @beginor 这个能减小体积吗?
        24
    laincat   8 天前
    不管怎么样,读一下
        25
    tandaly   8 天前
    我是来看桌面的
        26
    hcivincentchan   8 天前
    entrypoint 在容器启动时 是可以重新指定的好吗
        27
    beginor   8 天前
    @wenzhoou 能,我原来都是写在 Dockerfile, 放到 install.sh 的话,build 出来的镜像可以减少 100 多兆
        28
    beginor   8 天前
    @wenzhoou 而且 sh 文件容易进行测试, 可以一边运行, 一边编写
        29
    wenzhoou   8 天前 via Android
    @beginor 相信跟你放到哪里写是没有关系的。因为理论上是一样的执行。
        30
    g8287694   8 天前
    ADD 是不是无法添加非本目录的文件?
        31
    lfzyx   8 天前
    上面说 entrypoint 可以改的,人家设计这个目的是让你不会轻易去修改。就像私有变量不是不能访问,只是让你明白不应该直接去访问
        32
    mcfog   8 天前
    我现在基本形成了本能反应:看到这种开头一张图,内容里面大量副标题和加粗的公众号文笔 就自动判定成垃圾水文,速读垂直扫几眼直接拉到底,看到一排链接,嗯,枪毙,再见
        33
    raysonx   8 天前
    不同意尽量使用 VOLUME 的说法。
    除非你只是用 docker 做开发,也就是把依赖全都打进 image,然后挂载本地代码。
    如果做生产用途,除了数据和一部分配置,其他应该全部都打进 image,以保证“一次构建,处处运行”。
        34
    godjob   8 天前
    @beginor 这样做就脱离 docker 的宗旨了,docker 的目的就是一个完整的环境集装箱,在任何环境都可以运行,如果启动时再安装环境和软件,每次启动都可能有不同的因素干扰,就不能保证每个 doker 容器的环境一致
        35
    jojojo   8 天前
    这篇文章和我看的 docker-从入门到实践有很大出入
        36
    beginor   8 天前 via Android
    @godjob 不是启动时安装, 是编译时执行安装, 最终还是装好了所有软件的镜像。


    @wenzhoou 会的, 原来需要 Dockerfile 中写多个 run 指令, 现在只需要一个指令 run install.sh , 这样镜像只有一层, 能显著减小最终镜像的体积。install.sh 还是一句一句来写安装的命令
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   鸣谢   ·   实用小工具   ·   580 人在线   最高记录 3541   ·  
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.1 · 20ms · UTC 20:38 · PVG 04:38 · LAX 13:38 · JFK 16:38
    ♥ Do have faith in what you're doing.
    沪ICP备16043287号-1