一、Dockerfile简介

Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。

1、镜像构建步骤

  • 编写一个 dockerfile 文件

  • docker build 构建成为一个镜像

  • docker run 运行镜像

  • docker push 发布镜像(咱们可以发布到 DockerHub,也可以发布到阿里云上面)

2、镜像结构图

通过如下架构图可以看出通过 DockerFile 可以直接构建镜像:

dockerfile架构图

3、 Dockerfile 解析过程:

dockerfile构建

3、Dockerfile 基本结构

image-20220811170454773

二、Dockerfile 指令

1、Dockerfile 指令结构图

image-20220817150613423

2、Dockerfile 指令表

命令 运行阶段 描述
FROM BUILD 指定基础镜像,必须为第一个命令。
MAINTAINER BUILD 构建指令,用于将镜像制作者相关的信息写入到镜像中。
RUN BUILD 用于在镜像容器中执行命令。
COPY BUILD 拷贝文件,不会自动解压文件,也不能访问网络资源。
ADD BUILD 将本地文件添加到容器中,tar类型文件会自动解压(网络压缩资源不会被解压),可以访问网络资源,类似wget。
LABEL BUILD 用于为镜像添加元数据
ARG BUILD 用于指定传递给构建运行时的变量(给dockerfile传参),相当于构建镜像时可以在外部为里面传参
ONBUILD BUILD 用于设置镜像触发器
CMD RUN 指定启动容器时执行的命令,每个 Dockerfile 只能有一条 CMD 命令。
EXPOSE RUN 将容器中的端口映射成宿主机器中的某个端口。
ENTRYPOINT RUN 类似于CMD指令,配置容器启动后执行的命令(容器的初始化命令)。
ENV RUN 设置环境变量。
WORKDIR BOTH bulid以及run阶段大家工作的场所(CMD以及RUN执行的工作目录)。
USER BOTH 指定运行容器时的用户名或UID,默认是root,后续的RUN也会使用指定用户。
VOLUME RUN 运行阶段容器为了持久化存储以正常的文件或者目录的形式挂载于宿主机上(持久化存储,容器挂了数据还在,用户存储有状态的数据)。

3、DockerFile 注意事项

  • 每个 Dockerfile 的保留字(指令),都必须是大写的
  • DockerFile 脚本执行是按照顺序执行的
  • # 表示注释
  • 每一个指令都会创建提交一个新的镜像层,并提交

4、Dockerfile 命令介绍

(1)FROM

FROM:指定基础镜像,并且必须是第一条指令。如果不以任何镜像为基础,那么写法为:FROM scratch 。同时意味着接下来所写的指令将作为镜像的第一层开始。

语法:

1
2
3
4
5
6
7
8
9
# 格式:
  FROM <image>
  FROM <image>:<tag>
  FROM <image>@<digest>

# 示例:
  FROM mysql:5.6

# 注:tag或digest是可选的,如果不使用这两个值时,会使用 latest 版本的基础镜像

(2)MAINTAINER

MAINTAINER:维护者信息

语法:

1
2
3
4
5
6
格式:
MAINTAINER <name>
示例:
MAINTAINER bertwu
MAINTAINER xxx@163.com
MAINTAINER bertwu <xxx@163.com>

(3)RUN

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

RUN命令有两种格式,一种是 shell 格式,还有一种为 exec 格式:

shell 语法如下:

1
2
3
4
5
RUN <command>
# 注:<command> 相当于终端操作的 shell 命令。

# 示例:
RUN yum -y install vim

exec 语法如下:

1
2
3
4
RUN ["executable", "param1", "param2"]

# 示例:
RUN ["/etc/execfile", "arg1", "arg1"] 等价于 RUN /etc/execfile arg1 arg2

注:RUN指令创建的中间镜像会被缓存,并会在下次构建中使用。如果不想使用这些缓存镜像,可以在构建时指定–no-cache参数,如:docker build --no-cache。

(4)ADD

ADD:将本地文件添加到容器中。ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。

1
2
3
4
5
6
7
8
9
# 格式:
ADD [--chown=<user>:<group>] <源路径>... <目标路径>
ADD [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]

# 示例:
ADD hom* /mydir/ # 添加所有以"hom"开头的文件
ADD hom?.txt /mydir/ # ? 替代一个单字符,例如:"home.txt"
ADD test relativeDir/ # 添加 "test" 到 `WORKDIR`/relativeDir/
ADD test /absoluteDir/ # 添加 "test" 到 /absoluteDir/

如果 <源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。

在某些情况下,这个自动解压缩的功能非常有用,比如官方镜像 ubuntu 中:

1
2
3
4
# 示例
FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
...

但在某些情况下,如果我们真的是希望复制个压缩文件进去,而不解压缩,这时就不可以使用 ADD 命令了。

因此在 COPYADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD

在使用该指令的时候还可以加上 --chown=<user>:<group> 选项来改变文件的所属用户及所属组。

1
2
3
4
5
# 示例:
ADD --chown=55:mygroup files* /mydir/
ADD --chown=bin files* /mydir/
ADD --chown=1 files* /mydir/
ADD --chown=10:11 files* /mydir/

(5)COPY

COPY:指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置。功能类似 ADD,也有两种格式,一种类似于命令行,一种类似于函数调用。不会自动解压文件,也不能访问网络资源。

1
2
3
# 格式:
COPY [--chown=<user>:<group>] <源路径>... <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]

<源路径> 可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match (opens new window)规则,如:

1
2
3
# 示例:
COPY hom* /mydir/
COPY hom?.txt /mydir/

<目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。

在使用该指令的时候还可以加上 --chown=<user>:<group> 选项来改变文件的所属用户及所属组。

1
2
3
4
5
# 示例
COPY --chown=55:mygroup files* /mydir/
COPY --chown=bin files* /mydir/
COPY --chown=1 files* /mydir/
COPY --chown=10:11 files* /mydir/

注意:

  • COPY 不会自动解压文件,也不能访问网络资源。
  • 如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径。

(6)LABEL

LABEL:用于为镜像添加元数据,LABEL 是键值对。要在 LABEL 值中包含空格,请像在命令行中一样使用引号和反斜杠。一些用法示例:

1
2
3
4
5
6
7
8
9
# 格式:
LABEL <key>=<value> <key>=<value> <key>=<value> ...

# 示例:
  LABEL version="1.0" description="这是一个Web服务器" by="IT笔录"

# 注:
  使用LABEL指定元数据时,一条LABEL指定可以指定一或多条元数据,指定多条元数据时不同元数据
  之间通过空格分隔。推荐将所有的元数据通过一条LABEL指令指定,以免生成过多的中间镜像。

(7)ARG

ARG:用于指定传递给构建运行时的变量(给dockerfile传参),相当于构建镜像时可以在外部为里面传参。

1
2
3
4
5
6
#格式:
ARG <name>[=<default value>]

# 示例:
ARG site
ARG build_user=www

(8)ONBUILD

OUTBUILD:用于设置镜像触发器,是一个特殊的指令,它后面跟的是其它指令,比如 RUN, COPY 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。

1
2
3
4
5
6
# 格式: 
ONBUILD [INSTRUCTION]

# 示例:
  ONBUILD ADD . /app/src
  ONBUILD RUN /usr/local/bin/python-build --dir /app/src

(9)CMD

CMD:为启动的容器指定默认要运行的程序,程序运行结束,容器也就结束。CMD 指令指定的程序可被 docker run 命令行参数中指定要运行的程序所覆盖。

类似于 RUN 指令,用于运行程序,但二者运行的时间点不同:

  • CMD 在docker run 时运行。
  • RUN 是在 docker build。

注意:如果 Dockerfile 中如果存在多个 CMD 指令,仅最后一个生效。

1
2
3
4
5
6
7
8
# 格式:
CMD ["executable","param1","param2"] (执行可执行文件,优先)
CMD ["param1","param2"] (设置了ENTRYPOINT,则直接调用ENTRYPOINT添加参数)
CMD command param1 param2 (执行shell内部命令)

# 示例:
CMD echo "This is a test." | wc -l
CMD ["/usr/bin/wc","--help"]

运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,ubuntu 镜像默认的 CMD/bin/bash,如果我们直接 docker run -it ubuntu 的话,会直接进入 bash。我们也可以在运行时指定运行别的命令,如 docker run -it ubuntu cat /etc/os-release。这就是用 cat /etc/os-release 命令替换了默认的 /bin/bash 命令了,输出了系统版本信息。

提到 CMD 就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。

Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用 systemd 去启动后台服务,容器内没有后台服务的概念。

一些初学者将 CMD 写为:

1
CMD service nginx start

然后发现容器执行后就立即退出了。甚至在容器内去使用 systemctl 命令结果却发现根本执行不了。这就是因为没有搞明白前台、后台的概念,没有区分容器和虚拟机的差异,依旧在以传统虚拟机的角度去理解容器。

对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。

而使用 service nginx start 命令,则是希望 upstart 来以后台守护进程形式启动 nginx 服务。而刚才说了 CMD service nginx start 会被理解为 CMD [ "sh", "-c", "service nginx start"],因此主进程实际上是 sh。那么当 service nginx start 命令结束后,sh 也就结束了,sh 作为主进程退出了,自然就会令容器退出。

正确的做法是直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:

1
CMD ["nginx", "-g", "daemon off;"]

(10)EXPOSE

EXPOSE:用于为容器暴露端口到外部,用于实现通讯,类似于docker run 的 -p 选项。语法如下:

1
2
3
4
5
6
7
# 格式:
EXPOSE <port> [<port>...]

# 示例:
EXPOSE 80 443
EXPOSE 8080
EXPOSE 11211/tcp 11211/udp

不管 EXPOSE 设置是什么,都可以通过使用 -p 标志在运行时覆盖它们。例如:

1
2
示例:
docker run -p 80:80/tcp -p 80:80/udp ...

(11)ENTRYPOINT

ENTRYPOINT 指定镜像的默认入口命令,该入口命令会在启动容器时作为根命令执行,所有其他传入值作为该命令的参数。

ENTRYPOINT 的值可以通过 docker run --entrypoint 来覆盖掉。

ENTRYPOINT 有两种语法格式,如下所示:

1
2
3
4
5
# exec 格式
ENTRYPOINT ["executable", "param1", "param2"]

# shell 格式
ENTRYPOINT command param1 param2

示例:

1
2
3
4
5
# exec 格式
ENTRYPOINT ["echo", "hello docker"]

# shell 格式
ENTRYPOINT echo "hello docker"

注意:

  • ENTRYPOINT 与 CMD 非常类似,不同的是通过 docker run 执行的命令不会覆盖 ENTRYPOINT。
  • Dockerfile 中只允许有一个 ENTRYPOINT 命令,多指定时会覆盖前面的设置,而只执行最后的 ENTRYPOINT 指令。
    通常情况下, ENTRYPOINT 与 CMD 一起使用,ENTRYPOINT 写默认命令,当需要参数时候使用 CMD 传参。

(12)ENV

ENV:用来在镜像构建过程中设置环境变量,后续的 RUN 可以使用它所创建的环境变量。当创建基于该镜像的 container 的时候,会自动拥有设置的环境变量。

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

示例:
ENV myName John Doe
ENV myDog Rex The Dog
ENV myCat=fluffy

(13)WORKDIR

WORKDIR:用于指定容器的一个目录, 相当于设置容器的工作目录。以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。

1
2
3
4
5
6
7
# 格式:
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 参数覆盖构建时所设置的工作目录。

(14)USER

USER:指定运行容器时的用户名或 UID,后续的 RUN 也会使用指定用户。这个用户必须是事先建立好的,否则无法切换。使用 USER 指定用户时,可以使用用户名、UID 或 GID,或是两者的组合。

1
2
3
4
5
6
7
8
9
10
# 格式:  
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 参数来覆盖所指定的用户。

(15)VOLUME

VOLUME:用于指定持久化目录(指定此目录可以被挂载出去)。

1
2
3
4
5
6
# 格式
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>

# 示例:
VOLUME /data

容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。

上面示例的 /data 目录就会在容器运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行容器时可以覆盖这个挂载设置。比如:`

1
docker run -d -v mydata:/data xxxx

在这行命令中,就使用了 mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。

三、实战 Dockerfile

1、实战介绍

基础镜像为 CentOS 7 ,本次实战模拟在 CentOS 7 的基础上安装 vimifconfigjava 环境。

java环境下载地址:Index of /jdk/ (yangxingzhen.com)

2、Dockerfile 文件配置

在指定路径下 vim 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
# 指定基础镜像
FROM centos:centos7

# 维护者信息
MAINTAINER whbblog<whbblog.cn>

# 安装 VIM 编辑器
RUN yum -y install vim

# 安装 ifconfig 命令
RUN yum -y install net-tools

# 安装 java8 及 lib 库
RUN yum -y install glibc.i686
RUN mkdir /usr/local/java

# ADD 是把 jdk-8u192-linux-x64.tar.gz 添加到容器中,安装包必须要和 Dockerfile 文件在同一位置
ADD jdk-8u192-linux-x64.tar.gz /usr/local/java/

# 配置java环境变量
ENV JAVA_HOME /usr/local/java/jdk1.8.0_192
ENV JRE_HOME $JAVA_HOME/jre
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib:$CLASSPATH
ENV PATH $JAVA_HOME/bin:$PATH

# 暴漏端口
EXPOSE 80

# 配置容器启动执行命令
CMD echo $MYPATH
CMD echo "success--------------ok"
CMD /bin/bash

3、镜像构建

执行以下命令进行镜像构建。

1
2
3
4
5
docker build -t centosjava .

# 镜像构建成功输入如下:
Successfully built cd6fa33c8f3e
Successfully tagged centosjava:latest

4、镜像测试

启动容器,验证构建的镜像,是否满足要求。

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
# 启动容器
docker run -it centosjava /bin/bash

# 查看 java 版本
[root@5fe7e4724010 /]# java -version
java version "1.8.0_192"
Java(TM) SE Runtime Environment (build 1.8.0_192-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.192-b12, mixed mode)

# 验证 vim 命令
[root@5fe7e4724010 /]# vim a.txt

# 验证 ifconfig 命令
[root@5fe7e4724010 /]# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.2 netmask 255.255.0.0 broadcast 172.17.255.255
ether 02:42:ac:11:00:02 txqueuelen 0 (Ethernet)
RX packets 8 bytes 656 (656.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1000 (Local Loopback)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

四、参考文献

1️⃣Dockerfile的使用及使用

2️⃣初识Docker&&Dockerfile快速入门知识点整理

3️⃣Dockerfile 详解

4️⃣Dockerfile 指令详解 | Docker 从入门到实践 (docker-practice.com)