[Docker] 写 Dockerfile 的最佳实践理论

通过llama.cpp与羊驼聊天的网页界面- 详解 Serge 的启动使用

 

  • 指导方针
 
创建短暂的容器
 
  意思是 container 可以停止和销毁,接着以最小化启动和配置进行重新构建和替换。
 
理解构建的上下文
 
  使用 docker build ,当前工作环境称为 构建的上下文,默认 Dockerfile 是在同级目录找,可通过 -f 指定 Dockerfile。
 
  无论 Dockerfile 实际在哪里,当前目录的所有递归的文件和目录的内容被发送到 docker daemon 作为构建的上下文。
 
  (无意中包含的不必要文件会增加 image 大小,增加 build/pull/push 时间和 container 运行时大小)
 
  上下文内容支持本地 PATH 和远程 URL,docker build [OPTIONS] PATH | URL | -
 
17.05开始支持用 stdin 管道化 Dockerfile 的内容
 
docker build -t foo:v1  .  -f-<<<EOF
FROM ubuntu:16.04
RUN echo "hello"
COPY  .  /copy-files
EOF
   
使用 .dockerignore 排除内容进入构建上下文
 
 
使用多级构建
 
  不需要努力去减少中间层数量和文件,从变化少的层到经常变化的层来排序(这样可以保证复用到构建历史缓存)
 
  不同层的顺序安排:安装工具 -> 安装库依赖 -> 生成应用
 
不安装不必要的包
 
解耦应用
 
  每一个 container 应该只关心一件事。解耦应用到多个容器可以让水平扩展更容易和重复使用容器。比如 web 技术栈的分为 应用/数据库/缓存 三个不同的容器。
 
最小化层的数量
 
  在 17.05 及更高版本中,通过 multi-stage 多级构建减少了这个限制。
 
给多行参数排序
 
  帮助避免包重复和更容易修改,更易读。
 
Run apt-get update && apt-get install -y \
    bzr \
    cvs \
    git \
    mercurial

 

利用构建缓存
 
  如果不想在构建中使用缓存的,给 docker build 命令加 --no-cache=true 选项。
 
    
  • Dockerfile 指令
        
    FROM - 只要有可能,使用当前官方仓库的作为基础 image。https://docs.docker.com/engine/reference/builder/#from
 
 
    LABEL - 通过object帮助组织image,每行以LABEL开头,有一个或多个 key-value 对。https://docs.docker.com/config/labels-custom-metadata/
 
 
    RUN - 把长且复杂的 RUN 语句用反斜线分割成多行,保持 Dockerfile 更可读,可理解,可维护。https://docs.docker.com/engine/reference/builder/#run
 
               把 update 和 install 放在一行,保证 Dockerfile 安装最近的包版本,如下:
 
RUN apt-get update && apt-get install -y \
                        package-foo \
                        package-bar

 

    APT-GET - 在 apt-get 应用中可能是最常使用的用于 RUN 的命令。因为它可以安装包,RUN apt-get 有几个陷阱需要提防。
 
                       不要使用 RUN apt-get dist-upgrade 和 dist-upgrade。因为许多来自父级 image 的基础的包不能在没有权限的 container 中升级。
 
                       如果父级 image 中的一个 package 过时了,联系它的维护者。
 
                       如果你知道有一个特定的包 foo 需要升级,使用 apt-get install -y foo 来自动更新。
 
                       总是把 RUN apt-get update 和 apt-get install 合并为一条 RUN 语句,确保无干涉的安装最新的包版本。
     
RUN apt-get update && apt-get install -y \
                                  package-foo \
                                  package-bar \
                                  package-baz=1.3.*
                              && rm -rf /var/lib/apt/lists/*          # 通过移除 apt cache 减小 image 尺寸

 

                      单独在一行 RUN 语句中使用 apt-get update 会引起缓存问题,随后的 apt-get install 指令失败,这叫做’ cache busting ’;同样可以通过指定包版本获取 cache-busting,这叫做版本固定,这可以避免由包变化而引起的意外失败。
 
                      官方 debian 和 ubuntu 的 image 会自动运行 *apt-get clean*,所以明确调用不是必需的。
     
 
    使用管道 - 如果想让管道连接的命令在任何阶段遇到错误时就失败,在命令前追加  set -o pipefail &&  来保证遇到未期的错误时阻止构建。
 
                      例如:RUN set -o pipefail && wget -O - https://some.site | wc -l > /number
 
                      不是所有的 shell 都支持 -o pipefail , 基于 debian 的 image 需要指定 bash
 
                      例如:RUN [ “/bin/bash”, “-c”, “set -o pipefail && wget -O - https://some.site | wc -l > /number" ]
 
 
    CMD - 该指令用于运行 image 内的软件,随同任何参数。https://docs.docker.com/engine/reference/builder/#cmd
 
                格式形式为 CMD ["command", "param1", "param2"],这个形式的指令推荐用在任何基于服务的 image。
 
                在大多数其他案例中,CMD 应该给一个交互式的shell,比如 CMD ["php", "-a"],意味着执行 docker run -it php,你会有一个可用的shell。
 
                CMD 很少以 CMD ["param", "param"] 的方式与 ENTRYPOINT 协同,除非你和用户已经非常熟悉 ENTRYPOINT 是如何工作的。
 
 
    EXPOSE - 该指令指示 container 在哪一个端口监听用于连接。https://docs.docker.com/engine/reference/builder/#expose
 
                      因此,应该使用常见传统的端口用于应用软件。例如 包含 Apache 的 image 将使用 EXPOSE 80,而包含 MongoDB 的 image 将使用 EXPOSE 27017 等等。
 
                      用于外部访问,使用者可以执行 docker run 带上一个标记来标识映射指定端口到他们选择的端口。
 
                      对于容器连接,Docker 为从接收容器返回到源的路径提供环境变量。(如,MYSQL_PORT_3306_TCP)
    
 
    ENV - 为了使新软件更容易运行,可以使用 ENV 来更新容器中安装软件的 PATH 环境变量。https://docs.docker.com/engine/reference/builder/#env
 
               例如:ENV PATH /usr/local/nginx/bin:$PATH 保证 CMD ["nginx"] 能运行。
 
               ENV 指令 在 为你希望容器化的服务提供所需的环境变量 上同样有用,例如 Postgres 的 PGDATA。
 
               每个 ENV 行创建一个新的中间层,就像 RUN 命令。这意味着即使在之后的层 unset 这个环境变量,它仍然存在于这个层,并且它的值可以被打印。测试如下:
FROM alpine
ENV ADMIN_USER="mark"
RUN echo $ADMIN_USER > ./mark
RUN unset ADMIN_USER
CMD sh
$ docker run --rm -it test sh echo $ADMIN_USER

 

                要阻止这种情况,真正 unset 环境变量,使用 RUN 指令运行 shell 命令,在一个独立的层中 set, use, unset 变量。
 
                使用 ; 或 && 来分割命令,使用 && 只要一个命令失败,docker build 也失败。
FROM alpine
ENV export ADMIN_USER="mark" \
       && echo $ADMIN_USER > ./mark \
       && unset ADMIN_USER
CMD sh

$ docker run --rm -it test sh echo $ADMIN_USER

 

 

    ADD 或 COPY - 尽管两者功能相似,一般来讲,首选 COPY。https://docs.docker.com/engine/reference/builder/#add | https://docs.docker.com/engine/reference/builder/#copy
 
                              因为 COPY 比 ADD 更易懂。COPY 只支持从本地文件到 container 的基本拷贝,而 ADD 有一些不明显的特性(如,本地 tar包 自动解压和支持远程 URL)
 
                              因此 ADD 的最佳使用是本地 tar 文件在 image 中的自动解压,如,ADD rootfs.tar.xz / .
 
                              如果 Dockerfile 有多个步骤使用了上下文中的不同文件,单独的拷贝它们,而不是一次拷贝所有。这确保每一步的构建缓存在文件发生改变时是失效的,如:                           
COPY requirements.txt /tmp
RUN pip install --requirement /tmp/requirements.txt
COPY . /tmp/

 

                              要让 RUN 步骤导致更少的缓存失效,那么把 COPY . /tmp/ 放到其前面。
 
                              因为 image 大小问题,使用 ADD 从远程地址拉取包是极不鼓励的;你应该使用 curl 或 wget 代替。这种方式你可以在解压后把不需要的文件删除,不需要把其它层加到你的 image 中。
 
                              避免做的是:
ADD http://example.com/big.tar.xz /usr/src/things/
RUN tar -zJf /usr/src/things/big.tar.xz -C /usr/src/things
RUN make -C /usr/src/things all

 

                              代替的做法是:
RUN mkdir -p /usr/src/things \
      && curl -SL http://example.com/big.tar.xz \
       |  tar -xJC /usr/src/things \
      && make -C /usr/src/things all

 

                              对于不需要 ADD 自动解压能力的文件和目录,你应该总是使用 COPY。
 
 
    ENTRYPOINT - 最好的使用点是设置 image 的主命令,允许 image 像命令一样运行(接着使用 CMD 做为默认标记)。https://docs.docker.com/engine/reference/builder/#entrypoint
ENTRYPOINT ["s3cmd"]
CMD ["--help"]
 
                             现在 image 可以像这样显示命令的帮助信息:$ docker run s3cmd
 
                             或者使用正确的参数来执行一个命令:$ docker run s3cmd ls s3://mybucket
 
                             这是有用的,因为 image 名称可以两次作为到如上命令二进制的引用。
 
                             ENTRYPOINT 指令同样可以用来和一个帮助脚本结合,允许它做和命令方式一样的事,尽管需要多进行一步 - 写脚本,以下是 Postgres 官方 image 的 ENTRYPOINT 使用的脚本:
#!/bin/bash
set -e
if [ "$1" = 'postgres' ]; then
  chown -R postgres "$PGDATA"
  if [ -z "$(ls -A "$PGDATA")" ]; then
    gosu postgres initdb
  fi
fi
exec "$@"

 

                             把帮助脚本拷贝到 container 中,并在 container 启动时通过 ENTRYPOINT 运行:          
COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["postgres"]

 

                             帮助脚本允许命令有多个参数可以交互,比如:
$ docker run postgres
$ docker run postgres postgres --help
$ docker run --rm -it postgres bash

 

    VOLUME - 指令应该用于暴露任何数据库存储区域,配置区域,或 docker 容器创建的文件/目录。https://docs.docker.com/engine/reference/builder/#volume
 
                       强烈建议你把 VOLUMN 使用在 image 中易变的或用户维护的部分。
 
 
    USER - 如果一个服务不需要权限可以运行,使用 USER 切换为非 root 用户。https://docs.docker.com/engine/reference/builder/#user
 
                 通过在 Dockerfile 中创建用户和组开始:RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres
 
                 image 中的用户和组被分配一个非确定的 UID/GID,下次 image 重建时重新分配。所以如果 UID/GID 至关重要,你应该分配一个准确的。
 
                (由于Go archive/tar 包中未解决的bug,在docker 容器中创建相当长的UID会耗尽磁盘,因为容器层中的 /var/log/faillog 填充了NULL字符,权宜措施是传递 --no-log-init 给 useradd)
 
                 避免使用 sudo,如果你确实需要像 sudo 一样的功能,例如像 root 一样初始化守护进程但以非 root 用户运行,考虑使用 gosu。https://github.com/tianon/gosu
 
                 为了减少层次和复杂性,避免频繁的切换用户。
 
 
    WORKDIR - 为了清晰和可靠,你应该总是为 WORKDIR 使用绝对路径。https://docs.docker.com/engine/reference/builder/#workdir
 
                         同样,你应该使用 WORKDIR 而不是 RUN cd ... && do-something ,更难度阅读和排除问题以及维护。
 
 
    ONBUILD - 命令在当前 Dockerfile 构建完毕执行。https://docs.docker.com/engine/reference/builder/#onbuild
 
                        ONBUILD 在当前 FROM image 派生的任何子 image 中执行。可以认为 ONBUILD 命令是父 Dockerfile 给子 Dockerfile 的指令。
 
                        Docker 构建时会在任何子 Dockerfile 中的命令前执行 ONBUILD。
 
                        ONBUILD 对于从给定的 image 构建 image 时有用。例如,你想为一个语言栈的 包含该语言写的任意用户软件到 Dockerfile 中的 image 使用 ONBUILD。
 
                        从 ONBUILD 构建的 image 应该有一个分割 tag,例如,ruby:1.9-onbuild 或 ruby-2.0-onbuild
 
                        当把 ADD 或 COPY 放到 ONBUILD 时要小心。如果新构建的上下文缺少被添加的资源,onbuild 构建 image 会灾难性失败。通过如上添加分割的 tag 来减轻这种影响。
 
 

posted on 2018-06-26 21:15  ercom  阅读(3709)  评论(1编辑  收藏  举报