简明教程 | 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
不会。
关于 CMD
和 ENTRYPOINT
的组合示例可以看看这张图。
从图中我们可以看出,ENTRYPOINT
如果以 shell
的形式,则它会忽略所有的 CMD
参数和 docker run
的参数,而且会运行在 sh -c
内。
这就代表进程的 pid
不是 1 ,并且无法接收 UNIX
信号,也就是说接收不到 docker stop
的 SIGTERM
信号,这样的话最后会被强制 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 的 ADD
、COPY
和 RUN
指令会生成新的镜像层,下一个指令会用这个新的镜像层执行指令后,再保存为一个新的镜像层,提供给下一个指令使用。
这就代表即使用 RUN
指令运行了一些持久化的进程,在启动容器的时候也会消失。不过要想启动容器时同时运行一个进程,可以使用 ENTERPOINT
或 CMD
。
我们甚至可以使用 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
了~