简明教程 | Docker篇 · 其二:Dockerfile的编写

Dockerfile是什么

一个包含用于组合 image 的命令的文本文件,docker 通过 dockerfile 和构建环境的上下文来构建 image 。


编写Dockerfile

FROM

首先,我们必须用 FROM 指定一个基础image,然后后续的指令会运行在该image上

FROM [--platform=<platform>] <image>[:<tag>] [AS <名称>]
FROM [--platform=<platform>] <image>[:@<digest>] [AS <名称>]

示例:

FROM redis:5.0.12

LABEL

然后让我们添加维护者的基本信息

MAINTAINER <name>

不过要注意的是该命令已经被标记为deprecated,所以我们最好用 LABEl 代替它

LABEL maintainer="lihua@163.com"

同时,LABEL 指令还可以设置任何的元数据,就像这样

LABEL version="1.0"

当我们对 build 后的 image 使用 docker inspect 命令时会就能看见该 LABEl


WORKDIR

好的,现在我们已经编写完了基本的指令,接下来来编写执行指令。

在我们正式工作之前,先来设置工作目录。

设置一个工作目录只需要

WORKDIR /mydata

然后我们下面的大多数命令都会在这个目录下运行。

当然,如果它不存在则会自动创建的。


RUN

RUN 指令应该是最重要的指令之一,它可以在容器内执行指定的指令,并把结果保存下来,一条 RUN 指令应该长这样:

RUN <command>			# 这是shell格式
RUN ["exec", "arg1", "arg2"]	# 这是exec格式
Exec 和 Shell 格式

shell 格式需要一个字符串,其会传给 "/bin/sh -c" 执行

exec 格式需要接收一个JSON数组,其中第一个元素是可执行文件,其他元素为执行时用到的参数

和shell对比,exec格式适用于需要规避shell对字符串作出错误解析的情况,或当基础镜像里没有 "/bin/sh" 时

你可以这样使用它,以下两条命令是等价的:

RUN bash -c 'touch /hello.txt'
RUN ["bash", "-c", "touch", "/hello.txt"]

ENV、ARG

如果你想设置环境变量,那么你将用到 ENV 指令,就像这样:

ENV <key>=<value> ...
ENV name=lihua local=cn

当你想使用它时只需要 ${<key>}

ENV my_version=1.0
RUN apt-get install -y mypackage=${my_version}

不过,exec格式下不会调用命令shell,所以变量替换不会生效。

RUN ["echo", "$name"]

要使其生效可以使用

RUN ["sh", "echo $name"]

你还可以使用标准的bash修饰符

${name:-default} - name未设置则为"default"

${name:+lihua} - name设置了则为"lihua",否则为空字符串

需要注意的是,该环境变量会保留到容器中,如果只是想在构建中使用变量,可以使用 ARG 指令,ARG 的用法和 ENV 十分接近


ADD、COPY

然后我们还需要一个能够将本地文件添加到容器中的指令,ADD 指令,像这样

ADD <src> ... <dest>
ADD ["<src>", ..., "<dest>"]

该指令将当前上下文中的 <src> 添加 image 里的 <dest> 文件或目录中,<src> 可以是文件系统,也可以网络,同时如果是压缩文件会自动解压。并且,你还可以用 ?* 匹配一个或多个字符

ADD ./system.jar /app.jar 

特别要注意的是,我们不能指定上下文以外的 src 路径,例如 ../book/b.txt ,这是不被允许的

COPY 指令是功能与 ADD 相近的指令,区别在于,COPY 不会访问网络资源且不会解压文件


ENTRYPOINT、CMD

通常我们可以用 ENTRYPOINT 来设置一个可执行文件在容器启动后运行,同样的,你可以使用 shell 格式或 exec 格式。不过如果定义了多个 ENTRYPOINT ,那只有最后一个会生效。

ENTRYPOINT ["java", "-jar", "/service.jar"]

CMD 指令和 ENTRYPOINT 相似,也是在容器启动时执行指定的指令,不过如果还同时出现了 ENTRYPOINT 指令,那 CMD 将作为 ENTRYPOINT 默认参数的形式在容器中执行。

同时如果 CMD 只是被用来为 ENTRYPOINT 提供参数,则可以使用 ["param1","param2"] 的格式。

并且,CMD 的参数会被 docker run 的参数覆盖,而 ENTRYPOINT 不会。

关于 CMDENTRYPOINT 的组合示例可以看看这张图。

从图中我们可以看出,ENTRYPOINT 如果以 shell 的形式,则它会忽略所有的 CMD 参数和 docker run 的参数,而且会运行在 sh -c 内。

这就代表进程的 pid 不是 1 ,并且无法接收 UNIX 信号,也就是说接收不到 docker stopSIGTERM 信号,这样的话最后会被强制 kill 掉。


VOLUME

我们当然可以在 Dockerfile 中设置数据卷,可以使用以下两种方式

VOLUME ["/data"]
VOLUME /data

不过,主机的目录只能在使用 docker run 的时候申明,这是为了保持 image 的可移植性。


USER

这里有两种方法可以指定所使用的用户

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

EXPOSE

EXPOSE 能通知 Docker 在运行时监听指定的端口,但它不会实际的发布端口,只是一个文档信息,并且你可以在 docker run 时带上 -P 标签,这个标签能将 EXPOSE 通知的端口全部发布出去,不过会使用随机的端口。

EXPOSE 80/tcp

ONBUILD

这个命令可以让子镜像在构建时执行这个命令里的命令。

例如,我们在父镜像的 Dockerfile 的文件中编写

FROM grandfather
ONBUILD echo "hey! my child"

然后将该镜像构建好后,再构建子镜像

FROM father

构建子镜像的时候,就会打印出 hey! my child


build

在我们编写完 Dockerfile 文件后,就可以开始构建了。

我们使用 docker build 进行构建

docker build \
# -f 用来指定我们在本次build时需要用到的Dockerfile的位置,不指定则为当前目录
-f /path/to/a/Dockerfile \
# -t 用来指定build后的存储库的<名称>:<标记>
-t myApplication:latest \
# 此处的上下文为".",即当前目录,该目录下的所有文件会被放入一个tar文件(除非你编写了dockerignore文件)
.

当然也可以直接使用 docker build ,此时会在以当前目录为上下文,并在当前上下文中寻找 Dockerfile 文件。


Dockerfile 的 ADDCOPYRUN 指令会生成新的镜像层,下一个指令会用这个新的镜像层执行指令后,再保存为一个新的镜像层,提供给下一个指令使用。

这就代表即使用 RUN 指令运行了一些持久化的进程,在启动容器的时候也会消失。不过要想启动容器时同时运行一个进程,可以使用 ENTERPOINTCMD

我们甚至可以使用 docker history 来查看组成 image 的所有层,当构建失败的时候,我们可以将中间层启动起来,就如同下面这个例子一样。


我们先编写一个 Dockerfile

FROM ubuntu:20.04
RUN no_cmd
ENTRYPOINT ["echo","world"]

然后使用 docker build

[root@VM-0-3-centos docker]# docker build -t test:1 .
Sending build context to Docker daemon  4.608kB
Step 1/3 : FROM ubuntu:20.04
 ---> 26b77e58432b
Step 2/3 : RUN no_cmd
 ---> Running in 6e1c94e4ccf6
/bin/sh: 1: no_cmd: not found
The command '/bin/sh -c no_cmd' returned a non-zero code: 127

毫无疑问,它出错了。虽然这次演示的错误很简单,我们可以直接看出来。

然后我们使用使用 docker run 进入中间的层

[root@VM-0-3-centos docker]# docker run -it 26b77e58432b
root@59c3c3641111:/# no_cmd
bash: no_cmd: command not found

通过演示我们发现,原因当然就是这个不存在的命令。


最后,在你的 image 构建成功后,你就可以像一般的 image 来任意使用你构建的 image 了~

posted @ 2021-05-02 18:57  en_oc  阅读(217)  评论(0编辑  收藏  举报