Dockerfile的编写

记录Dockerfile的编写规则和用法

Dockerfile就是用于构建image的一系列命令和参数构成的脚本,通过docker build -t <image_name:tag> -f </path/to/Dockerfile> .来构建。

docker build命令从名为Dockerfile的文件和context来构建image,context是PATH(本地目录)或者URL(Git repository位置)处的文件。context会以递归方式处理,所以PATH的子目录和git的submodules都会处理,同样这里要小心用于作为PATH的目录最好不要有与镜像无关的文件,通常会新建一个空文件夹做为context的PATH。

PATH下的.dockerignore可以用于排除文件和目录。

构建工作由Docker守护进程运行,而不是docker的CLI,其中-t参数用于指定镜像的repository和tag,可以有多个-t-f指定Dockerfile的路径,最后的.表示上下文件环境为当前目录。

例如:

1
docker build -t blueyi/python-3.6:dev -f ~/docker/Dockerfile .

.dockerignore

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

.dockerignore文件中以#开头的行将被视为注释,!开头用于排除例外(即不排除)

另外有一个特殊的通配符**,它匹配任何数量的目录(包含零),例如**/*.go将排除所有目录中找到的以.go结尾的所有文件,包含构建上下文的根。

举例:

1
2
3
4
# comment
*/temp*
*/*/temp*
temp?

解释:

  • # comment 忽略
  • /temp* 在根的任何直接子目录中排除其名称以temp开头的文件和目录。 例如,普通文件/somedir/temporary.txt被排除,目录/somedir/temp也被排除。
  • */*/temp* 从根目录下两级的任何子目录中排除以temp开头的文件和目录。 例如,排除了/somedir/subdir/temporary.txt
  • temp? 排除根目录中名称为temp的单字符扩展名的文件和目录。 例如,/tempa/tempb被排除。

Dockerfile编写规范

Dockerfile文件内容格式如下:

1
2
# Comment
INSTRUCTION arguments

其中INSTRUCTION不区分大小写,但建议大写,第一条指令必须是FROM,用于指定构建镜像的基础镜像,#后面跟注释,但解析器指令除外(Parser directives)

每一条指令都会独立运行,相互之间没有没有上下文关系

在构建过程中每次生成一层新的镜像的时候这个镜像就会被缓存。即使是后面的某个步骤导致构建失败,再次构建的时候就会从失败的那层镜像的前一条指令继续往下执行。
如果不想使用这种缓存功能,可以在构建的时候加上--no-cache选项:

1
docker build --no-cache -t="blueyi/centos" .

解析器指令(Parser directives)

解析器指令为可选的,会影响后续处理Dockerfile行,解析器指令形如:

1
# directive = value

解析器指令必须放在Dockerfile的最顶端,使用行延续(\)、出现2次、写在任何构建指令后等都将导致解析器指令无效。
暂时只有一个解析器指令escape,用于指定Dockerfile中的转义字符,如果未定义,则默认为\,用法如下:

1
# escape=\

或者

1
# escape=`

为了避免windows上的路径中会有\\(例如:c:\\),建议windows上将转义符设置为

1
`

环境变量(Environment replacement)

环境变量由ENV语句声明(ENV其实也是一个指令),可以用于其他一些指令中被解释出来,声明之后可以通过类似于shell中变量引用的方式引用:$variable_name或者${variable_name}。同样支持类似bash的修饰符:

  • ${variable:-word} 表示如果设置了variable,则结果将是该值。如果variable未设置,那么word将是结果。
  • ${variable:+word} 表示如果设置了variable,那么word将是结果,否则结果是空字符串。

ENV指令用法如下:

1
2
ENV <key> <value>
ENV <key>=<value> ...

第一种单行的形式value部分的任何内容包含空格,都会成为key的值,而第二种形式可以设置多个对,引号和反斜杠可用于值内包含空格。如:

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

等价于:

1
2
3
4
ENV myName John Doe
ENV myDog Rex The Dog
ENV myCat fluffy
`

当前前一咱更好,构建的镜像更高效。

可以通过反斜扛\\来进行对$转义。用例:

1
2
3
4
5
FROM busybox
ENV foo /bar
WORKDIR ${foo} # WORKDIR /bar
ADD . $foo # ADD . /bar
COPY \$foo /quux # COPY $foo /quux

由于每一条ENV对应一个镜像层,所以环境变量替换具有延迟性,即环境变量的声明将只会在其之后的指令中才有效
例如:

1
2
3
ENV abc=hello
ENV abc=bye def=$abc
ENV ghi=$abc

def值为hello,不是bye,因为该句中设置的变量只在后面的指令中有效。而ghi的值为bye,因为abc在上一条指令中被修改为bye。

环境变量支持以下命令:

  • ADD
  • COPY
  • ENV
  • EXPOSE
  • LABEL
  • USER
  • WORKDIR
  • VOLUME
  • STOPSIGNAL

以及ONBUILD与上面指令的组合(1.4之前的版本不支持)。

注意:

  • ENV定义的环境变量不能被CMD引用
  • 通过ENV定义的环境变量会永久保存到通过该镜像创建的任何容器中,可以通过env来查看
  • 可以通过docker run命令中通过-e <key>=<value>(或--env)参数来传递环境变量,以便在运行容器后命令或修改该变量,如:docker run -it -e "TEST=hello" <image>

定义环境变量的同时可以引用已经定义的环境变量,ENV指令中可以直接引用的环境变量有:HOMEHOSTNAMEPATHTERM(默认是xterm)。

举例:

如下Dockerfile是一个非常不合理的处理方式,因为多条的ENV可以合并为一条。

1
2
3
4
5
6
7
RUN set -ex && apt-get update && apt-get install -y iputils-ping
ENV PATH /usr/local/bin:$PATH
ENV LANG C.UTF-8
ENV TERM xterm
ENV PYTHON_VERSION 3.5.3
ENV name1=ping name2=on_ip
CMD $name1 $name2

可以使用docker inspect来查看镜像的ENV

Dockerfile指令

FROM

FROM用于为后续的指令指定其运行所需要的基础镜像(Base Image),所以Dockerfile中FROM必须是第一个指令(ARG除外),如果指定的该镜像不在本地,则Docker会自动从远程仓库获取。

新的docker版本中引入了一个新的指令ARG,该指令是唯一可以出现在FROM之前的指令,可以配合FROM指令来指定一些FROM指令中想要引用的变量,例如版本信息。

FROM指令用法如下:

1
2
3
4
5
FROM <image> [AS <name>]
# 或者
FROM <image>[:<tag>] [AS <name>]
# 或者
FROM <image>[@<digest>] [AS <name>]
  • ARG是唯一可以出现在FROM之前的指令
  • FROM可以在单个Dockerfile中多次出现,以创建多个镜像
  • AS <name>选项为可选,如果有的话,该name将可以用于后面的FROM或COPY --from=<name|index>指令中的name,用以引用该image。
  • tagdigest也为可选的,如果省略,则默认为latest

ARGFROM一起使用的一个用法:

1
2
3
4
5
6
ARG  CODE_VERSION=latest
FROM base:${CODE_VERSION}
CMD /code/run-app

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

RUN

RUN用于运行命令。
RUN有2种形式:

  • RUN <command>shell形式,命令在shell中运行,Linux上为/bin/sh -c,Windows上为cmd /S/C
  • RUN ["executable","param1","param2"](exec 形式)

每一条RUN指令都会提交一个新层到当前镜像,提交结果将会用于后续Dockerfile中的指令, 镜像历史类似于版本控制系统,可以从任何历史点进行镜像的创建,可以通过命令docker history <image>查看镜像历史层

shell形式的RUN命令支持续行,例如:

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

等价于

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

如果要使用不同的shell,例如bash,则需要使用exec形式,如:

1
RUN ["/bin/bash", "-c", "echo hello"]

由于exec形式作为JSON数组解析,所以单词必须使用双引号(”)而不是单引号(’)括住。

每一次运行RUN指令时,由于会重用缓存,并且不会对缓存进行有效性检查,例如RUN apt-get dist-upgrade -y生成的缓存也会用于下一次build,可以通过docker build --no-cache来禁止使用缓存。

CMD

CMD指令用于指定容器启动时需要运行的程序。例如我们通常运行容器时为了能够执行其中的/bin/bash来进入到容器中交互,会如下运行:

1
docker run -it <image> /bin/bash

后面的/bin/bash就相当于告诉容器运行之后运行/bin/bash,等效于我们在Dockerfile中添加一条:

1
CMD ['/bin/bash']

如果Dockerfile已经有了CMD指令,我们传递的运行命令将会覆盖其中的CMD设置。

一个Dockerfile中只能有一个CMD指令

CMD指令三种形式:

  • CMD ["executable","param1","param2"] (exec形式, 首选形式)
  • CMD ["param1","param2"] (做为ENTRYPOINT中命令的默认参数)
  • CMD command param1 param2 (shell形式,类似RUN,<cmd>将在/bin/sh -c中执行)

当CMD指令做为ENTRYPOINT指令时的命令参数时,同样是以JSON格式解析,所以也要使用双引号来包围参数而不是单引号。

尽量各命令参数都使用双引号而不是单引号

注意:CMD命令与RUN命令完全不同,RUN运行一个命令将提交一个镜像的层(layer),而CMD在构建时并不执行任何操作,只是用于指定容器的运行时要执行的命令

LABEL

LABEL指令用于向镜像中添加元数据,可以通过docker inspect命令查看,如

1
docker inspect --format='{{.Config.Labels}}' <image>

每一个LABEL指令都会产生一个镜像层,所以尽量将多条LABEL放到一条中。

LABEL用法:

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

例如:

1
2
3
4
5
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."

等价于:

1
2
3
4
5
LABEL "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."

MAINTAINER (deprecated)

该指令已经新废弃,用于指定该镜像的维护者,现在可以使用LABEL更好的实现该功能。用法如下:

1
MAINTAINER <name>

等价于:

1
LABEL maintainer="name"

EXPOSE

用于指定容器运行后容器中监听的端口(也称为私有端口),当运行容器时使用-P参数时,容器将自动为监听端口分配宿主主机上相应的映射端口(也称为公共端口)。

用法:

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

协议可选为TCPUDP,如果不指定协议,则默认为TCP

虽然支持EXPOSE时指定端口映射(EXPOSE 80:8080私有80到公共8080),而由于镜像在构建时并不能确定当容器运行时该端口是否被其他程序占用,所以不建议在Dockerfile中进行端口映射。

用例:

1
2
3
4
5
6
# PORT
EXPOSE 8080
EXPOSE 22
EXPOSE 8009
EXPOSE 8005
EXPOSE 8443

ENV

同上面的环境变量部分

ADD

用于从指定目录或者URL拷贝文件到镜像中。
用法如下:

1
2
ADD <src>... <dest>
ADD ["<src>",... "<dest>"] # 用于路径中带有空格的情况

注意:

  • src不是URL时,必须是相对于本地上下文件环境路径的相对路径,因为Docker会有构建之前将上下文件内容全部发送给Docker Daemon。
  • dest结尾带有/时,docker会自动推测文件名,将目标文件拷贝到镜像中。(与linux中的cp命令其实一样)
  • src为目录时,将复制目录里面的所有内容,目录本身不会被复制
  • src为可以识别的压缩格式(tar.gz、gzip、bzip2、xz)等时,docker将会自动将其解压,解压功能类似于tar -x,但src为url时不可以
  • dest必须是绝对路径,或者相对于WORKDIR的相对路径。

用法举例:

不好的用法

1
2
3
4
ADD http://foo.com/package.tar.bz2 /tmp/
RUN tar -xjf /tmp/package.tar.bz2 \
&& make -C /tmp/package \
&& rm /tmp/package.tar.bz2

实际上最后的删除并不会生效,因为rm命令将位于另一个独立的镜像层。

可以这样:

1
2
3
RUN curl http://foo.com/package.tar.bz2 \
| tar -xjC /tmp/package \
&& make -C /tmp/package

COPY

用法与ADD几乎完全一样,但更纯粹,不支持URL,不支持自动解压。

所以现在Docker团队推荐使用COPY进行文件拷贝,而不是ADD

ENTRYPOINT

ENTRYPOINT指令与CMD非常相似,都是指定容器运行后需要运行的命令,不同的是当与docker run配合使用时,docker run后跟的执行内容将做为ENTRYPOINT指令指定的运行命令的参数,例如:

1
2
3
4
5
6
FROM centos
MAINTAINER allocator
RUN yum install -y nginx
RUN echo 'hello world' > /usr/share/nginx/html/index.html
EXPOSE 80
ENTRYPOINT ["/usr/sbin/nginx"]

启动容器

1
docker run --name test -p 1080:80 -it test_nginx -g "daemon off"

后面两个参数-g "daemon off"将传递给/usr/sbin/nginx作为参数运行。

当CMD与ENTRYPOINT同时出现时,如果CMD的内容不是一个完整的指令,则不带参数运行容器时,CMD的内容将做为ENTRYPOINT指令的参数。而如果此时CMD是一个完整的命令,它将覆盖掉ENTRYPOINT中的内容。如果此时运行容器时再带参数,则此参数会覆盖掉CMD指令的内容。

所以,可以这样使用:

1
2
ENTRYPOINT ["/usr/sbin/nginx"]
CMD ["-g daemon off"]

VOLUME

VOLUME指令为容器创建挂载卷,类似于docker run时使用的-v命令进行目录映射,只是-v可以指定本地目录到容器指定目录的挂载,而Dockerfile中只能指定从该镜像创建容器时容器中的挂载点,具体对应的本地目录将由docker自动分配,可以通过docker inspect <container>查看。

用法:

1
VOLUME ["/data1", "/data2", ...]

USER

USER指令用于设置其后的RUNCMDENTRYPOINT命令的运行用户,因为默认docker将以root身份运行这些命令。

用法:

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

WORKDIR

WORKDIR用于设定Dockerfile中其命令之后的RUNCMDENTRYPOIHNTCOPYADD命令的执行路径。如果指定的WORKDIR不存在,则会自动被创建。该命令可以出现多次,如果使用的是相对目录,则该相对目录将会相对于前一个WORKDIR设置的目录。

用法:

1
WORKDIR /path/to/workdir

举例:

1
2
3
4
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

最终pwd输出为/a/b/c

WORKDIR中可以使用前面ENV设置的环境变量,如:

1
2
3
ENV DIRPATH /path
WORKDIR $DIRPATH/$DIRNAME
RUN pwd

pwd输出为/path/$DIRNAME

ARG

ARG指令在Docker 1.9版本才引入,该指令用于定义变量,其定义的变量只在build镜像过程中有效,镜像创建完成后消失,并可以通过build命令的--build-arg <varname>=<value>来指定其构建过程中varname的值。

可以有多个ARG指令。

ARG定义的变量只在其后的指定中才有效。

通过命令docker history可以看到在构建镜像时设置的ARG

如果build过程中指定了在Dockefile中未经ARG定义的变量,构建将会给出警告:

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

用法:

1
ARG <name>[=<default value>]

ARG设置的变量只会影响到构建过程,而ENV相当于改变了整个相应容器的环境变量。
如果在ARG指令的后面使用ENV进行了同名的定义,则即使在build时指定的新值,ENV的定义依然会覆盖ARG定义。

ARGENV配合使用:

1
2
3
4
FROM ubuntu
ARG CONT_IMG_VER
ENV CONT_IMG_VER ${CONT_IMG_VER:-v1.0.0}
RUN echo $CONT_IMG_VER

可以这样构建:

1
docker build --build-arg CONT_IMG_VER=v2.0.1 .

Docker中预定义了一些ARG变量:

  • HTTP_PROXY
  • http_proxy
  • HTTPS_PROXY
  • https_proxy
  • FTP_PROXY
  • ftp_proxy
  • NO_PROXY
  • no_proxy

这些变量可以在构建时直接进行设置,如:

1
--build-arg HTTP_PROXY=http://user:pass@proxy.lon.example.com

ONBUILD

ONBUILD指令用于为镜像添加一个触发器,其参数是任意一个Dockerfile指令,该指令不会在当前的build过程中生效,而是会在当FROM这个镜像创建新镜像时首先触发执行。

用法:

1
ONBUILD [INSTRUCTION]

以下内容引用于网络:

当我们在一个Dockerfile文件中加上ONBUILD指令,该指令对利用该Dockerfile构建镜像(比如为A镜像)不会产生实质性影响。

但是当我们编写一个新的Dockerfile文件来基于A镜像构建一个镜像(比如为B镜像)时,这时构造A镜像的Dockerfile文件中的ONBUILD指令就生效了,在构建B镜像的过程中,首先会执行ONBUILD指令指定的指令,然后才会执行其它指令。

需要注意的是,如果是再利用B镜像构造新的镜像时,那个ONBUILD指令就无效了,也就是说只能再构建子镜像中执行,对孙子镜像构建无效。其实想想是合理的,因为在构建子镜像中已经执行了,如果孙子镜像构建还要执行,相当于重复执行,这就有问题了。

利用ONBUILD指令,实际上就是相当于创建一个模板镜像,后续可以根据该模板镜像创建特定的子镜像,需要在子镜像构建过程中执行的一些通用操作就可以在模板镜像对应的dockerfile文件中用ONBUILD指令指定。 从而减少dockerfile文件的重复内容编写。

STOPSIGNAL

STOPSIGNAL指令用于设置容器退出时所要发送给容器的退出信号,必须是内核系统调用表中的合法值,如9SIGKILL

用法:

1
STOPSIGNAL signal

HEALTHCHECK

HEALTHCHECK指令用于告诉docker如何去检测容器的健康状态。1.12的版本中加入,用法如下:

1
2
HEALTHCHECK [OPTIONS] CMD command # 通过运行一个CMD指令指定的命令来检查容器健康状态
HEALTHCHECK NONE # 禁用从基础镜像(base image)继承来的任何健康检查

CMD命令可以带有参数有:

  • --interval=DURATION (default: 30s) 表示检查的时间间隔
  • --timeout=DURATION (default: 30s) 表示检查命令多久
  • --start-period=DURATION (default: 0s) 表示启动预留给容器启动的时间
  • --retries=N (default: 3) 表示命令重试次数

每一个Dockerfile中只能有一个HEALTHECK指令,如果有多个,则只有最后一个有效。

用例:

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

上述示例表示每隔5分钟,运行一次curl获取网页,如果3s未返回,则认为命令运行失败,容器报告不健康。可以看到CMD执行的curl如果成功,则会正常返回,即报告健康,但如果curl失败,则会执行exit 1,即返回不健康。(这里假设重试次数为1)
其中,CMD执行的命令退出状态码可以为以下3个中的一个:

  • 0: success - 健康
  • 1: unhealthy - 不健康
  • 2: reserved - 系统保留,不建议用户使用

SHELL

SHELL指令用于覆盖其他指令以及容器中默认运行命令的shell程序,默认情况下Linux的shell为["/bin/sh", "-c"],Windows为["cmd", "/S", "/C"]

SHELL指令同样必须以JSON格式写在Dockerfile中

指定的shell将会影响RUNCMDENTRYPOINT的运行。

该指令在1.12中引入。

用法:

1
SHELL ["executable", "parameters"]

如:

1
SHELL ["/bin/bash", "-c"]

则将Linux容器的默认执行命令指定为/bin/bash

下面一下Win下的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM microsoft/windowsservercore

# Executed as cmd /S /C echo default
RUN echo default

# Executed as cmd /S /C powershell -command Write-Host default
RUN powershell -command Write-Host default

# Executed as powershell -command Write-Host hello
SHELL ["powershell", "-command"]
RUN Write-Host hello

# Executed as cmd /S /C echo hello
SHELL ["cmd", "/S"", "/C"]
RUN echo hello

Dockerfile最佳实践

官方给出的建议

  • 容器应该是短暂的,其中不应该保存数据,让容器更容易创建和销毁
  • 使用.dockerignore来排除不需要的文件,最好让新建镜像时都从一个空文件夹开始
  • 使用多阶段构建(multi-stage build),仅限Docker17.05之后的版本,即使用多个FROM指令,例如:
  • 避免安装不必要的包
  • 一个容器只干一件事,最好一个进程一个容器
  • 最小化镜像的层数,只有RUNCOPYADD创建层,其他指令只是创建临时的中间镜像,不会直接增加镜像大小
  • 对多行参数排序,特别是安装包的时候,可以有效避免重复包或者重复参数

对于多阶段构建的一个例子:

1
2
3
4
5
6
7
8
9
10
11
FROM golang:1.7.3 as builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]

对于参数排序的一例子,该例子基于debian配置一个带有很多依赖的镜像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
FROM buildpack-deps:jessie-scm

RUN set -ex; \
apt-get update; \
apt-get install -y --no-install-recommends \
autoconf \
automake \
bzip2 \
dpkg-dev \
file \
g++ \
gcc \
imagemagick \
libbz2-dev \
libc6-dev \
libcurl4-openssl-dev \
libdb-dev \
libevent-dev \
libffi-dev \
libgdbm-dev \
libgeoip-dev \
libglib2.0-dev \
libjpeg-dev \
libkrb5-dev \
liblzma-dev \
libmagickcore-dev \
libmagickwand-dev \
libncurses5-dev \
libncursesw5-dev \
libpng-dev \
libpq-dev \
libreadline-dev \
libsqlite3-dev \
libssl-dev \
libtool \
libwebp-dev \
libxml2-dev \
libxslt-dev \
libyaml-dev \
make \
patch \
xz-utils \
zlib1g-dev \
\
# https://lists.debian.org/debian-devel-announce/2016/09/msg00000.html
$( \
# if we use just "apt-cache show" here, it returns zero because "Can't select versions from package 'libmysqlclient-dev' as it is purely virtual", hence the pipe to grep
if apt-cache show 'default-libmysqlclient-dev' 2>/dev/null | grep -q '^Version:'; then \
echo 'default-libmysqlclient-dev'; \
else \
echo 'libmysqlclient-dev'; \
fi \
) \
; \
rm -rf /var/lib/apt/lists/*

命令的建议

FROM

最好使用官方源中的镜像做为基础镜像,推荐使用Alpine镜像,因为它只有5mb

LABEL

为了更好地组织镜像,最好使用容易理解的LABEL内容,并且值都用双引号(”),多个键值对应该放在同一行,如下:

1
2
3
4
5
6
# Set multiple labels at once, using line-continuation characters to break long lines
LABEL vendor=ACME\ Incorporated \
com.example.is-beta= \
com.example.is-production="" \
com.example.version="0.0.1-beta" \
com.example.release-date="2015-02-12"

RUN

为了提高可读性,应该使用续行符(\)将命令分成多行。并且避免运行如apt-get upgradedist-upgrade等命令来升级整个系统,记得删除临时文件。
如下:

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

因为Ubuntu和Debian官方镜像会自动运行apt-get clean,所以不需要再在命令中添加。

RUN中使用管道符要注意

由于Docker只关注最好一个命令执行是否正确,所以当管道前面错误时,后面依然可能成功,所以应该设置set -o pipefail &&来使执行过程中任何产生的错误都失败。
如下:

1
RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]

CMD

通过将CMD指令用于交互模式,如CMD ["python"]docker run -it python将会进入python的交互环境。除非你很了解CMD与ENTRYPOINT协同工作的方式,否则不要与ENTRYPOINT一起使用。

EXPOSE

指定容器的监听端口时,尽量使用通用的端口,如Apache web服务器的EXPOSE 80

ENV

该指令通常用于应用程序提供必要的环境变量和版本号等设置:

1
2
3
4
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

不需要保存压缩包的情况下尽量使用管道替代:

1
2
3
4
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

ENTRYPOINT

其与CMD一起使用时可以使镜像像命令一样运行:

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

运行命令:

1
docker run s3cmd

也可以覆盖其默认命令:

1
docker run s3cmd ls s3://mybucket

其也可以与脚本结合,让脚本成为其命令,组成更强大的功能。

VOLUME

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

USER

如果不需要root权限,可以通过USER切换成非root用户,在Dockerfile中如下方式创建:

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

尽量避免安装和使用 sudo,如果一定要使用类似 sudo 功能,可以使用 gosu 替代它,为了减少层数和复杂度,避免频繁使用 USER 进行用户切换。

WORKDIR

为了清楚可靠,应该使用绝对路径作为WORKDIR,而不是增加指令,如RUN cd ..

ONBUILD

ONBUILD 指令在当前 Dockerfile 构建完成后执行,存储到镜像 的manifest 清单中,我们可以通过 docker inspect 查看 OnBuild 的信息。

当我们使用带有 ONBUILD 触发器的镜像作为基础镜像来创建新镜像时,当 Dockerfile 执行到 FROM 时会自动查找 OnBuild 信息并执行这个触发器命令。成功后继续向下执行下一条指令,失败的话就停止向下执行并中止创建过程。如果成功创建了新的镜像后,这个新镜像中不会继承基础镜像中的 ONBUILD 触发器内容。

建立的带有 ONBUILD 的镜像时应该有一个单独的标签,例如:ruby:1.9-onbuild 或 ruby:2.0-onbuild。

当把 ADD 或 COPY 加入 ONBUILD 中时要小心,如果新创建镜像的上下文缺少这些要添加的资源时,会导致创建镜像失败。因而添加单独的标签可以帮助我们减小这种情况发生的可能, 让 Dockerfile 作者来做决定。

Dockerfile举例

通过VNC从容器中运行Firefox

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Firefox over VNC
#
# VERSION 0.3

FROM ubuntu

# Install vnc, xvfb in order to create a 'fake' display and firefox
RUN apt-get update && apt-get install -y x11vnc xvfb firefox
RUN mkdir ~/.vnc
# Setup a password
RUN x11vnc -storepasswd 1234 ~/.vnc/passwd
# Autostart firefox (might not be the best way, but it does the trick)
RUN bash -c 'echo "firefox" >> /.bashrc'

EXPOSE 5900
CMD ["x11vnc", "-forever", "-usepw", "-create"]

一次创建多个镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Multiple images example
#
# VERSION 0.1

FROM ubuntu
RUN echo foo > bar
# Will output something like ===> 907ad6c2736f

FROM ubuntu
RUN echo moo > oink
# Will output something like ===> 695d7793cbe4

# You'll now have two images, 907ad6c2736f with /bar, and 695d7793cbe4 with
# /oink.

参考

  1. Dockerfile reference
  2. Best practices for writing Dockerfiles
  3. 其他大量互联网资料