dockerfile编写的一些规范和建议
在写dockerfile前,我们可以花一点时间去了解每个指令的作用以及如何才能优化我们的dockerfile。本文档包含docker官方推荐的最佳实践和指令方法。
准则和建议
容器生命周期很短,我们可以很容易的创建、销毁、配置容器。
基于dockerfile构建docker镜像时,默认情况下,当前的工作目录被称为构建上下文,我们也可以使用(-f)指定Dockerfile在不同的位置。无论dockerfile实际存在于哪里,当前目录中包含的文件和目录及其递归内容都将作为构建的上下文发送到dockerd daemon进程。
使用.dockerignore. 为了避免在编译镜像时一些无关紧要的文件,我们可以采用.dockerignore文件来排除文件和目录,类似.gitignore作用一样。
-
使用多阶段编译。在docker 17.05及更新的版本,则可以使用多阶段构建来极大减少最终映像的大小。比如Go应用程序编译成一个镜像步骤如下:
- 安装构建应用程序所需的工具(dep或者git),安装应用依赖库(dep ensure),生成应用程序(go build)
- 编译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"]
避免安装不必要的包,构建镜像应该尽可能减少复杂性、依赖关系、构建时间及镜像大小。
每个容器只关心一件事。所以最好不要在同个容器启动多个进程。
-
减少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以及更高版本添加了对多阶段构建的支持,这允许我们只将需要的构件复制到最终图像中即可。极大简化了最终镜像的大小。
-
构建缓存,大家知道 Docker 构建镜像的过程是顺序执行 Dockerfile 每个指令的过程。执行过程中,Docker 将在缓存中查找可重用的镜像,如果不想使用缓存,你也可以使用 docker build --no-cache=true ... 命令。
如果使用缓存,docker 将使用一下基本规则:- 从第一条指令开始,它将比较从基础镜像导出的所有子镜像,查看是否有相同的的构建指令,以此来获取缓存。
- 在大多数情况下,简单地比较 Dockerfile 与其中一个子镜像的指令是足够的。但是,某些说明需要更多的检查和解释。
- 对于 ADD 和 COPY 指令,会去比较文件的校验和,但不考虑文件的修改时间和访问时间。如果有任何变化,缓存无效。
- 除了 ADD 和 COPY 指令,缓存检查不会查看容器中的文件来确定缓存匹配。例如,当处理 RUN apt-get -y update 命令时,将不会检查在容器中更新的文件以确定是否存在高速缓存命中。在这种情况下,只需使用命令字符串本身来查找匹配。
一旦缓存无效,所有后续 Dockerfile 命令将生成新的映像,并且高速缓存将不被使用。
容器进程需要作为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"]