Docker用户指南(1) – 编写Dockerfile的最佳实践

Docker通过读取Dockerfile里的指令来自动构建一个镜像。Dockerfile是一个包含了所有用于构建镜像的命令的文本文件。
Dockerfile遵循特定的格式来使用一组特定的指令。你可以在Dockerfile Reference了解其基础知识。
本文涵盖了Docker,Inc推荐的最佳实践和方法。以及Docker社区创建易于使用的,有效的Dockerfile文件。

一般准则和建议

容器应该是精简的

用来生成容器的Dockerfile文件应该尽可能的精简。意味着它可以停止和销毁并生成一个新的最小配置的容器替换旧的。

使用.dockerignore文件

在大多数情况下,最好把Dockerfile放到一个空的目录下。然后只添加构建Dockerfile所需的文件。为了提升构建性能,你应该通过添加一个.dockerignore文件到那个目录来排除文件和目录。这个文件的排除语法与.gitignore文件类似。

避免安装不必要的包

为了减少复杂性,依赖性,文件大小和构建时间,你应该避免安装额外的或不必要的包。例如,你不需要在一个数据库镜像添加一个文本编辑器。

一个容器一个进程

在决大多数情况中,你应该在一个容器只运行一个进程。将应用程序解耦到多个容器中使得容器更易水平扩展和重用。如果一个服务依赖另一个服务,使用容器链接功能。

尽量减少层的数量

你需要在Dockerfile的可读性(以及因此的长期可维护性)和最小化它使用的层数之间找到平衡。 要慎重引用新的数据层。

排序多行参数

尽可能的通过以字母数字排序多行参数以方便以后的更改。这会帮助你避免重复的软件包以及之后更容易地更新这个列表。通过添加反斜杠,可以使代码更易读。如下示例:

  1. RUN apt-get update && apt-get install -y \
  2.   bzr \
  3.   cvs \
  4.   git \
  5.   mercurial \
  6.   subversion

构建缓存

在构建一个镜像期间,Docker将按顺序执行Dockerfile中的每一个指令。当执行每个指令前,Docker会在缓存中查找可以重复的镜像,而不是创建一个新的,重复的镜像。如果不想使用缓存可以在docker build命令中加入–no-cache=true参数。
不过当你要用Docker镜像缓存时,很有必要了解Docker什么时候会和什么时候不会使用缓存。Docker将遵循的基本规则如下:

  • 现在你要重新构建已存在缓存中的镜像,docker将指令与该缓存镜像导出的数据层作对比看它们中的任意一个数据层构建使用的指令是否一样。如果不一样,则认为缓存是无效的。
  • 在大多数情况下仅仅对比Dockerfile中的与数据层的指令就足够了。不过某些指令需要更多的检查和解释。
  • 对于ADD和COPY指令,检查镜像中文件的内容,并计算每个文件的checksum。文件最后修改时间和最后访问时间不会影响到checksum结果。在查找缓存时,将对比当前文件与缓存镜像中文件的checksum。如果文件有更改,如内容和元数据,那么缓存将失效。
  • 除了ADD和COPY命令,docker不会通过对比文件的checksum来决定缓存是否匹配。如当执行apt-get -y update命令时,docker不会对比更新的文件的checksum,只会对比命令本身。
  • 一旦一个指令的缓存无效,接下来的Dockerfile命令将生成新的数据层,不会再使用缓存。

    Dockerfile指令

    FROM

    只要有可能,使用当前的官方镜像作为你的基础镜像。我们推荐使用Debian镜像,因为非常严格控制并保持最小大小(目前150mb以下),然后仍然是一个完整的发行版本。

    LABEL

    你可以向镜像添加标签,以帮助按项目组织镜像,记录许可信息,帮助自动化或出于其他原因。 对于每个标签,添加一行以LABEL开头,并使用一个或多个键值对。 以下示例显示了不同的可接受格式。

    1. # Set one or more individual labels
    2. LABEL com.example.version="0.0.1-beta"
    3. LABEL
    4. LABEL com.example.release-date="2015-02-12"
    5. LABEL com.example.version.is-production=""
    6.  
    7. # Set multiple labels on one line
    8. LABEL com.example.version="0.0.1-beta" com.example.release-date="2015-02-12"
    9.  
    10. # Set multiple labels at once, using line-continuation characters to break long lines
    11. LABEL vendor=ACME\ Incorporated \
    12.       com.example.is-beta= \
    13.       com.example.is-production="" \
    14.       com.example.version="0.0.1-beta" \
    15.       com.example.release-date="2015-02-12"

    RUN

    一如既往,为了使你的Dockerfile更易读,可理解和可维护,使用反斜杠分隔复杂的RUN语句。
    使用RUN最常用的场景可能是使用apt-get安装软件。RUN apt-get是安装软件的命令,有几个问题需要注意。
    你应该避免使用RUN apt-get upgrade或dist-upgrade,因为许多来自基础镜像的“必需”包不会在非特权容器内升级。如果一个基础镜像的软件包过期了,你应该联系它的维护者。如果你知道的foo软件包需要升级,使用apt-get install -y foo来自动更新它。
    始终将RUN apt-get update与apt-get install组合在同一RUN语句中,例如:

    1. RUN apt-get update && apt-get install -y \
    2.         package-bar \
    3.         package-baz \
    4.         package-foo

    在一个RUN语句中单独使用apt-get update可能会引起缓存问题和随后的apt-get install指令失败。例如,你有这样一个Dockerfile:

    1. FROM ubuntu:14.04
    2.     RUN apt-get update
    3.     RUN apt-get install -y curl

    构建镜像后,所有的数据层在docker缓存中。假设你要安装额外的包:

    1. FROM ubuntu:14.04
    2.     RUN apt-get update
    3.     RUN apt-get install -y curl nginx

    docker看到第一个指令RUN apt-get update没有更改就开始使用上一次的缓存。因为docker使用了缓存,所以导致apt-get update没有执行。因为apt-get update没有被执行,那么有可能curl和nginx的包是过期的版本。
    使用RUN apt-get update && apt-get install -y确保你的Dockerfile安装最新的软件包版本,无需进一步的编码或手动干预。
    这种技术被称为“cache busting”。 你还可以通过指定软件包版本来实现缓存无效化。 例如:

    1. RUN apt-get update && apt-get install -y \
    2.         package-bar \
    3.         package-baz \
    4.         package-foo=1.3.*

    指定版本强制构建镜像时安装特定版本的软件而不管缓存里的是什么版本。
    以下是apt-get安装软件时推荐的格式:

    1. RUN apt-get update && apt-get install -y \
    2.     aufs-tools \
    3.     automake \
    4.     build-essential \
    5.     curl \
    6.     dpkg-sig \
    7.     libcap-dev \
    8.     libsqlite3-dev \
    9.     mercurial \
    10.     reprepro \
    11.     ruby1.9.1 \
    12.     ruby1.9.1-dev \
    13.     s3cmd=1.1.* \
    14.  && rm -rf /var/lib/apt/lists/*

    s3cmd指令指定一个1.1.0*版本。如果之前的镜像使用的是一个旧版本,指定一个新版本会使缓存失效而开始执行apt-get update命令,从而确保安装了新的版本。
    另外,清除apt缓存和删除/var/lib/apt/lists能有效减小镜像大小。

    注意:官方的Debian和Ubuntu镜像会自动执行apt-get clean,所以不需要我们显示调用。

    CMD

    CMD指令用来运行镜像里的软件,命令后面可以添加参数。CMD指令的格式为CMD [“executable”, “param1”, “param2”…]。因此如果镜像是用于运行服务,如Apache和Rails,指令应该为CMD [“apache2″,”-DFOREGROUND”]。实际上这种格式适用于所有运行服务的镜像。
    在大多数其它情况下,CMD应该使用一个交互式的shell,如bash,python的perl。例如,CMD [“perl”, “-de0”], CMD [“python”], 或CMD [“php”, “-a”]. 使用这种形式意味着当你执行如docker run -it python,你会进入到一个可用的shell。CMD应该很少以CMD [“param”,“param”]的形式与ENTRYPOINT连接使用,除非你对ENTRYPOINT很熟悉。

    EXPOSE

    EXPOSE指令表示容器中的哪个端口用来监听连接。因此你应该使用常见的惯例的端口。例如,Apache web server应该EXPOSE 80端口,而MongoDB容器应该EXPOSE 27017等。
    对于容器需要外部访问的时候,用户可以执行docker run跟随一个参数来映射指定的端口,此时EXPOSE对这种情况无作用。对于container linking,Docker为链接容器提供了访问被链接容器的路径环境变量,如PHP容器连接到MySQL容器的环境变量MYSQL_PORT_3306_TCP。

    ENV

    为了使新软件更容易运行,你可以使用ENV来更新PATH环境变量。例如ENV PATH /usr/local/nginx/bin:$PATH会确保CMD [“nginx”]正常运行。
    ENV指令也可以为你想容器化的软件指定所需的环境变量,如Postgres的PGDATA环境变量。
    最后,ENV也可以用来指定一个版本号,为之后的安装配置软件使用,以便更好的进行维护。

    ADD或COPY

    虽然ADD和COPY功能类似,一般来讲,首选COPY。因为它比ADD更透明。COPY只是比本地文件复制到容器中,而ADD有一些其它的功能(如会解压tar文件和下载远程文件)不会很明显。ADD最佳用途是将本地的tar文件自动解压到镜像中,如ADD rootfs.tar.xz /。
    如果在Dockerfile中有多处需要不同的文件,每个文件单独使用一个COPY,而不是使用一个COPY指令一次复制完。这保证了当其中的某个文件更新时,只是这个文件的缓存失效,其它的还是能够正常使用缓存。
    例如:

    1. COPY requirements.txt /tmp/
    2. RUN pip install --requirement /tmp/requirements.txt
    3. COPY . /tmp/

    这个示例当除requirements.txt其它文件更新时,前两步还是能够使用缓存的,如果只用一条COPY指令,那么/tmp/目录里的文件一旦更新,缓存将全部失效。
    由于关系到镜像的大小,不推荐使用ADD来获取远程文件;你应该使用curl或wget替代。用这种方式你可以当文件解压后删除原来的压缩文件,且没有新加一层数据层。例如,应该避免如下用法:

    1. ADD http://example.com/big.tar.xz /usr/src/things/
    2. RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things
    3. RUN make -C /usr/src/things all

    使用如下方法:

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

    对于其它不需要自动解压文件的情况,你应该始终使用COPY。

    ENTRYPOINT

    ENTRYPOINT最佳用途是设置镜像的主命令,允许镜像作为命令一样运行(然后使用CMD设置默认参数)
    如下示例:

    1. ENTRYPOINT ["s3cmd"]
    2. CMD ["--help"]

    运行如下命令会显示命令帮助:

    1. $ docker run s3cmd

    或者设置参数:

    1. $ docker run s3cmd ls s3://mybucket

    ENTRYPOINT指令还可以与helper脚本结合使用,允许它以类似于上述命令的方式工作,即使启动软件可能需要多个步骤。
    例如,Postgres Official Image使用以下脚本作为其ENTRYPOINT:

    1. #!/bin/bash
    2. set -e
    3.  
    4. if [ "$1" = 'postgres' ]; then
    5.     chown -R postgres "$PGDATA"
    6.  
    7.     if [ -z "$(ls -A "$PGDATA")" ]; then
    8.         gosu postgres initdb
    9.     fi
    10.  
    11.     exec gosu postgres "$@"
    12. fi
    13.  
    14. exec "$@"

    注意:脚本里使用bash命令exec,使得程序以容器的PID 1运行。这样程序就能接收到发送到容器的Unix信号。

    帮助程序脚本复制到容器中,并通过容器启动时的ENTRYPOINT运行:

    1. COPY ./docker-entrypoint.sh /
    2. ENTRYPOINT ["/docker-entrypoint.sh"]
    3. CMD ["postgres"]

    该脚本允许用户以几种方式运行Postgres.
    最简单启动Postgres的方法:

    1. $ docker run postgres

    或者运行Postgres并传递参数过去:

    1. $ docker run postgres postgres --help

    最后,也可以启动一个完全不同的工具,如bash:

    1. $ docker run --rm -it postgres bash

    VOLUME

    VOLUME指令应用于公开由docker容器创建的任何数据库存储区域,配置存储或文件/文件夹。
    强烈建议你对镜像的任何更改和/或用户可维护的部分使用VOLUME。

    USER

    如果服务可以在没有权限的情况下运行,请使用USER更改为非root用户。 首先在Dockerfile中创建一个类似于RUN groupadd -r postgres && useradd -r -g postgres postgres的用户和组。

    注意:镜像中的用户和组获得非确定性的UID / GID,因为无论镜像如何重建,都会分配“下一个”UID / GID。 所以,如果它是关键的,你应该分配一个显式的UID / GID。

    你应该避免安装或使用sudo,因为它具有不可预测的TTY和信号转发行为,可能导致更多的问题。 如果你绝对需要类似于sudo的功能(例如,以root身份初始化守护程序,但以非root身份运行它),则可以使用“gosu”。
    最后,为了减少数据层和复杂性,避免频繁地来回切换USER。

    WORKDIR

    为了清晰和可靠,你应该始终为WORKDIR使用绝对路径。 此外,你应该使用WORKDIR,而不是如RUN cd …&do-something,这很难维护。

    标签:Docker 发布于:2019-11-20 05:47:40