Dockerfile规范及建议

dockerfile编写的一些规范和建议

在写dockerfile前,我们可以花一点时间去了解每个指令的作用以及如何才能优化我们的dockerfile。本文档包含docker官方推荐的最佳实践和指令方法。

准则和建议

  1. 容器生命周期很短,我们可以很容易的创建、销毁、配置容器。

  2. 基于dockerfile构建docker镜像时,默认情况下,当前的工作目录被称为构建上下文,我们也可以使用(-f)指定Dockerfile在不同的位置。无论dockerfile实际存在于哪里,当前目录中包含的文件和目录及其递归内容都将作为构建的上下文发送到dockerd daemon进程。

  1. 使用.dockerignore. 为了避免在编译镜像时一些无关紧要的文件,我们可以采用.dockerignore文件来排除文件和目录,类似.gitignore作用一样。

  2. 使用多阶段编译。在docker 17.05及更新的版本,则可以使用多阶段构建来极大减少最终映像的大小。比如Go应用程序编译成一个镜像步骤如下:

    1. 安装构建应用程序所需的工具(dep或者git),安装应用依赖库(dep ensure),生成应用程序(go build)
    2. 编译go应用的最终镜像

    通过多个阶段构建,我们最终镜像无需关心步骤1做什么事,只需要将步骤1的最终结果通过步骤2生成最终镜像即可。这个特性可以极大减少镜像大小以及layer数量。

    FROM golang:1.9.2-alpine3.6 AS build
    
    # Install tools required to build the project
    # We need to run `docker build --no-cache .` to update those dependencies
    RUN apk add --no-cache git
    RUN go get github.com/golang/dep/cmd/dep
    
    # Gopkg.toml and Gopkg.lock lists project dependencies
    # These layers are only re-built when Gopkg files are updated
    COPY Gopkg.lock Gopkg.toml /go/src/project/
    WORKDIR /go/src/project/
    # Install library dependencies
    RUN dep ensure -vendor-only
    
    # Copy all project and build it
    # This layer is rebuilt when ever a file has changed in the project directory
    COPY . /go/src/project/
    RUN go build -o /bin/project
    
    # This results in a single layer image
    FROM scratch
    COPY --from=build /bin/project /bin/project
    ENTRYPOINT ["/bin/project"]
    CMD ["--help"]
    
    
  3. 避免安装不必要的包,构建镜像应该尽可能减少复杂性、依赖关系、构建时间及镜像大小。

  4. 每个容器只关心一件事。所以最好不要在同个容器启动多个进程。

  5. 减少layer数量。排序多行参数,通过版本管理时我们可以清楚看到我们修改的变化。

    • 在docker1.10以及更高版,RUN、COPY, and ADD指令都会叠加多个layer,所以尽量在一个指令中完成。例如需要安装很多依赖
    RUN apk --no-cache update && \
        apk --no-cache add curl \
        git \
        make \
        docker \
        bzr \
        cvz
    
    • docker 17.05以及更高版本添加了对多阶段构建的支持,这允许我们只将需要的构件复制到最终图像中即可。极大简化了最终镜像的大小。
  1. 构建缓存,大家知道 Docker 构建镜像的过程是顺序执行 Dockerfile 每个指令的过程。执行过程中,Docker 将在缓存中查找可重用的镜像,如果不想使用缓存,你也可以使用 docker build --no-cache=true ... 命令。
    如果使用缓存,docker 将使用一下基本规则:

    • 从第一条指令开始,它将比较从基础镜像导出的所有子镜像,查看是否有相同的的构建指令,以此来获取缓存。
    • 在大多数情况下,简单地比较 Dockerfile 与其中一个子镜像的指令是足够的。但是,某些说明需要更多的检查和解释。
    • 对于 ADD 和 COPY 指令,会去比较文件的校验和,但不考虑文件的修改时间和访问时间。如果有任何变化,缓存无效。
    • 除了 ADD 和 COPY 指令,缓存检查不会查看容器中的文件来确定缓存匹配。例如,当处理 RUN apt-get -y update 命令时,将不会检查在容器中更新的文件以确定是否存在高速缓存命中。在这种情况下,只需使用命令字符串本身来查找匹配。
      一旦缓存无效,所有后续 Dockerfile 命令将生成新的映像,并且高速缓存将不被使用。
  2. 容器进程需要作为server运行时候,主命令需要通过前台运行。默认情况下docker容器启动时,主进程会以PID 1进程号启动。而容器仅在它的1号进程运行时,才会保持运行。

以上是dockerfile编写前的一些准则,下面介绍如何更好的编写Dockerfile的一些建议

Dockerfile 指令

FROM

尽可能的使用官方仓库存储的镜像作为基础镜像。官方建议使用 Alpine,它大小仅5mb左右,麻雀虽小五脏俱全,用户态工具基本都有。

建议:私有registry存在时,可以通过官方镜像from下来自己维护,可以自定义调整时区、基础命令等

LABEL

我们可以给镜像添加标签(LABEL),如记录仓库地址,维护人联系方式等等。label标签以键值对的形式出现,如果包含空格请用""扩起来。标签对象必须唯一,否则后者会覆盖前者。键可以包含 .、-、a-zA-Z、0-9。下面是一些例子:

# Set one or more individual labels
LABEL maintainer="Yichen Wang <wangycc1028@gmail.com>" \
       reference="https://github.com/wangycc/jumpserver" \
       nodejs=5.12.0 \
       tengine=2.2.0

建议: 可以通过label标记项目仓库地址,维护人联系方式、依赖的版本等信息

RUN

最常见的应该是安装软件包和一些shell命令,如 RUN apk --no-cache add curl git ....,我们可以通过 \ 分隔成多行便于查看软件列表的变更。如

apk --no-cache update && \
    apk --no-cache add curl \
        git \
        docker \
        nodejs 

建议:多个脚本命令通过&&符号写在一个run指令中,可以有效减少layer数量。通过""分割行,可以方便预览命令变更。

CMD

如果你的镜像用于中间件 Server,CMD 的形式一般都是 CMD [“executable”, “param1”, “param2”…]。如 CMD ["apache2","-DFOREGROUND"]。

CMD 还在大多数情况以交互式的方式出现。如 CMD ["python"],当你执行 docker run -it python 的时候,将进入 shell 的交互模式。

CMD 很少以 CMD [“param”, “param”] 协同 ENTRYPOINT 工作,除非我们很清楚它们俩的运行机制。

EXPOSE

指定容器侦听端口,应该尽量使用应用程序通用的传统端口,如 Apache Web 服务器使用 EXPOSE 80 等。这个指令只是声明标记,具体不会在创建容器时被应用。

建议: dockerfile中通过EXPOSE 标记服务会listen的端口

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

这两者很相似,这里建议优先选择COPY,它比ADD透明度更高。

COPY只支持将本地文件复制到容器中。ADD除了COPY的功能外,还支持远程URL下载。但最好的用途是将本地tar文件提取到镜像中比如:

ADD rootfs.tar.xz /

如果在Dockerfile中使用不用的文件,那么COPY它们可以单独使用。这样,特定文件的更改,将确保每一步的构建缓存无效,如:

COPY requirements.txt /tmp/
RUN pip install --requirement /tmp/requirements.txt
COPY . /tmp/

将COPY . /tmp/ 放在后面,这能够使 RUN 的缓存无效的数量减少。

因为镜像大小很重要,故用 ADD 远程 URL 提取包是不被鼓励的,因该使用 curl 或 wget 替代。这样,能够减小镜像的层数。例如,你应该避免这样做:

ADD http://example.com/big.tar.xz /usr/src/things/
RUN tar -xJf /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 tar 自动提取功能的其他项目(文件,目录),我们应该始终使用 COPY。

ENTRYPOINT

ENTRYPOINT 的最好的用途时设置镜像的主命令,用 CMD 作为参数,这样就可以是镜像像命令一样运行,在容器启动时,我们只需要覆盖CMD参数即可。如:

CMD和ENTRYPOINT共存时,CMD会被当成ENTRYPOINT的参数传入,比如:

ENTRYPOINT ["python"]
CMD ["--help"]

当容器运行时候我们可以覆盖CMD指令,比如:

docker run python -c "import sys ;print sys.version"

#-c "import sys ;print sys.version" 会覆盖掉--help参数。

ENTRYPOINT 也可以于脚本组合使用,如一个 Postgres Official Image 的例子。:

#!/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 bash 命令 ,使最终运行的应用程序成为容器的 PID 1。这允许应用程序接收发送到容器任何 Unix 信号。将脚本复制到容器,并通过 ENTRYPOINT 开始运行:

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

建议在业务的dockerfile中用entrypoint作为镜像的主命令,CMD作为主命令默认的flag

VOLUME

VOLUME 指令应该用于如下内容:任何类型的数据库存储区域、配置存储、容器创建的文件或目录。
推荐 VOLUME 用于挂载镜像中那些经常变化(易变化的)或者用户可维护的部分。

USER
如果一个服务不需要超级权限来运行,我们可以通过 USER 切换成非 root 用户。在 Dockerfile 中用如下方式创建:

RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres
USER postgres

建议为了减少层数和复杂度,避免频繁使用 USER 进行用户切换

WORKDIR

我们使用WROKDIR作为工作路径。而不是增加复杂的命令,如 RUN cd … && do-something这样难以阅读以及排障困难和难以维护。

COPY cli /data/apps/bin/
WORKDIR /data/apps/bin
ENTRYPOINT ["cli"]

参考地址: Best practices for writing Dockerfiles:

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

推荐阅读更多精彩内容