制作自己的Docker镜像主要有如下两种方式:
- 运行一个已有的镜像,启动镜像后,进入容器,进行相关的操作(安装、配置等)然后将容器保存为一个新的镜像
- 构建一个DockerFile(记录了镜像创建步骤),然后使用docker build创建一个新的镜像。
本文主要介绍通过DockerFile进行镜像的创建,commit的创建可以参考另外一篇文章。
Dockerfile是一个文本文档,其中包含用户可以在命令行上运行调用以生成镜像的所有命令。获得 Dockerfile 文件后,我们可以使用 docker build 来完成镜像的创建。
要创建一个 Dockerfile 文件,我们首先介绍一下 DockerFile 文件支持的指令和文件格式。
获得一个DokcerFile
通过已知的镜像获取DockerFile
dfimage 是一款第三方工具,可用来从镜像中提取 Dockerfile1
2alias dfimage="docker run -v /var/run/docker.sock:/var/run/docker.sock --rm alpine/dfimage"
dfimage -sV=1.36 test:v1.0
如果我们有一个基本满足我们需求的镜像,但是可能这个镜像相对我们有一些冗余的功能,我们可以通过已知的镜像生成一个DockerFile进行微调。当然也可以通过这种方式了解一些镜像的构建过程,相比于 docker history 使用dfimage得到的镜像构建信息会更全面。
从头写一个 DockerFile
要自己写 DockerFile, 我们先来简单的了解一下DockerFile的语法。
Dockerfile 中使用#来进行注释,同时Dockerfile支持多种不同的命令分别用以指定初始镜像(FROM)、工作目录(WORKDIR)、运行数据挂载和网络设置(RUN)、命令行执行(CMD)、设置环境变量(ENV)、添加文件到镜像文件系统中(ADD)、添加文件到容器文件系统中(COPY)、创建挂载点(VOLUME)、设置用户(USER)、变量(ARG)、镜像添加元数据(LABEL)、监听网络(EXPOSE)、入口点(ENTRYPOINT)等等。
现在为大家介绍一些镜像构建过程中会用到的基本命令,一些指令在现在的个人进行镜像构建的过程中并未用到,暂时不进行扩展,大家有需要的时候可以参考官方文档DockerFile reference
DockerFile 指令说明
| DockerFile指令 | 作用 | 示例 |
|---|---|---|
| FROM | 初始基础镜像,后续所有操作都是以基础镜像为初始环境 | FROM ubuntu:14.04 |
| RUN | 用于执行后面跟着的命令行命令。有以下俩种格式。 | RUN <命令行命令> |
| COPY | 复制指令,从上下文目录中复制文件或者目录到容器里指定路径。 | COPY [--chown=<user>:<group>] <源路径1>... <目标路径> |
| ADD | 添加本地或远程文件和目录。 | |
| ARG | 使用构建时变量。 | |
| CMD | 指定默认命令。 | |
| ENTRYPOINT | 指定默认可执行文件。 | |
| ENV | 设置环境变量。 | |
| EXPOSE | 描述您的应用程序正在侦听哪些端口。 | |
| HEALTHCHECK | 在启动时检查容器的运行状况。 | |
| LABEL | 将元数据添加到镜像。 | |
| MAINTAINER | 指定图像的作者。 | |
| ONBUILD | 指定在构建中使用映像时的说明。 | |
| SHELL | 设置图像的默认外壳。 | |
| STOPSIGNAL | 指定退出容器的系统调用信号。 | |
| USER | 设置用户和组 ID。 | |
| VOLUME | 创建卷挂载。 | |
| WORKDIR | 更改工作目录。 |
解析器指令
顾名思义,解析器指令和之前列的这些镜像操作指令不一样,解析器指令是用来配置DockerFile解释器如何解析文件的,
会影响 Dockerfile 中后续行的处理方式。解释器指令不是必须的,只有需要对解析逻辑进行调整时,需要配置。解析器指令不会向构建添加层,也不会显示为构建步骤。解析器指令以 # directive=value 形式编写为特殊类型的注释。单个指令只能使用一次。
解析器指令位于 Dockerfile 的顶部,否则都会视为注释。
1 | # 指定DockerFile的版本 |
ENV
环境变量(使用 ENV 语句声明)也可以在某些指令中用作由 Dockerfile 解释的变量。还可以处理转义以将类似变量的语法按字面意思包含到语句中。
1 |
|
word 可以是任何字符串,包括其他环境变量。1
2
3
4
5
6# 示例(解析后的表示显示在 # 之后):
FROM busybox
ENV FOO=/bar
WORKDIR ${FOO} # WORKDIR /bar
ADD . $FOO # ADD . /bar
COPY \$FOO /quux # COPY $FOO /quux
在 ADD、COPY、ENV、EXPOSE、FROM、LABEL、STOPSIGNAL、USER、VOLUME、WORKDIR中都支持使用环境变量。
** 环境变量替换在整个指令中对每个变量使用相同的值。更改变量的值仅在后续指令中生效。
From
1 | # 三种语法示例 |
FROM 初始化一个新的构建阶段并为后续指令设置基础镜像。因此,有效的 Dockerfile 必须以 FROM 开头。该镜像可以是任何有效的镜像(尽可能选择满足需求的最小镜像)。
- ARG是唯一可以位于FROM之前的指令
- FROM 可以在单个 Dockerfile 中出现多次,以创建多个镜像或使用一个构建阶段作为另一个构建阶段的依赖项。前面的FROM及对应的构建阶段都不会保存,只有最后一个阶段构建的镜像会被直接保存。
tag或digest值是可选的。如果省略其中任何一个,构建器默认采用 latest 标记。
RUN
RUN 指令将执行任何命令以在当前图像之上创建新层。添加的层将在 Dockerfile 的下一步中使用。 RUN 有两种形式:1
2
3
4
5
6
7shell格式,<命令行命令> 等同于,在终端操作的 shell 命令。
RUN <命令行命令>
exec 格式:
RUN ["可执行文件", "参数1", "参数2"]
例如:
RUN ["./test.php", "dev", "offline"] 等价于 RUN ./test.php dev offline
shell 形式是最常用的,它允许您将较长的指令分成多行,可以使用换行符转义,也可以使用下属方法:1
2
3
4
5#shell格式,<命令行命令> 等同于,在终端操作的 shell 命令。相当于命令行续航符"\"
RUN <<EOF
apt-get update
apt-get install -y curl
EOF
前面的RUN构建过程相对容易,比较复杂的时相关文件系统的处理,因为可能在构建过程中,我们需要使用到一些特定的数据,这些数据我们需要在执行某些特定命令的时候,挂载到镜像上。 RUN –mount 允许您创建构建可以访问的文件系统挂载。这可以用于:
- 创建到主机文件系统或其他构建阶段的绑定挂载
- 访问构建机密或 ssh-agent 套接字
- 使用持久的包管理缓存来加快构建速度
| 类型 | 说明 |
|---|---|
| bind(default) | 用于挂载一个上下文目录(不能挂载宿主机的目录)到容器中 ,默认时只读的 |
| cache | 主要用于挂载一个临时目录来缓存编译器和包管理器的目录。 |
| tmpfs | 主要用于挂载一个tmpfs |
| secret | 允许构建容器访问诸如私钥之类的安全文件,并且此类文件不会出现在构建好的镜像中,避免密钥外泄。 |
| ssh | 允许构建容器通过SSH代理访问SSH密钥,并支持密码短语 |
这部分使用因为在自己业务场景中没有太大需求,这里介绍一些简单的示例。知道有这个功能就好,就不进行展开了。1
2
3
4
5RUN --mount=[type=TYPE][,option=<value>[,option=<value>]...]
# 挂载一个大小为100MB的临时文件系统到 /mnt 目录,并在其中执行命令
RUN --mount=type=tmpfs,size=100m,uid=1000,gid=1000,mode=0755 \
touch /mnt/test_file
CMD
CMD 指令设置从映像运行容器时执行的命令。
Dockerfile 中只能有一条 CMD 指令。如果列出多个 CMD ,则只有最后一个生效。
CMD 的目的是为执行容器(创建镜像阶段不会执行)提供默认值。这些默认值可以包含可执行文件,也可以省略可执行文件,在这种情况下,您还必须指定 ENTRYPOINT 指令。
LABEL
1 | LABEL <key>=<value> <key>=<value> <key>=<value> ... |
LABEl支持将元数据添加到镜像中,LABEL的格式是key=value的键值对。如果value中涉及到空格的话,参考命令行使用引号和反斜杠来处理。
示例:1
2
3
4
5LABEL "com.example.vendor"="ACME Incorporated"
LABEL org.opencontainers.image.authors="liubo4@genomics.com"
LABEL version="1.0"
LABEL description="This text illustrates \
that label-values can span multiple lines."
一个镜像可能会有多个标签,针对早期的Docker(Docker 1.10之前)写成一行可以有效的降低最终的镜像大小;新版本的Docker(Docker 1.10之后)两种写法是类似的。1
2
3
4
5LABEL multi.label1="value1" multi.label2="value2" other="value3"
LABEL multi.label1="value1" \
multi.label2="value2" \
other="value3"
父镜像(From的镜像)中的标签将会子镜像继承,在子镜像重新赋值的可以覆盖父镜像的标签。查看镜像的标签使用 docker image inspect $imageid
ADD
ADD 还可以复制压缩(tarball)文件并在目的地自动提取内容。此功能仅适用于本地存储的压缩文件和目录。但 ADD 可以提取压缩文件并通过URL从远程位置复制文件。
1 | ADD [source] … [destination] |
COPY示例
Docker 引入了 COPY 作为复制内容的附加命令,以解决 ADD 命令的一些功能问题。 ADD 的主要问题是压缩文件的自动提取,这可能会导致 Docker 镜像损坏。如果用户没有预料到该命令的行为,就会发生这种情况。
COPY 命令的唯一指定功能是以现有格式复制指定的文件和目录。此限制会影响按原样复制的压缩文件,即不解压。
此外, COPY 仅适用于本地存储的文件。它不能与 URL 一起使用来将外部文件复制到镜像。
需要拷贝的目录需要在dockerFIle目录中保存,测试使用中,只能访问该目录下的文件,其他目录进行拷贝时,会存在无法访问相关文件的问题。
1 | COPY [source] … [destination] |
除了用户想要提取本地压缩文件外,在所有情况下都不鼓励使用 ADD 命令。对于复制远程文件,run命令结合wget或curl更安全、更高效。此方法避免创建额外的图像层并节省空间。
SHELL
SHELL 指令用于更改执行命令的默认 shell。
Linux 上的默认 shell 是 [“/bin/sh”, “-c”] ,
Windows 上的默认 shell 是 [“cmd”, “/S”, “/C”] 。
SHELL 指令必须以JSON形式写入 Dockerfile 中。 SHELL 指令在 Windows 上特别有用,其中有两种常用且截然不同的本机 shell: cmd 和 powershell ,以及可用的备用其它 shell。
1 | FROM microsoft/windowsservercore |
当 Dockerfile 中使用 shell 形式时,以下指令可能会受到 SHELL 指令的影响: RUN 、 CMD 和 ENTRYPOINT .
ARG
ARG 指令定义了一个变量,用户可以在构建时使用 –build-arg
ARG 变量定义从 Dockerfile 中定义它的行开始生效,而不是从命令行或其他地方使用参数开始生效。所以尽可能紧跟FROM 完成需要传递变量的定义,从而避免引用变量时,还未进行定义。
1 | FROM busybox |
- 每个构建阶段单独声明
ARG 指令在定义它的构建阶段结束时超出范围。要在多个阶段使用参数,每个阶段必须包含 ARG 指令。1
2
3
4
5
6
7FROM busybox
ARG SETTINGS
RUN ./run/setup $SETTINGS
FROM busybox
ARG SETTINGS
RUN ./run/other $SETTINGS
VOLUME
撰写DockerFile
了解了DockerFile的语法之后,我们可以利用对应的指令编写我们需要环境对应的一个 DockerFile。
再编写DockerFile的时候,我们需要注意几个点:
规范要求
将以 # 开头的行视为注释,除非该行是有效的解析器指令。行中任何其他位置的 # 标记都被视为参数。这允许这样的语句:
1
2
3RUN echo hello \
# comment
world约定俗成
- 该指令不区分大小写。然而,惯例是它们是大写的,以便更容易地将它们与参数区分开来。
注意事项
- 编写Dockerfile,Dockerfile中每一条/行指令都创建镜像的一层,所以尽可能合并无意义的层,避免镜像过大,例如:
这时候,也许会想,我们把所有环境安装都放到一层不就可以了么。
创建镜像的时候还要考虑其他问题:
不建议的写法(命令拆分产生无意义的多层镜像):1. 那就是镜像创建环境的网络稳定性。如果只有一层,而网络又不稳定的话,那么可能每次构建都会中途中断,然后需要从头重构。进行合理的分层,相当于设置一些构建过程中的一些重要节点(层),这样在进行build的时候,会记录保存中间层的构建,二次构建的时候,可以直接引用已有层的结果,即使中途中断,也可以从对应节点继续。 2. 如果需要使用的镜像环境非常多,我们需要构建多个镜像,但是他们有一些相同的底层镜像依赖。那么我们可以通过镜像分层构建共同的底座,然后通过继承的方式来构建不同的镜像。1
2
3
4
5
6
7
8# 这里是注释
# 设置继承自哪个镜像
FROM ubuntu:14.04
# 下面是一些创建者的基本信息
MAINTAINER liubo (liubo4@genomics.com)
# 在终端需要执行的命令
RUN apt-get install -y openssh-server # 创建第一层
RUN mkdir -p /var/run/sshd # 创建第二层
建议的写法(如非必要只创建一层)1
2
3
4
5
6# 设置继承自哪个镜像
FROM ubuntu:14.04
# 下面是一些创建者的基本信息
MAINTAINER birdben (191654006@163.com)
# 在终端需要执行的命令
RUN apt-get install -y openssh-server && mkdir -p /var/run/sshd # 创建第一层
通过DockerFile生成镜像
编写完成 Dockerfile 后可以使用 docker build 来生成镜像。
首先创建一个新的目录,将 DockerFile 文件存档道目录中,执行 docker build 命令来生成镜像。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ sudo docker build -t="birdben/ubuntu:v1" .
# 下面是一堆构建日志信息
############
我是日志
############
# 参数:
# -t 标记来添加 tag,指定新的镜像的用户和镜像名称信息。
# “.” 是 Dockerfile 所在的路径(当前目录),也可以替换为一个具体的 Dockerfile 的路径。
# 以交互方式运行docker
$ docker run -it birdben/ubuntu:v1 /bin/bash
# 运行docker时指定配置
$ sudo docker run -d -p 10.211.55.4:9999:22 ubuntu:tools '/usr/sbin/sshd' -D
# 参数:
# -i:表示以“交互模式”运行容器,-i 则让容器的标准输入保持打开
# -t:表示容器启动后会进入其命令行,-t 选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上
# -v:表示需要将本地哪个目录挂载到容器中,格式:-v <宿主机目录>:<容器目录>,-v 标记来创建一个数据卷并挂载到容器里。在一次 run 中多次使用可以挂载多个数据卷。
# -p:指定对外80端口
# 不一定要使用“镜像 ID”,也可以使用“仓库名:标签名”
在build阶段设置参数
Dockerfile 最后一行如下:
1
2
3
4
5
6
7
8
9
10
11
12
13[root@fangjike temp]# cat Dockerfile
FROM python:2.7-slim
MAINTAINER yellowtail
COPY startup.sh /opt
RUN chmod +x /opt/startup.sh
ARG envType=xxx
ENV envType ${envType}
CMD /opt/startup.sh ${envType}build
1
docker build -t yellow:4.0 --build-arg envType=dev .
run
1
2[root@fangjike temp]# docker run -ti --rm=true yellow:4.0
in startup, args: dev