Dockerfile详解

什么是Dockerfile

Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。

镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。

Dockerfile 是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。有了 Dockerfile,当我们需要定制自己额外的需求时,只需在 Dockerfile 上添加或者修改指令,重新生成 image 即可,省去了敲命令的麻烦。

例:

1
docker build -f /path/to/a/Dockerfile

Dockerfile的基本结构

Dockerfile 一般分为四部分:基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令,’#’ 为 Dockerfile 中的注释。

四部分 指令
基础镜像信息 FROM
维护者信息 MAINTAINER
镜像操作指令 RUN、COPY、ADD、EXPOSE等
容器启动时执行指令 CMD、ENTRYPOINT

Dockerfile文件的第一条指令必须是FROM,其后可以是各种镜像的操作指令,最后是CMD或ENTRYPOINT指定容器启动时执行的命令。

下面引用yeasy/docker_practice对Dockerfile中各个指令的介绍,

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
指令
指令的一般格式为 INSTRUCTION arguments,指令包括 FROM、MAINTAINER、RUN 等。

FROM
格式为 FROM <image>或FROM <image>:<tag>。
第一条指令必须为 FROM 指令。并且,如果在同一个Dockerfile中创建多个镜像时,可以使用多个 FROM 指令(每个镜像一次)。

MAINTAINER
格式为 MAINTAINER <name>,指定维护者信息。

RUN
格式为 RUN <command> 或 RUN ["executable", "param1", "param2"]。
前者将在 shell 终端中运行命令,即 /bin/sh -c;后者则使用 exec 执行。指定使用其它终端可以通过第二种方式实现,例如 RUN ["/bin/bash", "-c", "echo hello"]。
每条 RUN 指令将在当前镜像基础上执行指定命令,并提交为新的镜像。当命令较长时可以使用 \ 来换行。

CMD
支持三种格式
CMD ["executable","param1","param2"] 使用 exec 执行,推荐方式;
CMD command param1 param2 在 /bin/sh 中执行,提供给需要交互的应用;
CMD ["param1","param2"] 提供给 ENTRYPOINT 的默认参数;
指定启动容器时执行的命令,每个 Dockerfile 只能有一条 CMD 命令。如果指定了多条命令,只有最后一条会被执行。
如果用户启动容器时候指定了运行的命令,则会覆盖掉 CMD 指定的命令。

EXPOSE
格式为 EXPOSE <port> [<port>...]。
告诉 Docker 服务端容器暴露的端口号,供互联系统使用。在启动容器时需要通过 -P,Docker 主机会自动分配一个端口转发到指定的端口。

ENV
格式为 ENV <key> <value>。 指定一个环境变量,会被后续 RUN 指令使用,并在容器运行时保持。
例如
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
格式为 ADD <src> <dest>。
该命令将复制指定的 <src> 到容器中的 <dest>。 其中 <src> 可以是Dockerfile所在目录的一个相对路径;也可以是一个 URL;还可以是一个 tar 文件(自动解压为目录)。

COPY
格式为 COPY <src> <dest>。
复制本地主机的 <src>(为 Dockerfile 所在目录的相对路径)到容器中的 <dest>。
当使用本地目录为源目录时,推荐使用 COPY。

ENTRYPOINT
两种格式:
ENTRYPOINT ["executable", "param1", "param2"]
ENTRYPOINT command param1 param2(shell中执行)。
配置容器启动后执行的命令,并且不可被 docker run 提供的参数覆盖。
每个 Dockerfile 中只能有一个 ENTRYPOINT,当指定多个时,只有最后一个起效。

VOLUME
格式为 VOLUME ["/data"]。
创建一个可以从本地主机或其他容器挂载的挂载点,一般用来存放数据库和需要保持的数据等。

USER
格式为 USER daemon。
指定运行容器时的用户名或 UID,后续的 RUN 也会使用指定用户。
当服务不需要管理员权限时,可以通过该命令指定运行用户。并且可以在之前创建所需要的用户,例如:RUN groupadd -r postgres && useradd -r -g postgres postgres。要临时获取管理员权限可以使用 gosu,而不推荐 sudo。

WORKDIR
格式为 WORKDIR /path/to/workdir。
为后续的 RUN、CMD、ENTRYPOINT 指令配置工作目录。
可以使用多个 WORKDIR 指令,后续命令如果参数是相对路径,则会基于之前命令指定的路径。例如
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
则最终路径为 /a/b/c。

ONBUILD
格式为 ONBUILD [INSTRUCTION]。
配置当所创建的镜像作为其它新创建镜像的基础镜像时,所执行的操作指令。
例如,Dockerfile 使用如下的内容创建了镜像 image-A。
[...]
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
[...]
如果基于 image-A 创建新的镜像时,新的Dockerfile中使用 FROM image-A指定基础镜像时,会自动执行 ONBUILD 指令内容,等价于在后面添加了两条指令。
FROM image-A #Automatically run the followingADD . /app/srcRUN /usr/local/bin/python-build --dir /app/src
使用 ONBUILD 指令的镜像,推荐在标签中注明,例如 ruby:1.9-onbuild。

Dockerfile文件说明

Docker以从上到下的顺序运行Dockerfile的指令。为了指定基本映像,第一条指令必须是FROM。一个声明以字符开头则被视为注释。可以在Docker文件中使用RUNCMDFROMEXPOSEENV等指令。

在这里列出了一些常用的指令。

FROM:指定基础镜像,必须为第一个命令

1
2
3
4
5
6
7
8
格式:
  FROM <image>
  FROM <image>:<tag>
  FROM <image>@<digest>
示例:
  FROM mysql:5.6
注:
  tag或digest是可选的,如果不使用这两个值时,会使用latest版本的基础镜像

MAINTAINER: 维护者信息

1
2
3
4
5
6
格式:
MAINTAINER <name>
示例:
MAINTAINER Jasper Xu
MAINTAINER sorex@163.com
MAINTAINER Jasper Xu <sorex@163.com>

RUN:构建镜像时执行的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
RUN用于在镜像容器中执行命令,其有以下两种命令执行方式:
shell执行
格式:
RUN <command>
exec执行
格式:
RUN ["executable", "param1", "param2"]
示例:
RUN ["executable", "param1", "param2"]
RUN apk update
RUN ["/etc/execfile", "arg1", "arg1"]
注:
  RUN指令创建的中间镜像会被缓存,并会在下次构建中使用。如果不想使用这些缓存镜像,可以在构建时指定--no-cache参数,如:docker build --no-cache

ADD:将本地文件添加到容器中,tar类型文件会自动解压(网络压缩资源不会被解压),可以访问网络资源,类似wget

1
2
3
4
5
6
7
8
格式:
ADD <src>... <dest>
ADD ["<src>",... "<dest>"] 用于支持包含空格的路径
示例:
ADD hom* /mydir/ # 添加所有以"hom"开头的文件
ADD hom?.txt /mydir/ # ? 替代一个单字符,例如:"home.txt"
ADD test relativeDir/ # 添加 "test" 到 `WORKDIR`/relativeDir/
ADD test /absoluteDir/ # 添加 "test" 到 /absoluteDir/

COPY:功能类似ADD,但是是不会自动解压文件,也不能访问网络资源

CMD:构建容器后调用,也就是在容器启动时才进行调用。

1
2
3
4
5
6
7
8
9
格式:
CMD ["executable","param1","param2"] (执行可执行文件,优先)
CMD ["param1","param2"] (设置了ENTRYPOINT,则直接调用ENTRYPOINT添加参数)
CMD command param1 param2 (执行shell内部命令)
示例:
CMD echo "This is a test." | wc -
CMD ["/usr/bin/wc","--help"]
注:
  CMD不同于RUN,CMD用于指定在容器启动时所要执行的命令,而RUN用于指定镜像构建时所要执行的命令。

ENTRYPOINT:配置容器,使其可执行化。配合CMD可省去”application”,只使用参数。

1
2
3
4
5
6
7
8
9
格式:
ENTRYPOINT ["executable", "param1", "param2"] (可执行文件, 优先)
ENTRYPOINT command param1 param2 (shell内部命令)
示例:
FROM ubuntu
ENTRYPOINT ["top", "-b"]
CMD ["-c"]
注:
   ENTRYPOINT与CMD非常类似,不同的是通过docker run执行的命令不会覆盖ENTRYPOINT,而docker run命令中指定的任何参数,都会被当做参数再次传递给ENTRYPOINT。Dockerfile中只允许有一个ENTRYPOINT命令,多指定时会覆盖前面的设置,而只执行最后的ENTRYPOINT指令。

LABEL:用于为镜像添加元数据

1
2
3
4
5
6
格式:
LABEL <key>=<value> <key>=<value> <key>=<value> ...
示例:
  LABEL version="1.0" description="这是一个Web服务器" by="IT笔录"
注:
  使用LABEL指定元数据时,一条LABEL指定可以指定一或多条元数据,指定多条元数据时不同元数据之间通过空格分隔。推荐将所有的元数据通过一条LABEL指令指定,以免生成过多的中间镜像。

ENV:设置环境变量

1
2
3
4
5
6
7
格式:
ENV <key> <value> #<key>之后的所有内容均会被视为其<value>的组成部分,因此,一次只能设置一个变量
ENV <key>=<value> ... #可以设置多个变量,每个变量为一个"<key>=<value>"的键值对,如果<key>中包含空格,可以使用\来进行转义,也可以通过""来进行标示;另外,反斜线也可以用于续行
示例:
ENV myName John Doe
ENV myDog Rex The Dog
ENV myCat=fluffy

EXPOSE:指定于外界交互的端口

1
2
3
4
5
6
7
8
格式:
EXPOSE <port> [<port>...]
示例:
EXPOSE 80 443
EXPOSE 8080
EXPOSE 11211/tcp 11211/udp
注:
  EXPOSE并不会让容器的端口访问到主机。要使其可访问,需要在docker run运行容器时通过-p来发布这些端口,或通过-P参数来发布EXPOSE导出的所有端口

VOLUME:用于指定持久化目录

1
2
3
4
5
6
7
格式:
VOLUME ["/path/to/dir"]
示例:
VOLUME ["/data"]
VOLUME ["/var/www", "/var/log/apache2", "/etc/apache2"]
注:
  一个卷可以存在于一个或多个容器的指定目录,该目录可以绕过联合文件系统,并具有以下功能:
1
2
3
4
5
1 卷可以容器间共享和重用
2 容器并不一定要和其它容器共享卷
3 修改卷后会立即生效
4 对卷的修改不会对镜像产生影响
5 卷会一直存在,直到没有任何容器在使用它

WORKDIR:工作目录,类似于cd命令

1
2
3
4
5
6
7
8
格式:
WORKDIR /path/to/workdir
示例:
WORKDIR /a (这时工作目录为/a)
WORKDIR b (这时工作目录为/a/b)
WORKDIR c (这时工作目录为/a/b/c)
注:
  通过WORKDIR设置工作目录后,Dockerfile中其后的命令RUN、CMD、ENTRYPOINT、ADD、COPY等命令都会在该目录下执行。在使用docker run运行容器时,可以通过-w参数覆盖构建时所设置的工作目录。

USER:指定运行容器时的用户名或 UID,后续的 RUN 也会使用指定用户。使用USER指定用户时,可以使用用户名、UID或GID,或是两者的组合。当服务不需要管理员权限时,可以通过该命令指定运行用户。并且可以在之前创建所需要的用户

1
2
3
4
5
6
7
8
9
10
11
格式:
  USER user
  USER user:group
  USER uid
  USER uid:gid
  USER user:gid
  USER uid:group
示例:
  USER www
注:
  使用USER指定用户后,Dockerfile中其后的命令RUN、CMD、ENTRYPOINT都将使用该用户。镜像构建完成后,通过docker run运行容器时,可以通过-u参数来覆盖所指定的用户。

ARG:用于指定传递给构建运行时的变量

1
2
3
4
5
格式:
ARG <name>[=<default value>]
示例:
ARG site
ARG build_user=www

ONBUILD:用于设置镜像触发器

1
2
3
4
5
6
7
格式:
  ONBUILD [INSTRUCTION]
示例:
  ONBUILD ADD . /app/src
  ONBUILD RUN /usr/local/bin/python-build --dir /app/src
注:
  当所构建的镜像被用做其它镜像的基础镜像,该镜像中的触发器将会被钥触发

以下是一个小例子:

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
# This my first nginx Dockerfile
# Version 1.0

# Base images 基础镜像
FROM centos

#MAINTAINER 维护者信息
MAINTAINER tianfeiyu

#ENV 设置环境变量
ENV PATH /usr/local/nginx/sbin:$PATH

#ADD 文件放在当前目录下,拷过去会自动解压
ADD nginx-1.8.0.tar.gz /usr/local/
ADD epel-release-latest-7.noarch.rpm /usr/local/

#RUN 执行以下命令
RUN rpm -ivh /usr/local/epel-release-latest-7.noarch.rpm
RUN yum install -y wget lftp gcc gcc-c++ make openssl-devel pcre-devel pcre && yum clean all
RUN useradd -s /sbin/nologin -M www

#WORKDIR 相当于cd
WORKDIR /usr/local/nginx-1.8.0

RUN ./configure --prefix=/usr/local/nginx --user=www --group=www --with-http_ssl_module --with-pcre && make && make install

RUN echo "daemon off;" >> /etc/nginx.conf

#EXPOSE 映射端口
EXPOSE 80

#CMD 运行以下命令
CMD ["nginx"]

最后用一张图解释常用指令的意义

image

常见问题

ADD VS COPY

从Dockerfile构建Docker镜像时,您可以选择两个指令将目录/文件添加到镜像:ADD和COPY。两条指令都遵循相同的基本形式,并完成了几乎相同的事情:

1
2
ADD <src>... <dest>
COPY <src>... <dest>

ADD

与COPY指令不同的是,ADD从一开始就是Docker的一部分,除了从构建上下文中复制文件之外,还支持一些其他技巧。

ADD指令允许您使用URL作为参数。提供URL时,将从URL下载文件并将其复制到。

1
2
ADD http://foo.com/bar.go /tmp/main.go
复制代码

上面的文件将从指定的URL下载并添加到容器的文件系统/tmp/main.go中。另一种形式可以让你简单地为下载的文件指定目标目录:

1
2
ADD http://foo.com/bar.go /tmp/
复制代码

由于参数以尾部”/“结尾,因此Docker会从URL中推断出文件名并将其添加到指定的目录中。在这种情况下,一个名为/tmp/bar.go的文件将被添加到容器的文件系统中。

ADD的另一个功能是能够自动解压缩压缩文件。如果参数是一个识别压缩格式(tar,gzip,bzip2等)的本地文件,那么它将被解压到容器文件系统中的指定处。

1
2
ADD /foo.tar.gz /tmp/
复制代码

上面的命令会导致foo.tar.gz归档文件的内容被解压到容器的/ tmp目录中。 有趣的是,URL下载和解压功能不能一起使用。任何通过URL复制的压缩文件都不会自动解压缩。

COPY

当版本1.0的Docker发布时,包含了新的COPY指令。与ADD不同的是,COPY直接将文件和文件夹从构建上下文复制到容器中。

COPY不支持URL作为参数,因此它不能用于从远程位置下载文件。任何想要复制到容器中的东西都必须存在于本地构建上下文中。

另外,COPY对压缩文件没有特别的处理。如果您复制归档文件,它将完全按照出现在构建上下文中的方式落入容器中,而不会尝试解压缩它。

COPY实际上只是ADD的精简版本,旨在满足大部分“复制文件到容器”的使用案例而没有任何副作用。

使用哪个?

如果现在还不明显,Docker团队的建议是在几乎所有情况下都使用COPY。

真的,使用ADD的唯一原因是当你有一个压缩文件,你一定想自动解压到镜像中。理想情况下,ADD将被重新命名为EXTRACT之类的内容,以真正将这一点引入Docker生态(同样,出于向后兼容的原因,这不太可能发生)。 好的,但是从远程URL获取软件包的方法不是仍然有用吗?技术上,是的,但在大多数情况下,您可能会更好地运行curl或wget。考虑下面的例子:

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

这里我们有一条ADD指令,它从一个URL中检索一个包,然后是一条RUN指令,它将它解包,构建它,然后尝试清理下载的存档。

不幸的是,由于软件包检索和rm命令在单独的镜像层中,我们实际上并没有在最终镜像中节省任何空间(有关此现象的更详细的解释,请参阅我的优化docker镜像文章)。

在这种情况下,你最好像下面这样做:

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

复制代码

这里我们使用curl命令下载压缩包,然后通过管道传递给tar命令解压。这样我们就不会在我们需要清理的文件系统上留下压缩文件。

将远程文件添加到镜像中可能仍有正当理由,但这应该是明确的决定,而不是您的默认选择。

最终,规则是这样的:使用COPY(除非你确定你需要ADD)。

CMD VS ENTRYPOINT

参考:https://www.cnblogs.com/rainwang/p/7274254.html

CMD指令和ENTRYPOINT指令的作用都是为镜像指定容器启动后的命令,那么它们两者之间有什么各自的优点呢?

为了更好地对比CMD指令和ENTRYPOINT指令的差异,我们这里再列一下这两个指令的说明,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CMD
支持三种格式
CMD ["executable","param1","param2"] 使用 exec 执行,推荐方式;
CMD command param1 param2 在 /bin/sh 中执行,提供给需要交互的应用;
CMD ["param1","param2"] 提供给 ENTRYPOINT 的默认参数;
指定启动容器时执行的命令,每个 Dockerfile 只能有一条 CMD 命令。如果指定了多条命令,只有最后一条会被执行。
如果用户启动容器时候指定了运行的命令,则会覆盖掉 CMD 指定的命令。

ENTRYPOINT
两种格式:
ENTRYPOINT ["executable", "param1", "param2"]
ENTRYPOINT command param1 param2(shell中执行)。
配置容器启动后执行的命令,并且不可被 docker run 提供的参数覆盖。
每个 Dockerfile 中只能有一个 ENTRYPOINT,当指定多个时,只有最后一个起效。

从上面的说明,我们可以看到有两个共同点:

  • 都可以指定shell或exec函数调用的方式执行命令
  • 当存在多个CMD指令或ENTRYPOINT指令时,只有最后一个生效

而它们有如下差异:

  • 差异1:CMD指令指定的容器启动时命令可以被docker run指定的命令覆盖,而ENTRYPOINT指令指定的命令不能被覆盖,而是将docker run指定的参数当做ENTRYPOINT指定命令的参数
  • 差异2:CMD指令可以为ENTRYPOINT指令设置默认参数,而且可以被docker run指定的参数覆盖

编写高效的Dockerfile

下面是一个常见企业门户网站架构,由一个Web Server和一个数据库组成,Web Server提供web服务,数据库保存用户数据。通常情况下,这样一个门户网站安装在一台服务器上。

image

如果把应用运行在一个Docker容器中,那么很可能写出下面这样的Dockerfile来。

1
2
3
4
5
6
7
8
9
10
11
12
13
FROM ubuntu

ADD . /app

RUN apt-get update
RUN apt-get upgrade -y
RUN apt-get install -y nodejs ssh mysql
RUN cd /app && npm install

# this should start three processes, mysql and ssh
# in the background and node app in foreground
# isn't it beautifully terrible? <3
CMD mysql & sshd & npm start

当然这样Dockerfile有很多问题,这里CMD命令是错误的,只是为了说明问题而写。

下面的内容中将展示对这个Dockerfile进行改造,说明如何写出更好的Dockerfile,共有如下几种处理方法。

一个容器只运行一个进程

从技术角度讲,Docker容器中可以运行多个进程,您可以将数据库,前端,后端,ssh等都运行在同一个Docker容器中。但是,这样跟未使用容器前没有太大区别,且这样容器的构建时间非常长(一处修改就要构建全部)、镜像体积大、横向扩展时非常浪费资源(不同的应用需要运行的容器数并不相同)。

通常所说的容器化改造是对应用整体微服务架构改造,再容器化,这样做可以带来如下好处。

  • 单独扩展:拆分为微服务后,可单独增加或缩减每个微服务的实例数量。
  • 提升开发速度:各微服务之间解耦,某个微服务的代码开发不影响其他微服务。
  • 通过隔离确保安全:整体应用中,若存在安全漏洞,会获得所有功能的权限。微服务架构中,若攻击了某个服务,只可获得该服务的访问权限,无法入侵其他服务。
  • 隔离崩溃:如果其中一个微服务崩溃,其它微服务还可以持续正常运行。

如前面的示例可以改造成下面架构,Web应用和MySQL运行在不同容器中。

image

MySQL运行在单独的镜像中,下面的示例中删掉了MySQL,只安装node.js。

1
2
3
4
5
6
7
8
9
10
11
FROM ubuntu

ADD . /app

RUN apt-get update
RUN apt-get upgrade -y

RUN apt-get install -y nodejs
RUN cd /app && npm install

CMD npm start

不要在构建中升级版本

为了降低复杂性、减少依赖、减小文件大小、节约构建时间,你应该避免安装任何不必要的包。例如,不要在数据库镜像中包含一个文本编辑器。

apt-get upgrade会使得镜像构建非常不确定,在构建时不确定哪些包会被安装,此时可能会产生不一致的镜像。

如果基础镜像中的某个包过时了,你应该联系它的维护者。如果你确定某个特定的包,比如 foo,需要升级,使用 apt-get install -y foo 就行,该指令会自动升级foo包。

删掉apt-get upgrade后,Dockerfile如下:

1
2
3
4
5
6
7
8
9
10
FROM ubuntu

ADD . /app

RUN apt-get update

RUN apt-get install -y nodejs
RUN cd /app && npm install

CMD npm start

将变化频率一样的RUN指令合一

Docker镜像是分层的,类似于洋葱,它们都有很多层,为了修改内层,则需要将外面的层都删掉。Docker镜像有如下特性:

  • Dockerfile中的每个指令都会创建一个新的镜像层。
  • 镜像层将被缓存和复用。
  • Dockerfile修改后,复制的文件变化了或者构建镜像时指定的变量不同了,对应的镜像层缓存就会失效。
  • 某一层的镜像缓存失效之后,它之后的镜像层缓存都会失效。
  • 镜像层是不可变的,如果我们再某一层中添加一个文件,然后在下一层中删除它,则镜像中依然会包含该文件,只是这个文件在Docker容器中不可见。

将变化频率一样的指令合并在一起,目的是为了更好的将镜像分层,避免带来不必要的成本。如本例中将node.js安装与npm模块安装放在一起的话,则每次修改源代码,都需要重新安装node.js,这显然不合适。

1
2
3
4
5
6
7
8
9
10
FROM ubuntu

ADD . /app

RUN apt-get update \
&& apt-get install -y nodejs \
&& cd /app \
&& npm install

CMD npm start

因此,正确的写法是这样的:

1
2
3
4
5
6
7
FROM ubuntu

RUN apt-get update && apt-get install -y nodejs
ADD . /app
RUN cd /app && npm install

CMD npm start

使用特定的标签

当镜像没有指定标签时,将默认使用latest 标签。因此, FROM ubuntu 指令等同于FROM ubuntu:latest。当镜像更新时,latest标签会指向不同的镜像,这时构建镜像有可能失败。

如下示例中使用16.04作为标签。

1
2
3
4
5
6
7
FROM ubuntu:16.04

RUN apt-get update && apt-get install -y nodejs
ADD . /app
RUN cd /app && npm install

CMD npm start

删除多余文件

假设我们更新了apt-get源,下载解压并安装了一些软件包,它们都保存在/var/lib/apt/lists/目录中。

但是,运行应用时Docker镜像中并不需要这些文件。因此最好将它们删除,因为它会使Docker镜像变大。

示例Dockerfile中,删除/var/lib/apt/lists/目录中的文件。

1
2
3
4
5
6
7
8
9
10
FROM ubuntu:16.04

RUN apt-get update \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*

ADD . /app
RUN cd /app && npm install

CMD npm start

选择合适的基础镜像

在示例中,我们选择了ubuntu作为基础镜像。但是我们只需要运行node程序,没有必要使用一个通用的基础镜像,node镜像应该是更好的选择。

更好的选择是alpine版本的node镜像。alpine是一个极小化的Linux发行版,只有4MB,这让它非常适合作为基础镜像。

1
2
3
4
5
6
FROM node:7-alpine

ADD . /app
RUN cd /app && npm install

CMD npm start

设置WORKDIR和 CMD

WORKDIR指令可以设置默认目录,也就是运行RUN / CMD / ENTRYPOINT指令的地方。

CMD指令可以设置容器创建时执行的默认命令。另外,您应该将命令写在一个数组中,数组中每个元素为命令的每个单词。

1
2
3
4
5
6
7
FROM node:7-alpine

WORKDIR /app
ADD . /app
RUN npm install

CMD ["npm", "start"]

使用ENTRYPOINT (可选)

ENTRYPOINT指令并不是必须的,因为它会增加复杂度。ENTRYPOINT是一个脚本,它会默认执行,并且将指定的命令作为其参数。它通常用于构建可执行的Docker镜像。

1
2
3
4
5
6
7
8
FROM node:7-alpine

WORKDIR /app
ADD . /app
RUN npm install

ENTRYPOINT ["./entrypoint.sh"]
CMD ["start"]

在entrypoint脚本中使用exec

在前文的entrypoint脚本中,我使用了exec命令运行node应用。不使用exec的话,我们则不能顺利地关闭容器,因为SIGTERM信号会被bash脚本进程吞没。exec命令启动的进程可以取代脚本进程,因此所有的信号都会正常工作。

优先使用COPY

COPY指令非常简单,仅用于将文件拷贝到镜像中。ADD相对来讲复杂一些,可以用于下载远程文件以及解压压缩包。

1
2
3
4
5
6
7
8
9
FROM node:7-alpine

WORKDIR /app

COPY . /app
RUN npm install

ENTRYPOINT ["./entrypoint.sh"]
CMD ["start"]

合理调整COPY与RUN的顺序

将变化最少的部分放在Dockerfile的前面,这样可以充分利用镜像缓存。

示例中,源代码会经常变化,则每次构建镜像时都需要重新安装NPM模块,这显然不是我们希望看到的。因此我们可以先拷贝package.json,然后安装NPM模块,最后才拷贝其余的源代码。这样的话,即使源代码变化,也不需要重新安装NPM模块。

1
2
3
4
5
6
7
8
9
10
FROM node:7-alpine

WORKDIR /app

COPY package.json /app
RUN npm install
COPY . /app

ENTRYPOINT ["./entrypoint.sh"]
CMD ["start"]

设置默认的环境变量,映射端口和数据卷

运行Docker容器时很可能需要一些环境变量。在Dockerfile设置默认的环境变量是一种很好的方式。另外,我们应该在Dockerfile中设置映射端口和数据卷。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
FROM node:7-alpine

ENV PROJECT_DIR=/app

WORKDIR $PROJECT_DIR

COPY package.json $PROJECT_DIR
RUN npm install
COPY . $PROJECT_DIR

ENTRYPOINT ["./entrypoint.sh"]
CMD ["start"]

ENV指令指定的环境变量在容器中可以使用。如果你只是需要指定构建镜像时的变量,你可以使用ARG指令。

使用EXPOSE暴露端口

EXPOSE指令用于指定容器将要监听的端口。因此,你应该为你的应用程序使用常见的端口。例如,提供Apache web服务的镜像应该使用EXPOSE 80,而提供MongoDB服务的镜像使用EXPOSE 27017。

对于外部访问,用户可以在执行 docker run 时使用一个标志来指示如何将指定的端口映射到所选择的端口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM node:7-alpine

ENV PROJECT_DIR=/app

WORKDIR $PROJECT_DIR

COPY package.json $PROJECT_DIR
RUN npm install
COPY . $PROJECT_DIR

ENV APP_PORT=3000
EXPOSE $APP_PORT

ENTRYPOINT ["./entrypoint.sh"]
CMD ["start"]

使用VOLUME管理数据卷

VOLUME 指令用于暴露任何数据库存储文件,配置文件,或容器创建的文件和目录。强烈建议使用 VOLUME来管理镜像中的可变部分和用户可以改变的部分。

下面示例中填写一个媒体目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FROM node:7-alpine

ENV PROJECT_DIR=/app

WORKDIR $PROJECT_DIR

COPY package.json $PROJECT_DIR
RUN npm install
COPY . $PROJECT_DIR

ENV MEDIA_DIR=/media \
APP_PORT=3000

VOLUME $MEDIA_DIR
EXPOSE $APP_PORT

ENTRYPOINT ["./entrypoint.sh"]
CMD ["start"]

使用LABEL设置镜像元数据

你可以给镜像添加标签来帮助组织镜像、记录许可信息、辅助自动化构建等。每个标签一行,由 LABEL 开头加上一个或多个标签对。

如果你的字符串中包含空格,必须将字符串放入引号中或者对空格使用转义。如果字符串内容本身就包含引号,必须对引号使用转义。

1
2
FROM node:7-alpine  
LABEL com.example.version="0.0.1-beta"

添加HEALTHCHECK

运行容器时,可以指定–restart always选项。这样的话,容器崩溃时,docker daemon会重启容器。对于需要长时间运行的容器,这个选项非常有用。但是,如果容器的确在运行,但是不可用怎么办?使用HEALTHCHECK指令可以让Docker周期性的检查容器的健康状况。我们只需要指定一个命令,如果一切正常的话返回0,否则返回1。当请求失败时,curl –fail 命令返回非0状态。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FROM node:7-alpine  
LABEL com.example.version="0.0.1-beta"

ENV PROJECT_DIR=/app
WORKDIR $PROJECT_DIR

COPY package.json $PROJECT_DIR
RUN npm install
COPY . $PROJECT_DIR

ENV MEDIA_DIR=/media \
APP_PORT=3000

VOLUME $MEDIA_DIR
EXPOSE $APP_PORT
HEALTHCHECK CMD curl --fail http://localhost:$APP_PORT || exit 1

ENTRYPOINT ["./entrypoint.sh"]
CMD ["start"]

编写.dockerignore文件

.dockerignore的作用和语法类似于.gitignore,可以忽略一些不需要的文件,这样可以有效加快镜像构建时间,同时减少Docker镜像的大小。

构建镜像时,Docker需要先准备context ,将所有需要的文件收集到进程中。默认的context包含Dockerfile目录中的所有文件,但是实际上,我们并不需要.git目录等内容。

示例如下:

1
.git/
--------------------本文结束,感谢您的阅读--------------------

本文标题:Dockerfile详解

文章作者:弓昭

发布时间:2019年08月05日 - 22:24

最后更新:2020年04月08日 - 22:20

原始链接:https://gongzhao1.gitee.io/Dockerfile详解/

联系邮箱:gongzhao1@foxmail.com