频道栏目
首页 > 资讯 > 云计算 > 正文

Dockerfile最佳实践

17-06-28        来源:[db:作者]  
收藏   我要投稿

虽然 Dockerfile 简化了镜像构建的过程,并且把这个过程可以进行版本控制,但是不正当的 Dockerfile 使用也会导致很多问题:

  • docker 镜像太大。如果你经常使用镜像或者构建镜像,一定会遇到那种很大的镜像,甚至有些能达到 数G
  • docker 镜像的构建时间过长。每个 build 都会耗费很长时间,对于需要经常构建镜像(比如单元测试)的地方这可能是个大问题
  • 重复劳动。多次镜像构建之间大部分内容都是完全一样而且重复的,但是每次都要做一遍,浪费时间和资源

    一般指导方针和建议

    容器应该是短暂的

    容器模型是进程而不是机器,不需要开机初始化。在需要时运行,不需要时停止,能够删除后重建,并且配置和启动的最小化。

    使用.dockerignore文件

    在 docker build 的时候,忽略部分无用的文件和目录可以提高构建的速度。比如.git目录。dockerignore的定义类似gitignore。

    避免安装不必要的安装包

    为了减少镜像的复杂性、镜像大小和构建时间,应该避免安装无用的包。

    每个容器只运行一个进程

    一个容器只运行一个进程。容器起到了隔离应用隔离数据的作用,不同的应用运行在不同的容器让集群的纵向扩展以及容器的复用都变的更加简单。需要多个应用交互时请使用 link 命令进行组合或者使用docker-compose。

    最小化层数

    需要掌握好Dockerfile的可读性和镜像层数之间的平衡。不推荐使用过多的镜像层。

    多行命令按字母排序

    命令行按字母顺序排序有助于避免重复执行和提高Dockerfile可读性。apt-get update应与apt-get install组合,换行使用反斜杠(\)。例如:

    RUN apt-get update && apt-get install -y \
      bzr \
      cvs \
      git \
      mercurial \
      subversion
    

    构建缓存

    Dockerfile的每条指令都会将结果提交为新的镜像。下一条指令基于上一条指令的镜像进行构建。如果一个镜像拥有相同的父镜像和指令(除了 ADD ),Docker将会使用镜像而不是执行该指令,即缓存。

    因此,为了有效的利用缓存,尽量保持Dockerfile一致,并且将不变的放在前面而经常改变放在末尾。

    如不希望使用缓存,在执行 docker build 的时候加上参数 --no-cache=true 。

    Docker匹配镜像决定是否使用缓存的规则如下:

    • 从缓存中存在的基础镜像开始,比较所有子镜像,检查它们构建的指令是否和当前的是否完全一致。如果不一致则缓存不匹配。
    • 多数情况中,比较Dockerfile中的指令是足够的。然而,特定的指令需要做更多的判断。
    • ADD COPY 指令中,将要添加到镜像中的文件也要被检查。通常是检查文件的校验和(checksum)。
    • 缓存匹配检查并不检查容器中的文件。例如,当使用 RUN apt-get -y update 命令更新了容器中的文件,并不会被缓存检查策略作为缓存匹配的依据。

      Dockerfile指令

      FROM

      使用官方仓库中的镜像作为基础镜像,推荐使用 Debian image ,大小保持在100mb上下,且仍是完整的发行版。

      RUN

      把复杂的或过长的 RUN 语句写成以 \ 结尾的多行的形式,以提高可读性和可维护性。

      apt-get update 和 apt-get install 一起执行,否则 apt-get install 会出现异常。

      避免运行 apt-get upgrade 或 dist-upgrade ,在无特权的容器中,很多 必要 的包不能正常升级。如果基础镜像过时了,应当联系维护者。 推荐 apt-get update && apt-get install -y package-a package-b 这种方式,先更新,之后安装最新的软件包。

      RUN apt-get update && apt-get install -y \
          aufs-tools \
          automake \
          build-essential \
          curl \
          dpkg-sig \
          libcap-dev \
          libsqlite3-dev \
          mercurial \
          reprepro \
          ruby1.9.1 \
          ruby1.9.1-dev \
          s3cmd=1.1.* \
       && rm -rf /var/lib/apt/lists/*
      

      此外,你可以通过移除/var/lib/apt/lists减少镜像大小。

      注意:官方的Ubuntu和Debian会自动运行apt-get clean,所以不需要显式的调用

      CMD

      推荐使用 CMD ["executable","param1","param2"] 这样的格式。如果镜像是用来运行服务,需要使用 CMD["apache2","-DFOREGROUND"] ,这种格式的指令适用于任何服务性质的镜像。

      ENTRYPOINT

      ENTRYPOINT 应该用于 镜像的主命令,并使用 CMD 作为默认设置,以 s3cmd 为例:

      ENTRYPOINT ["s3cmd"]
      CMD ["--help"]
      

      获取帮助:

      docker run s3cmd
      

      或者执行命令:

      docker run s3cmd ls s3://mybucket
      

      这在镜像名与程序重名时非常有用。

      ENTRYPOINT 也可以启动自定义脚本: 定义脚本:

      #!/bin/bash
      set -e
      
      if [ "$1" = 'postgres' ]; then
          chown -R postgres "$PGDATA"
      
          if [ -z "$(ls -A "$PGDATA")" ]; then
              gosu postgres initdb
          fi
      
          exec gosu postgres "$@"
      fi
      
      exec "$@"
      

      注意:这段脚本使用了exec命令以确保最终应用程序在容器内启动的PID为1。这段脚本允许容器接收Unix signals。

      COPY ./docker-entrypoint.sh /
      ENTRYPOINT ["/docker-entrypoint.sh"]
      

      这段脚本为用户提供了多种和 Postgres 交互的途径:

      你可以简单地启动 Postgres:

      docker run postgres。
      

      或者运行 postgres 并传入参数:

      docker run postgres postgres --help。
      

      你甚至可以从镜像中启动一个完全不同的程序,比如 Bash:

      docker run --rm -it postgres bash
      

      EXPOSE

      应该尽可能地使用默认端口。例如Apache web服务使用EXPOSE 80,MongoDB使用EXPOSE 27017。

      ENV

      • 可以使用ENV更新PATH环境变量。例如,ENV PATH /usr/local/nginx/bin:$PATH可以确保CMD [“nginx”]正常运行。
      • ENV还可以提供程序所需要的环境变量,例如Postgres的 PGDATA。
      • ENV可以设置版本等信息。使版本信息更易于维护。
        ENV PG_MAJOR 9.3
        ENV PG_VERSION 9.3.4
        RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …
        ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH
        

        ADD or COPY

        虽然 ADD 与 COPY 功能类似,但推荐使用 COPY 。 COPY 只支持基本的文件拷贝功能,更加的可控。而 ADD 具有更多特定,比如tar文件自动提取,支持URL。 通常需要提取tarball中的文件到容器的时候才会用到 ADD 。

        如果在Dockerfile中使用多个文件,每个文件应使用单独的 COPY 指令。这样,只有出现文件变化的指令才会不使用缓存。

        为了控制镜像的大小,不建议使用 ADD 指令获取URL文件。正确的做法是在 RUN 指令中使用 wget 或 curl 来获取文件,并且在文件不需要的时候删除文件。

        RUN mkdir -p /usr/src/things \
            && curl -SL http://example.com/big.tar.gz \
            | tar -xJC /usr/src/things \
            && make -C /usr/src/things all
        

        VOLUME

        VOLUME 通常用作数据卷,对于任何可变的文件,包括数据库文件、代码库、或者容器所创建的文件/目录等都应该使用 VOLUME 挂载。

        USER

        如果服务不需要特权来运行,使用 USER 指令切换到非root用户。使用RUN groupadd -r mysql && useradd -r -g mysql mysql之后用USER mysql切换用户

        要避免使用 sudo 来提升权限,因为它带来的问题远比它能解决的问题要多。如果你确实需要这样的特性,那么可以选择使用gosu。

        最后,不要反复的切换用户。减少不必要的layers。

        WORKDIR

        为了清晰和可维护性,应该使用WORKDIR来定义工作路径。推荐使用WORKDIR来代替RUN cd … && do-something这样的指令。

        ONBUILD

        Dockerfile 中的其它指令都是为了定制当前镜像而准备的,唯有 ONBUILD 是为了帮助别人定制自己而准备的。

        ONBUILD指令用来设置一些触发的指令,用于在当该镜像被作为基础镜像来创建其他镜像时(也就是Dockerfile中的FROM为当前镜像时)执行一些操作,ONBUILD中定义的指令会在用于生成其他镜像的Dockerfile文件的FROM指令之后被执行,上述介绍的任何一个指令都可以用于ONBUILD指令,可以用来执行一些因为环境而变化的操作,使镜像更加通用。

        注意:

        • ONBUILD中定义的指令在当前镜像的build中不会被执行。
        • 可以通过查看docker inspect 命令执行结果的OnBuild键来查看某个镜像ONBUILD指令定义的内容。
        • ONBUILD中定义的指令会当做引用该镜像的Dockerfile文件的FROM指令的一部分来执行,执行顺序会按ONBUILD定义的先后顺序执行,如果ONBUILD中定义的任何一个指令运行失败,则会使FROM指令中断并导致整个build失败,当所有的ONBUILD中定义的指令成功完成后,会按正常顺序继续执行build。
        • ONBUILD中定义的指令不会继承到当前引用的镜像中,也就是当引用ONBUILD的镜像创建完成后将会清除所有引用的ONBUILD指令。
        • ONBUILD指令不允许嵌套,例如ONBUILD ONBUILD ADD . /data是不允许的。
        • ONBUILD指令不会执行其定义的FROM或MAINTAINER指令。
        • 例如,Dockerfile使用如下的内容创建了镜像 image-A :
        [...]
        ONBUILD ADD . /app/src
        ONBUILD RUN /usr/local/bin/python-build --dir /app/src
        [...]
        

        如果基于 image-A 创建新的镜像时,新的Dockerfile中使用FROM image-A指定基础镜像时,会自动执行ONBUILD指令内容,等价于在后面添加了两条指令。

        FROM image-A
        #Automatically run the following
        ADD . /app/src
        RUN /usr/local/bin/python-build --dir /app/src
        

        使用场景

        Node.js

        假设我们要制作 Node.js 所写的应用的镜像。我们都知道 Node.js 使用 npm 进行包管理,所有依赖、配置、启动信息等会放到 package.json 文件里。在拿到程序代码后,需要先进行 npm install 才可以获得所有需要的依赖。然后就可以通过 npm start 来启动应用。因此,一般来说会这样写 Dockerfile:

        FROM node:slim
        RUN mkdir /app
        WORKDIR /app
        COPY ./package.json /app
        RUN [ "npm", "install" ]
        COPY . /app/
        CMD [ "npm", "start" ]
        

        把这个 Dockerfile 放到 Node.js 项目的根目录,构建好镜像后,就可以直接拿来启动容器运行。但是如果我们还有第二个 Node.js 项目也差不多呢?好吧,那就再把这个 Dockerfile 复制到第二个项目里。那如果有第三个项目呢?再复制么?文件的副本越多,版本控制就越困难,让我们继续看这样的场景维护的问题。

        如果第一个 Node.js 项目在开发过程中,发现这个 Dockerfile 里存在问题,比如敲错字了、或者需要安装额外的包,然后开发人员修复了这个 Dockerfile,再次构建,问题解决。第一个项目没问题了,但是第二个项目呢?虽然最初 Dockerfile 是复制、粘贴自第一个项目的,但是并不会因为第一个项目修复了他们的 Dockerfile,而第二个项目的 Dockerfile 就会被自动修复。

        那么我们可不可以做一个基础镜像,然后各个项目使用这个基础镜像呢?这样基础镜像更新,各个项目不用同步 Dockerfile 的变化,重新构建后就继承了基础镜像的更新?好吧,可以,让我们看看这样的结果。那么上面的这个 Dockerfile 就会变为:

        FROM node:slim
        RUN mkdir /app
        WORKDIR /app
        CMD [ "npm", "start" ]
        

        这里我们把项目相关的构建指令拿出来,放到子项目里去。假设这个基础镜像的名字为 my-node 的话,各个项目内的自己的 Dockerfile 就变为:

        FROM my-node
        COPY ./package.json /app
        RUN [ "npm", "install" ]
        COPY . /app/
        

        基础镜像变化后,各个项目都用这个 Dockerfile 重新构建镜像,会继承基础镜像的更新。

        那么,问题解决了么?没有。准确说,只解决了一半。如果这个 Dockerfile 里面有些东西需要调整呢?比如 npm install 需要统一加一些参数,那怎么办?这一行 RUN 是不可能放入基础镜像的,因为涉及到了当前项目的 ./package.json,难道又要一个个修改么?所以说,这样制作基础镜像,只解决了原来的 Dockerfile 的前4条指令的变化问题,而后面三条指令的变化则完全没办法处理。

        ONBUILD 可以解决这个问题。让我们用 ONBUILD 重新写一下基础镜像的 Dockerfile:

        FROM node:slim
        RUN mkdir /app
        WORKDIR /app
        ONBUILD COPY ./package.json /app
        ONBUILD RUN [ "npm", "install" ]
        ONBUILD COPY . /app/
        CMD [ "npm", "start" ]
        

        这次我们回到原始的 Dockerfile,但是这次将项目相关的指令加上 ONBUILD,这样在构建基础镜像的时候,这三行并不会被执行。然后各个项目的 Dockerfile 就变成了简单地:

        FROM my-node
        

        是的,只有这么一行。当在各个项目目录中,用这个只有一行的 Dockerfile 构建镜像时,之前基础镜像的那三行 ONBUILD 就会开始执行,成功的将当前项目的代码复制进镜像、并且针对本项目执行 npm install,生成应用镜像。

        Maven

        类似Java,Go等编译型项目,可以使用ONBUILD指令进行优化Dockerfile。

        编写onbuild Dockerfile如下:

        FROM maven:3-jdk-8
        
        RUN mkdir -p /usr/src/app
        WORKDIR /usr/src/app
        
        ONBUILD ADD . /usr/src/app
        
        ONBUILD RUN mvn install
        

        然后所有依赖maven编译的项目Dockerfile可以简化成如下形式:

        FROM maven:3.3-jdk-8-onbuild
        CMD ["java","-jar","/usr/src/app/target/demo-1.0-SNAPSHOT-jar-with-dependencies.jar"]
        

        官方Dockerfile示例:

        • Go
        • Perl
        • Hy
        • Ruby ? 著作权归作者所有
相关TAG标签
上一篇:Unable to connect to a repository at URL 'https://u-pc/svn/taotao/trunk/taotao/taotao-parent'
下一篇:栈帧详解
相关文章
图文推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站