Dockerfile最佳实践

 2018年7月1日 18:54   Nick王   云计算    0 评论   262 浏览 

Docker Server: 18.03.1-ce
Docker Client: 18.03.1-ce


AA

Dockerfile

Docker可以通过从Dockerfile读取指令自动构建Docker镜像。

构建方法

使用命令docker build命令来构建Docker镜像,需要Dockerfile和一个context。用于构建的context可以是一个PATH或者是一个URL;其中PATH可以是本地系统的一个目录,URL则可以是一个Git仓库位置。

上下文context是递归处理的。所以PATH将会包含任何的子目录,而URL则包含仓库的任何子模块。

整个构建镜像的过程是有Docker守护进程执行的,在构建的开始,会把context递归的都发送给Docker守护进程。

下面是一个使用当前目录为context的例子:

$ docker build .
Sending build context to Docker daemon  6.51 MB
...

通常,Dockerfile会被命名为Dockerfile并且会放置在context的根目录,如果不是,那么可以使用-f参数,指向任何位置的Dockerfile

$ docker build -f /path/to/a/Dockerfile .

还可以通过指定-t参数,来指定一个仓库和Tag来保存镜像:

$ docker build -t shykes/myapp .

可以是指定多个存储库:

$ docker build -t shykes/myapp:1.0.2 -t shykes/myapp:latest .

在Docker守护进程运行Dockerfile中的指令的时候,它会对Dockerfile进行一个预检查,如果有语法错误,就会返回一个错误:

$ docker build -t test/myapp .
Sending build context to Docker daemon 2.048 kB
Error response from daemon: Unknown instruction: RUNCMD

Docker守护进程逐个运行Dockerfile中的指令,在必要时将每条指令的结果提交给新镜像,最后输出新镜像的标识。Docker守护进程将自动清理您发送的上下文context

请注意,每条指令都是独立运行的,并且会导致创建一个新的镜像!所以RUN cd /tmp不会对下一条指令产生任何影响。

如果有可能,Docker会重新使用中间镜像(cache),来加快docker build的构建。如果使用,会在终端的输出中包含Using cache

$ docker build -t svendowideit/ambassador .
Sending build context to Docker daemon 15.36 kB
Step 1/4 : FROM alpine:3.2
 ---> 31f630c65071
Step 2/4 : MAINTAINER SvenDowideit@home.org.au
 ---> Using cache
 ---> 2a1c91448f5f
Step 3/4 : RUN apk update &&      apk add socat &&        rm -r /var/cache/
 ---> Using cache
 ---> 21ed6e7fbb73
Step 4/4 : CMD env | grep _TCP= | (sed 's/.*_PORT_\([0-9]*\)_TCP=tcp:\/\/\(.*\):\(.*\)/socat -t 100000000 TCP4-LISTEN:\1,fork,reuseaddr TCP4:\2:\3 \&/' && echo wait) | sh
 ---> Using cache
 ---> 7ea8aef582cc
Successfully built 7ea8aef582cc

如果不想使用缓存,可以指定参数--no-cache

Dockerfile格式

这是Dockerfile的格式:

# Comment
INSTRUCTION arguments

该指令不区分大小写,但是通常为了区分,建议采用大写。以#开头的是注释内容。注释中不支持换行符。

一个Dockerfile文件必须是以FROM指令开始,该指令指定了基于哪个基础镜像进行构建。Docker会按照Dockerfile中的指令顺序执行。

解析器指令

解析器指令是可选的。语法格式# directive=value。有如下特点:

  • 解析器指令不会向构建中添加镜像层,并且不会显示在构建步骤

  • 单个指令只能使用一次

  • 必须位于Dockerfile的最顶端的位置,包括注释。

  • 不支持换行

  • 通常都是小写

下面看一些无效的例子:

由于换行符,导致无效:

# direc \
tive=value

指令出现两次,导致无效:

# directive=value1
# directive=value2

FROM ImageName

由于出现在构建指令之后,被视为注释:

FROM ImageName
# directive=value

由于出现在注释之后,被视为注释:

# About my dockerfile
# directive=value
FROM ImageName

由于unknowndirective被视为注释,所以之后的也被视为注释:

# unknowndirective=value
# knowndirective=value

解析器指令中允许使用非换行符空格,下面几行被视为相同:

#directive=value
# directive =value
#   directive= value
# directive = value
#     dIrEcTiVe=value

escape 转义字符设置

# escape=\ (backslash)

或者

# escape=` (backtick)

指令escape用于在Dockerfile中设置转义字符。如果未指定,则缺省的转义字符为\

将转义字符设置为`在Windows上特别有用,其中\是目录路径分隔符。`与Windows PowerShell一致。

下面是一个在Windows上运行的Dockerfile:

# escape=`

FROM microsoft/nanoserver
COPY testfile.txt c:\
RUN dir c:\

环境变量替换

由ENV指令定义的环境变量,也可以在某些指令中用作由Dockerfile解释的变量。

环境变量在Dockerfile中以$variable_name或者是${variable_name}来表示。

语法${variable_name}还支持一些标准的bash修饰符,如下:

  • ${variable:-word},如果设置了variable,那么结果是variable;否则结果就是word

  • ${variable:+word}, 如果设置了variable,那么结果是word,否则结果是空字符串

Dockerfile中的以下指令列表支持环境变量:

  • ADD

  • COPY

  • ENV

  • EXPOSE

  • FROM

  • LABEL

  • STOPSIGNAL

  • USER

  • VOLUME

  • WORKDIR

整个指令中的环境变量替换将对每个变量使用相同的值。换句话说,在这个例子中:

ENV abc=hello
ENV abc=bye def=$abc
ENV ghi=$abc

最后def的结果是hello,而不是bye;然而ghi的结果是bye;因为它不是设置abcbye的相同命令的一部分。

.dockerignore文件

在docker CLI将上下文发送到docker守护程序之前,它会在上下文的根目录中查找名为.dockerignore的文件。如果此文件存在,CLI将修改上下文以排除匹配其中模式的文件和目录。这有助于避免不必要地向守护程序发送大型或敏感文件和目录。

下面是一个例子:

# comment
*/temp*
*/*/temp*
temp?

行开头!(感叹号)可用于排除例外。 以下是使用此机制的.dockerignore文件示例:

*.md
!README.md

FROM

FROM <image> [AS <name>]

或者

FROM <image>[:<tag>] [AS <name>]

或者

FROM <image>[@<digest>] [AS <name>]

FROM指令初始化一个新的编译阶段并为后续指令设置基本镜像。因此,有效的Dockerfile必须以FROM指令开始。

  • ARGDockerfile中可能位于FROM之前的唯一指令。

  • FROM可以在一个Dockerfile中出现多次,用于构建多个镜像;或者将一个构建阶段用作另一个构建阶段的依赖关系。

  • 通过添加AS name可以给新生成的阶段赋予一个名称。该名称可以用于后续的FROMCOPY --from=<name|index>指令引用此阶段构建的镜像。

  • tagdigest是可选的。如果忽略,如果忽略,那么值为latest

了解ARG和FROM如何交互

FROM指令允许,在第一个FROM指令之前用ARG指令定义的变量,下面看例子:

ARG  CODE_VERSION=latest
FROM base:${CODE_VERSION}
CMD  /code/run-app

FROM extras:${CODE_VERSION}
CMD  /code/run-extras

ARG定义的变量不能直接在构建阶段使用,如果要使用,在构建阶段内声明没有值得变量:

ARG VERSION=latest
FROM busybox:$VERSION
ARG VERSION
RUN echo $VERSION > image_version

RUN

RUN有两种形式:

  • RUN <command>: shell形式,命令运行在一个shell中,默认情况下是Linux上的/bin/sh -c

  • RUN ["executable", "param1", "param2"]: exec形式

RUN指令会基于当前顶部的镜像的新的镜像层中执行任何命令,并提交结果,生成新的镜像层,将用于后续的Dockerfile。

在shell模式中,可以使用\将单个RUN继续到下一行,如下例子:

RUN /bin/bash -c 'source $HOME/.bashrc; \
echo $HOME'

等同于:

RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOME'

注意:在使用exec模式的时候,由于会被解析成JSON数组,所以一定要使用双引号。

CMD

CMD有三种形式:

  • CMD ["executable","param1","param2"]: exec模式,推荐使用

  • CMD ["param1","param2"]: 作为ENTRYPOINT的默认参数

  • CMD command param1 param2: shell形式

Dockerfile中只能有一个CMD指令。如果列出多个CMD,则只有最后一个CMD才会生效。CMD的主要目的是为执行容器提供默认值。

同样,使用exec模式,要使用双引号。

当以shell或exec格式使用时,CMD指令设置运行镜像时要执行的命令。

如果您使用CMD的shell形式,那么<command>将在/bin/sh -c中执行:

FROM ubuntu
CMD echo "This is a test." | wc -

如果用户指定docker run参数,那么它们会覆盖CMD中指定的默认值。

不要将RUN与CMD混淆。RUN实际上运行一个命令并提交结果; CMD在构建时不执行任何操作,但是指定了镜像的预期命令。

ENTRYPOINT

ENTRYPOINT有两种形式:

  • ENTRYPOINT ["executable", "param1", "param2"]: exec模式,推荐

  • ENTRYPOINT command param1 param2:shell模式

ENTRYPOINT允许我们配置一个可执行程序来允许容器。

只有Dockerfile中的最后一条ENTRYPOINT才会生效。

命令行docker run <image>的所有参数,都会被追加到ENTRYPOINT的exec表单之后,并且会覆盖CMD的所有元素。可以使用docker run --entrypoint标志覆盖ENTRYPOINT指令。

如果使用shell模式,那么ENTRYPOINT会作为/bin/sh -c的子命令,这样会导致不传递信号。也就是说,容器中你得程序的PID并不是1,当执行docker stop <container>的时候你的应用不会收到SIGTERM的信号。

你可以使用ENTRYPOINT来设置默认不变的命令和参数,使用CMD设置额外的,可变的默认值:

FROM ubuntu
ENTRYPOINT ["top", "-b"]
CMD ["-c"]

可以这样运行容器:

$ docker run -it --rm --name test  top -H

重新开一个终端进行检查:

$ docker exec -it test ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  2.6  0.1  19752  2352 ?        Ss+  08:24   0:00 top -b -H
root         7  0.0  0.1  15572  2164 ?        R+   08:25   0:00 ps aux

可以为ENTRYPOINT指定一个纯字符串,它将在/bin/sh -c中执行。为了确保docker stop会将信号传递给ENTRYPOINT的可执行文件,你要记得使用exec

FROM ubuntu
ENTRYPOINT exec top -b

特别注意:使用exec执行的程序的PID将会是1。

此时可以使用docker stop来进行测试,容器中的PID为1的进程是否收到了SIGTERM信号:

$ /usr/bin/time docker stop test
test
real    0m 0.20s
user    0m 0.02s
sys 0m 0.04s

如果忘记使用exec,换句话说,容器中的PID进程为1的进程是/bin/sh -c, 效果如下:

$ docker exec -it test ps aux
PID   USER     COMMAND
    1 root     /bin/sh -c top -b cmd cmd2
    7 root     top -b
    8 root     ps aux

如果你运行docker stop test,容器不会干净,优雅的退出。会在超时(默认是10s, 使用docker stop --help查看)之后,被迫发出一个SIGKILL信号,如下所示:

$ /usr/bin/time docker stop test
test
real    0m 10.19s
user    0m 0.04s
sys 0m 0.03s

ENTRYPOINT 和 CMD

  1. 在Dockerfile中应该至少指定ENTRYPOINT和CMD中的一个

  2. ENTRYPOINT应该在使用容器作为可执行文件的时候定义

  3. 应该使用CMD作为定义ENTRYPOINT命令的缺省参数

  4. 当使用命令行参数运行容器的时候,CMD会被覆盖

ADD

ADD有如下两种使用形式:

  • ADD [--chown=<user>:<group>] <src>... <dest>

  • ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]: 当路径中包含有空格的时候,使用该方法

ADD指令会从<src>中复制新文件、目录或者是远程文件URL,并将它们添加到镜像中路径为<dest>的文件系统中。

其中<src>可以使用通配符, <dest>可以是绝对路径或者是相对于WORKDIR的相对路径,案例如下:

ADD hom* /mydir/        # adds all files starting with "hom"
ADD hom?.txt /mydir/    # ? is replaced with any single character, e.g., "home.txt"

ADD test relativeDir/          # adds "test" to `WORKDIR`/relativeDir/
ADD test /absoluteDir/         # adds "test" to /absoluteDir/

ADD遵循以下规则:

  • <src>的路径必须位于构建的上下文(context)中,ADD ../something /something是错误的

  • 如果<src>是一个URL,并且<dest>没有以斜杠结尾。那么会从URL下载文件,并复制到<dest>

  • 如果<src>是一个URL,并且<dest>是以斜杠结尾的。那么会从URL推断出文件名,最后把文件下载到<dest>/<filename>

  • 如果<src>是目录,则复制目录的全部内容,包括文件系统元数据。目录本身不被复制,只是它的内容。

  • 如果<src>是一个压缩文件,那么它会被解压缩为一个目录

  • 如果<src>是任何其他类型的文件,则将其与元数据一起单独复制。如果<dest>以尾部斜杠/结束,则将其视为目录

  • 如果指定了多个<src>资源(直接或由于使用通配符),那么<dest>必须是一个目录,并且它必须以斜杠/结尾

  • 如果<dest>不以结尾的斜杠结尾,则它将被视为常规文件,<src>的内容将写入<dest>

  • 如果<dest>不存在,则会在其路径中创建所有缺少的目录。

COPY

COPY有两种使用形式:

  • COPY [--chown=<user>:<group>] <src>... <dest>

  • COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]: 如果路径中有空格,使用这种形式

COPY 还接收一个可选的参数--from=<name|index>

COPY遵循以下规则:

  • <src>的路径必须位于构建的上下文(context)中,COPY ../something /something是错误的

  • 如果<src>是目录,则复制目录的全部内容,包括文件系统元数据。不复制目录本身,只复制其内容

  • 如果<src>是任何其他类型的文件,则将其与元数据一起单独复制。如果<dest>以尾部斜杠/结束,则将其视为目录

  • 如果指定了多个<src>资源(直接或由于使用通配符),那么<dest>必须是一个目录,并且它必须以斜杠/结尾

  • 如果<dest>不以结尾的斜杠结尾,则它将被视为常规文件,<src>的内容将写入<dest>

  • 如果<dest>不存在,则会在其路径中创建所有缺少的目录。

ENV

ENV <key> <value>
ENV <key>=<value> ...

ENV指令将环境变量<key>设置为值<value>

ENV myName="John Doe" myDog=Rex\ The\ Dog \
    myCat=fluffy

等同于

ENV myName John Doe
ENV myDog Rex The Dog
ENV myCat fluffy

设置的这些环节变量,在后续的Dockerfile中可以使用,也可以在最终的镜像中保留。可以使用docker inspect查看这些环节变量。可以使用docker run --env <key>=<value>来更改他们。

WORKDIR

WORKDIR /path/to/workdir

WORKDIR为Dockerfile中的任何RUN、CMD、ENTRYPOINT、COPY和ADD指令设置工作目录。

WORKDIR指令可以在Dockerfile中多次使用。如果提供了相对路径,则它将相对于先前WORKDIR指令的路径。

WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

最终的输出是/a/b/c

WORKDIR指令可以解析先前使用ENV设置的环境变量。并且,只能使用Dockerfile设置的显示的变量:

ENV DIRPATH /path
WORKDIR $DIRPATH/$DIRNAME
RUN pwd

最终输出是/path/$DIRNAME

VOLUME

VOLUME ["/data"]

VOLUME指令创建一个具有指定名称的挂载点,并将其标记为从本地主机或者其他容器中存储外部安装的卷。

该值可以是JSON数组,VOLUME ["/var/log/"]或者具有多个参数的纯字符串VOLUME /var/log或者VOLUME /var/log /var/db

FROM ubuntu
RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
VOLUME /myvol

此Dockerfile会创建一个新的镜像,并导致docker run会在/myvol创建一个新的挂载点,然后将新创建的greeting拷贝到新创建的卷中。

注意以下几点:

  • 如果任何构建步骤在声明后更改卷中的数据,那么这些更改将被丢弃

  • 该列表被解析为JSON数组,所有要使用双引号

  • 主机目录在容器运行的时候再指定。为了可移植性,不能在Dockerfile中挂在一个主机目录。VOLUME参数不支持指定host-dir参数。必须在创建和启动容器时指定挂载点。

USER

USER <user>[:<group>] or
USER <UID>[:<GID>]

USER指令设置用户名和可选的用户组。在运行镜像的时候以及后续Dockerfile中执行的任何RUN,CMD和ENTRYPOINT指令,都会是这个用户。

EXPOSE

EXPOSE <port> [<port>/<protocol>...]

EXPOSE指令通知Docker,该容器在运行时侦听指定的网络端口。您可以指定端口是侦听TCP还是UDP,如果未指定协议,则默认为TCP。

EXPOSE指令不会实际发布端口。它只是告诉容器或者别人,哪些端口准备发布。

要在运行容器时实际发布端口,请在docker run运行的时候指定-p或者-P参数。

EXPOSE 80/tcp
EXPOSE 80/udp

在这种情况下,如果您使用docker run运行-P,则端口将暴露一次TCP和一次UDP。

无论EXPOSE设置如何,您都可以在运行时使用-p标志覆盖它们。

docker run -p 80:80/tcp -p 80:80/udp ...

LABEL

LABEL <key>=<value> <key>=<value> <key>=<value> ...

LABEL指令为镜像添加元数据。LABEL是一个键值对。

下面是几个用法示例:

LABEL "com.example.vendor"="ACME Incorporated"
LABEL com.example.label-with-value="foo"
LABEL version="1.0"
LABEL description="This text illustrates \
that label-values can span multiple lines."

可以在一条指令中添加多个:

LABEL multi.label1="value1" multi.label2="value2" other="value3"

LABEL multi.label1="value1" \
      multi.label2="value2" \
      other="value3"

可以从父镜像继承,如果key相同,那么将覆盖。可以使用docker inspect查看:

"Labels": {
    "com.example.vendor": "ACME Incorporated"
    "com.example.label-with-value": "foo",
    "version": "1.0",
    "description": "This text illustrates that label-values can span multiple lines.",
    "multi.label1": "value1",
    "multi.label2": "value2",
    "other": "value3"
},

MAINTAINER (弃用)

MAINTAINER <name>

推荐使用LABEL进行设置,而不是使用该指令。比如:

LABEL maintainer="SvenDowideit@home.org.au"

ARG

ARG <name>[=<default value>]

ARG指令定义了一个变量,用户可以在构建期间,通过给命令docker build使用--build-arg <varname>=<value>传递给构建器。如果用户指定了构建参数,但是在Dockerfile中没有定义,那么构建过程中会输出警告信息:

[Warning] One or more build-args [foo] were not consumed.

Dockerfile可能包含一个或多个ARG指令。例如,以下是有效的Dockerfile:

FROM busybox
ARG user1
ARG buildno
...

如果ARG指令具有默认值,并且在构建时没有值传递,那么构建器将使用默认值:

FROM busybox
ARG user1=someuser
ARG buildno=1
...

要在多个阶段使用arg,每个阶段都必须包含ARG指令。

FROM busybox
ARG SETTINGS
RUN ./run/setup $SETTINGS

FROM busybox
ARG SETTINGS
RUN ./run/other $SETTINGS

可以使用ARG或ENV指令来指定RUN指令可用的变量。使用ENV指令定义的环境变量总是覆盖相同名称的ARG指令。与ARG指令不同,ENV值始终保留在构建的镜像中。

Docker预定义了一部分ARG,如下:

  • HTTP_PROXY

  • http_proxy

  • HTTPS_PROXY

  • https_proxy

  • FTP_PROXY

  • ftp_proxy

  • NO_PROXY

  • no_proxy

要使用它们,只需要在命令行传递即可:

-build-arg <varname>=<value>

ONBUILD

ONBUILD [INSTRUCTION]

当镜像被用作另一个构建的基础时,ONBUILD指令为镜像添加一个稍后执行的触发指令。这个触发器会在下游构建的上下文中执行。就好像它在下游的Dockerfile中的FROM指令之后立即插入一行。

任何构建指令都可以注册成触发器。

如果您正在构建将用作构建其他镜像的基础的镜像,这非常有用。

工作原理:

  1. 当它遇到ONBUILD指令时,构建器会为正在构建的镜像的元数据添加一个触发器。该指令不会影响当前的构建。

  2. 在构建结束时,所有触发器的列表都存储在镜像清单中的OnBuild键下。可以使用docker inspect命令检查它们。

  3. 稍后,可以使用FROM指令将图像用作新构建的基础。作为处理FROM指令的一部分,下游构建器会查找ONBUILD触发器,并按照它们的注册顺序执行它们。如果任何触发器失败,那么FROM指令将中止,从而导致构建失败。如果所有触发器都成功,则FROM指令完成并且构建继续照常。

  4. 触发器在执行后从最终图像中清除。

使用案例:

[...]
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
[...]

STOPSIGNAL

STOPSIGNAL signal

该指令设置,当容器退出的时候,给容器发送的信号。该信号可以是和内核的系统调用表中有效的信号。比如,9 或者 SIGNAME格式的信号名,比如 SIGKILL 。

HEALTHCHECK

有两种表现形式:

  • HEALTHCHECK [OPTIONS] CMD command 通过在容器内部运行命令来检查容器运行情况

  • HEALTHCHECK NONE 禁用从基础镜像继承的任何健康检查

HEALTHCHECK指令告诉Docker,如何测试容器以检查它是否工作。因为,即使进程正在运行,那也不能表示服务正常。

当指定健康检查的时候,Docker除了正常的状态,还包含health status。这个状态,初始值为starting。当健康检查通过,它会变成healthy。经过一定数量的监控检查失败之后,状态会变成unhealthy

CMD之前可以出现的选项如下:

  • --interval=DURATION: 间隔时间,默认是30s

  • --timeout=DURATION: 超时时间,默认是30s

  • --start-period=DURATION: 默认是0s

  • --retries=N: 重试次数,默认是3

可选项start period为容器启动需要的时间提供初始化时间。在这个时间如果探针检查失败,不会计入最大重试次数。

Dockerfile中只能有一个HEALTHCHECK指令。如果您列出多个,则只有最后一个HEALTHCHECK将生效。

命令的退出状态指示容器的运行状况。可能的值如下:

  • 0: 成功,容器健康并且随时可用

  • 1:不健康 - 容器工作不正常

  • 2:保留 - 不使用此退出代码

使用案例:

HEALTHCHECK --interval=5m --timeout=3s \
  CMD curl -f http://localhost/ || exit 1

在Docker 1.12中添加了HEALTHCHECK功能。

SHELL

SHELL ["executable", "parameters"]

编写Dockerfile的最佳实践

Docker镜像由只读镜像层组成,每个镜像层代表一个Dockerfile指令。这些镜像层都是堆叠的。看如下的Dockerfile:

FROM ubuntu:15.04
COPY . /app
RUN make /app
CMD python /app/app.py

每条指令都会创建一个镜像层:

  • FROM: 从ubuntu:15.04创建一个Docker镜像

  • COPY: 从Docker客户端的当前目录添加文件

  • RUN: 使用make构建应用程序

  • CMD: 指定在容器内运行的命令

了解构建上下文

当发出docker build命令的时候,当前的工作目录被称为是构建上下文。默认情况下,Dockerfile位于该目录,但是可以使用参数-f来指定其他位置。无论Dockerfile实际存在于哪里,当前目录中文件和目录的所有递归内容都将作为构建上下文发送到Docker守护进程。

下面是一个构建的案例, 明确指定了Dockerfile的位置和上下文,并且不使用缓存:

mkdir -p dockerfiles context
mv Dockerfile dockerfiles && mv hello context
docker build --no-cache -t helloapp:v2 -f dockerfiles/Dockerfile context

无意中包含构建镜像不需要的文件会导致更大的构建上下文和更大的镜像大小。这可以增加构建镜像的时间,拉取和推送镜像的时间以及容器运行时大小。

使用标准输入给定Dockerfile

从版本17.05之后,可以指定远程的构建上下文,可以通过标准输入提供Dockerfile:

docker build -t foo https://github.com/thajeztah/pgadmin4-docker.git -f-<<EOF
FROM busybox
COPY LICENSE config_local.py /usr/local/lib/python2.7/site-packages/pgadmin4/
EOF

善于使用.dockerignore文件

跟Git的.gitignore一样,在Dockerfile的上下文也可以提供.dockerignore,来排除与构建无关的文件,这会减少构建上下文的大小。

使用多阶段构建

多阶段构建,可以大幅度的减少最红镜像的大小,并且可以毫不费力的减少中间镜像层和文件的数量。

如果你得构建包含多个镜像层,可以从较不经常更改的版本,以确保版本缓存可以重复使用,对较频繁更新的进行排序:

  • 安装构建程序所必须的工具

  • 安装或者更新库依赖项

  • 生成应用程序

Go应用程序的Dockerfile可能如下所示:

FROM golang:1.9.2-alpine3.6 AS build

# 安装项目需要的工具
# Run `docker build --no-cache .` to update dependencies
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep

# List project dependencies with Gopkg.toml and Gopkg.lock
# 这些图层仅在Gopkg文件更新时重新构建
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
# 安装库依赖
RUN dep ensure -vendor-only

# 复制整个项目并构建
# 当项目目录中的文件发生更改时,该层将被重建
COPY . /go/src/project/
RUN go build -o /bin/project

# 结果是一个单层镜像
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]

不要安装不必要的软件包

为了降低复杂性,依赖性,文件大小和构建时间,避免安装额外的或不必要的软件包。例如,您不需要在数据库镜像中包含文本编辑器。

解耦应用程序

每个容器应该只有一个问题。将应用程序分离到多个容器中可以更轻松地水平缩放和重用容器。例如,Web应用程序堆栈可能由三个独立的容器组成,每个容器都有其独特的镜像,以分离的方式管理Web应用程序,数据库和内存缓存。

将每个容器限制在一个进程中是一个很好的经验法则,但这不是一条硬性规定。因为,某些应用程序可能会自行生成其他进程。

总之,尽可能保持容器的清洁和模块化。如果容器相互依赖,则可以使用Docker容器网络来确保这些容器可以通信。

最小化层数

在旧版本的Docker中,最大限度的减少镜像中的层数量以确保它们具有高性能非常重要。添加了以下功能以减少此限制:

  • 在Docker 1.10之后的版本,只有指令RUN,COPY,ADD创建图层。其他指令创建临时中间图像,而不是直接增加构建的大小。

  • 在Docker 17.05及更高版本中,您可以执行多阶段构建,并只将需要的构件复制到最终镜像中。

对多行参数进行排序

这有助于避免软件包的重复,并使列表更容易更新。

下面是一个例子:

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

利用构建缓存

构建镜像时,Docker会逐步执行Dockerfile中的指令,按指定的顺序执行每个指令。在检查每条指令时,Docker会在其缓存中查找可以重用的现有镜像,而不是创建新的(重复)镜像。

如果您根本不想使用缓存,可以在docker build命令中使用--no-cache = true选项。

Docker使用缓存遵循的基本规则如下:

  • 从已经在缓存中的父镜像开始,将下一条指令与从该基本镜像派生的所有子镜像进行比较,以查看是否使用完全相同的指令构建了其中的一条。如果不是,则缓存无效。

  • 在大多数情况下,只需将Dockerfile中的指令与其中一个子镜像进行比较就足够了。

  • 对于ADD和COPY指令,将检查映像中文件的内容,并为每个文件计算校验和。在这些校验和中不考虑文件的最后修改时间和最后访问时间。如果文件中有任何更改(例如内容和元数据),则缓存无效。

  • 除了ADD和COPY命令之外,高速缓存检查不会查看容器中的文件以确定高速缓存匹配。例如,处理RUN apt-get -y update命令时,不检查容器中更新的文件以确定是否存在缓存命中。在这种情况下,只是命令字符串本身用于查找匹配。

一旦缓存失效,所有后续的Dockerfile命令将生成新镜像,并且不使用缓存。

基础镜像选择

只要有可能,请使用当前的官方存储库作为图像的基础。我们推荐使用Alpine图像,因为它严格控制,体积小(目前小于5 MB),同时仍然是完整的Linux发行版。

RUN指令使用注意事项

注意,应该始终要将RUN apt-get updateapt-get install写在同一个语句中:

这是正确的写法:

RUN apt-get update && apt-get install -y \
        package-bar \
        package-baz \
        package-foo

这是错误的写法:

FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y curl

ADD 和 COPY

尽管ADD和COPY在功能上相似,但一般来说,COPY是首选。这是因为它比ADD更透明。COPY只支持将本地文件基本复制到容器中,而ADD则具有一些功能。

ADD的最佳用途是将本地tar文件自动提取到镜像中。

如果您有多个使用上下文中不同文件的Dockerfile步骤,请单独复制它们,而不是一次复制它们。这可确保每个步骤的构建缓存仅在特定所需文件发生更改时才会失效。

下面是一个最佳的例子:

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

因为/tmp的缓存失效几率更高。

由于镜像大小很重要,因此强烈建议不要使用ADD从远程URL中获取包。你应该使用curl或wget代替。这样,您可以删除提取后不再需要的文件,也不必在镜像中添加其他镜像层。

下面是一个绝佳的例子:

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

尽量使用非root账号

如果服务可以在没有权限的情况下运行,请使用USER将其更改为非root用户。

首先在Dockerfile中创建用户和组,例如RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres

为了减少层次和复杂性,请避免频繁切换USER。

避免安装或使用sudo,因为它具有可能导致问题的不可预知的TTY和信号转发行为。如果您绝对需要与sudo类似的功能,例如以root身份初始化守护程序,但将其作为非root用户运行),请考虑使用“gosu”。

使用绝对路径

为了清晰和可靠,您应该始终为您的WORKDIR使用绝对路径。


参考链接:


如无特殊说明,文章均为本站原创,转载请注明出处
  • 转载请注明来源:Dockerfile最佳实践
  • 本文永久连接地址: http://ibash.cc/frontend/article/105/