镜像的简介
镜像是一个只读的 Docker 容器模板,包含启动容器所需要的所有文件系统结构和内容。简单来讲,镜像是一个特殊的文件系统,它提供了容器运行时所需的程序、软件库、资源、配置等静态数据(即镜像不包含任何动态数据,镜像内容在构建后不会被改变)。
从上图中可知,镜像的操作可分为以下内容:
-
拉取镜像:使用
docker pull
命令拉取远程仓库的镜像到本地; -
重命名镜像:使用
docker tag
命令重命名镜像; -
查看镜像:使用
docker image ls
或docker images
命令查看本地已经存在的镜像; -
删除镜像:使用
docker rmi
命令删除无用镜像; -
构建镜像:构建镜像有两种方式(第一种方式是使用
docker build
命令基于 Dockerfile 构建镜像;第二种方式是使用docker commit
命令基于已经运行的容器提交为镜像)。
镜像的实现原理
Docker 镜像是由一系列镜像层(layer)组成的,每一层代表了镜像构建过程中的一次提交。下面以一个镜像构建的 Dockerfile 来说明镜像是如何分层的。
FROM busybox
COPY test /tmp/test
RUN mkdir /tmp/testdir
上面的 Dockerfile 由三步组成:
-
第一行基于
busybox
创建一个镜像层; -
第二行拷贝本机
test
文件到镜像内; -
第三行在
/tmp
文件夹下创建一个目录testdir
。
为了验证镜像的存储结构,使用 docker build
命令在上面 Dockerfile 所在目录构建一个镜像:
# 以下为 tree . 命令输出内容
|-- 3e89b959f921227acab94f5ab4524252ae0a829ff8a3687178e3aca56d605679
| |-- diff # 这一层为基础层,对应上述 Dockerfile 第一行,包含 busybox 镜像所有文件内容,例如 /etc,/bin,/var 等目录
... 此次省略部分原始镜像文件内容
| -- link
|-- <span class="hljs-number">6591</span>d4e47eb2488e6297a0a07a2439f550cdb22845b6d2ddb1be2466ae7a9391
| |-- diff # 这一层对应上述 Dockerfile 第二行,拷贝 test 文件到 /tmp 文件夹下,因此 diff 文件夹下有了 /tmp/test 文件
| |-- tmp
| | -- test
| |-- link
| |-- lower
|-- work
|-- backingFsBlockDev
|-- bec6a018080f7b808565728dee8447b9e86b3093b16ad5e6a1ac3976528a8bb1
| |-- diff # 这一层对应上述 Dockerfile 第三行,在 /tmp 文件夹下创建 testdir 文件夹,因此 diff 文件夹下有了 /tmp/testdir 文件夹
| | -- tmp
| |-- testdir
| |-- link
| |-- lower
| `-- work
...
通过上面的目录结构可以看到,Dockerfile 的每一行命令都生成了一个镜像层,每一层的 diff
文件夹下只存放了增量数据,如下图所示:
分层的结构使得 Docker 镜像非常轻量,每一层根据镜像的内容都有一个唯一的 ID 值,当不同的镜像之间有相同的镜像层时,便可以实现不同的镜像之间共享镜像层的效果。
总结: Docker 镜像是静态的分层管理的文件组合,镜像底层的实现依赖于联合文件系统(UnionFS)
镜像的构建
镜像的构建过程如下图所示:
基于容器制作镜像
根据容器的更改创建新映像,用法如下:
docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]
# 使用pull命令拉网上的busybox镜像
[root@localhost ~]# docker pull busybox
Using default tag: latest
latest: Pulling from library/busybox
50783e0dfb64: Pull complete
Digest: sha256:ef320ff10026a50cf5f0213d35537ce0041ac1d96e9b7800bafd8bc9eff6c693
Status: Downloaded newer image for busybox:latest
docker.io/library/busybox:latest
[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
busybox latest 7a80323521cc 6 days ago 1.24MB
# 使用镜像busybox创建并运行一个名叫bus1容器并使用交互模式进去编辑,创建一个index.html文件
[root@localhost ~]# docker run -it --name bus1 busybox
/ # ls
bin dev etc home proc root sys tmp usr var
/ # mkdir /data
/ # echo 'hellow world' > /data/index.html
/ # cat /data/index.html
hellow world
# 创建镜像的时候不能关闭容器,必须使其处于运行状态,所以我们必须要另起一个终端
[root@localhost ~]# docker commit -a "alone@qq.com" -p -m "first commit" bus1
sha256:c476706b4b674189c8c47342398195108dfab017006d0351b31196f2d83aed47
[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> c476706b4b67 8 seconds ago 1.24MB
busybox latest beae173ccac6 7 months ago 1.24MB
# 替换信息,名称为web,版本为v1
[root@localhost ~]# docker tag c476706b4b67 web:v1
[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
web v1 c476706b4b67 44 seconds ago 1.24MB
busybox latest beae173ccac6 7 months ago 1.24MB
# 复制web镜像,名称为483607723,版本为v2
[root@localhost ~]# docker tag web:v1 483607723/web:v2
[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
483607723/web v2 c476706b4b67 About a minute ago 1.24MB
web v1 c476706b4b67 About a minute ago 1.24MB
busybox latest beae173ccac6 7 months ago 1.24MB
打开 Docker 官网 ,登录后在首页选择 Create a Repository ,如下图所示:
输入仓库名称 web ,然后点击 Create ,如下图所示:
上传的方法如下图所示:
# 登陆 dockerhub 账号
[root@localhost ~]# docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: 483607723
Password:
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
# 上传刚才修改的镜像 webv2
[root@localhost ~]# docker push 483607723/web:v2
The push refers to repository [docker.io/483607723/web]
af1fc705554b: Pushed
01fd6df81c8e: Mounted from library/busybox
v2: digest: sha256:e7ffae293a2b4ad19dcbd821198769af4b7abde789ec2109af6f71628e7ef730 size: 734
上传成功后,如下图所示:
Dockerfile 构建镜像
(1)介绍与使用
Dockerfile 是一个包含了用户所有构建命令的文本通过 docker build
命令可以从 Dockerfile 生成镜像,使用 Dockerfile 构建镜像具有以下特性:
-
Dockerfile 的每一行命令都会生成一个独立的镜像层,并且拥有唯一的 ID;
-
Dockerfile 的命令是完全透明的,通过查看 Dockerfile 的内容,就可以知道镜像是如何一步步构建的;
-
Dockerfile 是纯文本的,方便跟随代码一起存放在代码仓库并做版本管理。
使用 Dockerfile 构建镜像,本质上也是通过镜像创建容器并在容器中执行相应的指令,然后停止容器,提交存储层的文件变更和用 docker commit
构建镜像的方式相比有如下的三个好处:
-
Dockerfile 包含了镜像制作的完整操作流程,其他开发者可以通过 Dockerfile 了解并复现制作过程。
-
Dockerfile 中的每一条指令都会创建新的镜像层,这些镜像可以被 Docker Daemnon 缓存,再次制作镜像时,Docker 会尽量复用缓存的镜像层(using cache),而不是重新逐层构建,这样可以节省时间和磁盘空间。
-
Dockerfile 的操作流程可以通过
docker image history [镜像名称]
查询,方便开发者查看变更记录。
一个 Dockerfile 文件中包含了多条指令,这些指令可以分为以下的 5 类:
-
定义基础镜像的指令:FROM ;
-
定义镜像维护者的指令:MAINTAINER(可选);
-
定义镜像构建过程的指令:COPY、ADD、RUN、USER、WORKDIR、ARG、ENV、VOLUME、ONBUILD ;
-
定义容器启动时执行命令的指令:CMD、ENTRYPOINT ;
-
其他指令:EXPOSE、HEALTHCHECK、STOPSIGNAL 。
Dockerfile 指令 | 指令简介 |
---|---|
FROM | Dockerfile 除了注释第一行必须是 FROM ,FROM 后面跟镜像名称,代表要基于哪个基础镜像构建容器 |
RUN | RUN 后面跟一个具体的命令 |
ADD | 拷贝本机文件或者远程文件到镜像内 |
COPY | 拷贝本机文件到镜像内 |
USER | 指定容器启动的用户 |
ENTRYPOINT | 容器的启动命令 |
CMD | CMD 为 ENTRYPOINT 指令提供默认参数,也可以单独使用 CMD 指定容器启动参数 |
ENV | 指定容器运行时的环境变量,格式为 key=value |
ARG | 定义外部变量,构建镜像时可以使用 build-arg = 的格式传递参数用于构建 |
EXPOSE | 指定容器监听的端口,格式为 [port]/tcp 或者 [port]/udp |
WORKDIR | 为 Dockerfile 中跟在其后的所有 RUN、CMD、 |
例如以下的 Dockerfile 文件,该文件的具体内容如下:
FROM centos:7
COPY nginx.repo /etc/yum.repos.d/nginx.repo
RUN yum install -y nginx
EXPOSE 80
ENV HOST=mynginx
CMD ["nginx","-g","daemon off;"]
-
第一行表示基于 centos:7 这个镜像来构建自定义镜像。每个 Dockerfile 的第一行除了注释都必须以 FROM 开头。
-
第二行表示拷贝本地文件
nginx.repo
文件到容器内的/etc/yum.repos.d
目录下,这里拷贝nginx.repo
文件是为了添加 nginx 的安装源。 -
第三行表示在容器内运行
yum install -y nginx
命令,安装 nginx 服务到容器内,执行完第三行命令,容器内的 nginx 已经安装完成。 -
第四行声明容器内业务(nginx)使用 80 端口对外提供服务。
-
第五行定义容器启动时的环境变量
HOST=mynginx
,容器启动后可以获取到环境变量 HOST 的值为 mynginx。 -
第六行定义容器的启动命令,命令格式为 json 数组。这里设置了容器的启动命令为 nginx ,并且添加了 nginx 的启动参数 -g ‘daemon off;’ ,使得 nginx 以前台的方式启动。
(2)Dockerfile 指令详解
Dockerfile 中包含了大量的指令,具体的指令使用方法及功能如下:
- FROM
格式:FROM <image>
或 FROM <image>:<tag>
FROM 指令的功能是为后面的指令提供基础镜像,Dockerfile 必须以 FROM 指令作为第一条非注释指令,可以根据需要,选择任何有效的镜像作为基础镜像。
可以在一个 Dockerfile 文件中,使用多个 FROM 指令,来构建多个镜像。每个镜像构建完成之后,Docker 会打印出该镜像的 ID ,如果 FROM 指令中没有指定镜像的 tag ,则默认 tag 是 latest ;如果镜像不存在,则报错退出。
- MAINTAINER
格式:MAINTAINER <name> <Email>
用来指定维护该 Dockerfile 的作者信息。
- ENV
ENV指令有两种格式:ENV <key> <value>
/ ENV <key1>=<value1> <key2>=<value2>
ENV 指令用来在镜像创建出来的容器中声明环境变量,被声明的环境变量可被后面的特定指令(如:ENV、ADD、COPY、WORKDIR、EXPOSE、VOLUME、USER),其他指令引用格式为:$variableName 或 ${variableName} ,可以使用斜杠 \ 来转义环境变量(如:$test 或 ${test},这样二者将会被分别转换为 $test 和 ${test} 字符串,而不是环境变量所保存的值)。这里需要注意,ONBUILD 指令不支持环境替换。
- WORKDIR
格式:WORKDIR <工作目录路径>
该指令用于指定当前的工作目录,使用该命令后,接下来每一层的工作目录都会切换到指定的目录(除非重新使用 WORKDIR 切换)。WORKDIR 的路径始终使用绝对路径,这可以保证指令的准确和可靠;使用 WORKDIR 来替代RUN cd ... && do-something
这样难以维护的指令。
- COPY
格式:COPY <src> <dest>
COPY 指令复制本机上的 文件或目录到镜像的 路径下,若 或 以反斜杠 / 结尾则说明其指向的是目录,否则指向文件。
可以是多个文件或目录,但必须是上下文根目录中的相对路径,不能使用形如 COPY ../filename /filename
这样的指令。此外, 支持使用通配符指向所有匹配通配符的文件或目录(如:COPY xxx* /opt/
表示添加所有以 xxx 开头的文件到目录 /opt/ 中)。
可以是文件或目录,但必须是镜像中的绝对路径或者相对于 WORKDIR 的相对路径。 若 是一个文件,则 的内容会被复制到 中;否则 指向的文件或目录中的内容会被复制到 目录中。当 指定多个源时, 必须是目录;如果 在镜像中不存在,则会被自动创建。
- ADD
格式:ADD <src> <dest>
ADD 与 COPY 指令具有相似的功能,都支持复制本地文件到镜像的功能,但 ADD 指令还支持其他功。在 ADD 指令中, 可以是一个网络文件的下载地址, 比如 ADD http://example.com/xxx.yaml/
会在镜像中创建文件 /xxx.yaml 。
还可以是一个压缩归档文件,该文件在复制到容器时会被解压提取,比如 ADD xxx.tar.xz / 。但是若URL中的文件为归档文件,则不会被解压提取。
在编写 Dockerfile 时,推荐使用 COPY 指令,因为 COPY 只支持本地文件,它比 ADD 更加透明。
- RUN
RUN 指令有两种格式:RUN <command>
(shell 格式) / RUN ["executable", "param1", "param2"]
(exec 格式,推荐使用)
RUN 指令会在前一条命令创建出的镜像的基础上创建一个容器,并在容器中运行命令,在命令结束运行后提交容器为新镜像,新镜像被 Dockerfile 中的下一条指令使用。
当使用 shell 格式时,命令通过 /bin/sh -c
运行;当使用 exec 格式时,命令是直接运行的,即不通过 shell 来运行命令。exec 格式中的参数会以 JSON 数组的格式被 Docker 解析,所以参数必须使用双引号而不是单引号,因为 exec 格式不会在 shell 中执行,所以环境变量不会被替换。
- CMD
CMD 指令有 3 种格式: CMD <command>
(shell格式) / CMD ["executable", "param1", "param2"]
(exec格式,推荐使用) / CMD ["param1", "param2"]
(为ENTRYPOINT 指令提供参数)
CMD 指令提供容器运行时的默认命令或参数,一个 Dockerfile 中可以有多条 CMD 指令,但只有最后一条 CMD 指令有效。 CMD ["param1", "param2"]
格式用来跟 ENTRYPOINT 指令配合使用,CMD 指令中的参数会添加到 ENTRYPOINT 指令中。当使用 shell 和 exec 格式时,命令在容器中的运行方式与 RUN 指令相同,如果在执行 docker run
时指定了命令行参数,则会覆盖 CMD指令中的命令。
- ENTRYPOINT
ENTRYPOINT指令有两种格式:ENTRYPOINT <command>
(shell 格式) / ENTRYPOINT ["executable", "param1", "param2"]
(exec 格式,推荐格式)
ENTRYPOINT 指令和 CMD 指令类似,都可以让容器在每次启动时执行指定的命令,但二者又有不同。ENTRYPOINT 只能是命令 ,而 CMD 可以是参数,也可以是指令;docker run
命令行参数可以覆盖 CMD ,但不能覆盖 ENTRYPOINT 。
当使用 Shell 格式时,ENTRYPOINT 指令会忽略任何 CMD 指令和 docker run 命令的参数,并且会运行在 bin/sh -c 中。
使用 exec 格式时,docker run
传入的命令行参数会覆盖 CMD 指令的内容并且追加到 ENTRYPOINT 指令的参数中。一个 Dockerfile 中可以有多条 ENTRYPOINT 指令,但只有最后一条 ENTRYPOINT 指令有效。
- ONBUILD
格式:ONBUILD [INSTRUCTION]
ONBUILD 指令后面跟的是其它指令(如 RUN , COPY 等),在当前镜像构建时不会被执行,当以当前镜像为基础镜像,构建下一级镜像时才会被执行。
这里要注意,使用包含 ONBUILD 指令的 Dockerfile ,构建的镜像应该打上特殊的标签,在 ONBUILD 指令中使用 ADD 或 COPY 指令时要特别注意,假如新的构建过程的上下文中缺失了被添加的资源,则新的构建过程会失败。
- VOLUME
VOLUME 指令有两种格式:VOLUMN ["<路径1>", "路径2"...]
/ VOLUMN <路径>
为了防止容器内的重要数据因为容器重启而丢失且避免容器不断变大,应该将容器内的某些目录挂载到宿主机上。在 Dockerfile 文件中,可以通过 VOLUME 指令事先将某些目录挂载为匿名卷,这样在执行 docker run 时,如果没有指定 -v 选项,则默认会将 VOLUMN 指定的目录挂载为匿名卷 。
- EXPOSE
格式:EXPOSE <端口1> [<端口2> ...]
EXPOSE 指定了该镜像生成容器时提供服务的端口(默认对外不暴露端口),可以配合 docker run -P
使用,也可以配合 docker run --net=host
使用。
- LABEL
格式:LABEL <key>=<value> <key>=<value> <key>=<value> ...
LABEL 指令用来给镜像添加一些元数据(metadata)。
- USER
格式:USER <用户名>[:<用户组>]
USER 指令设定一个用户或者用户 ID ,在执行 RUN、CMD、ENTRYPOINT 等指令时指定以那个用户得身份去执行。如果容器中的服务不需要以特权身份来运行,则可以使用 USER 指令,切换到非 root 用户,以此来增加安全性。
- ARG
格式:ARG <参数名>[=<默认值>]
与 ENV 作用一致,但作用域不相同,通过 ARG 指定的环境变量仅在 docker build 的过程中有效,构建好的镜像内不存在此变量。虽然构建好的镜像内不存在此变量,但是使用 docker history 可以查看到,故敏感数据一般不使用 ARG,可以在 docker build 时用 --build-arg <参数名>=<值> 来覆盖 ARG 指定的参数值 。
- HEALTHCHECK
HEALTHCHECK 指令有两种格式:HEALTHCHECK [选项] CMD <命令>
(设置检查容器健康状况的命令)/ HEALTHCHECK NONE
(用来屏蔽掉已有的健康检查指令)
定时检测容器进程是否健康,可以设置检查的时间间隔、超时时间、重试次数、以及用于判断是否健康的指令。其中,CMD 指定的 <命令> 的返回值决定了该次健康检查是否成功:0 成功;1 失败;2 保留。
HEALTHCHECK 支持下列指令:
指令 | 说明 |
---|---|
–interval=<时间间隔> | 设置健康检查的时间间隔,默认是 30s |
–timeout=<超时时长> | 设置单次健康检查的超时时间,默认是 30s |
–retries=<重试次数> | 如果健康检查失败,重试多少次,默认是 3 次;如果连续 3 次健康检查都失败,容器的 STATUS 就会显示 unhealthy |
- STOPSIGNAL
格式:STOPSIGNAL signal
STOPSIGNAL 指令指定了容器退出时,发送给容器的系统调用信号。
(3)Dockerfile 编写建议
-
所有的 Dockerfile 指令大写,这样做可以很好地跟在镜像内执行的指令区分开来。
-
在选择基础镜像时,尽量选择官方的镜像并在满足要求的情况下,尽量选择体积小的镜像。Linux 镜像大小有以下关系:busybox < debian < centos < ubuntu ,最好确保同一个项目中使用一个统一的基础镜像;如无特殊需求,可以选择使用 debian:jessie 或者 alpine 。
-
在构建镜像时,删除不需要的文件,只安装需要的文件,保持镜像干净、轻量。
-
使用更少的层,把相关的内容放到一个层,并使用换行符进行分割。这样可以进一步减小镜像的体积,也方便查看镜像历史。
-
不应在 Dockerfile 中修改文件的权限。因为如果修改文件的权限,Docker 在构建时会重新复制一份,这会导致镜像体积越来越大。
-
给镜像打上标签,标签可以帮助理解镜像的功能(如:docker build -t=“nginx:3.0-onbuild” )。
-
FROM 指令应该包含 tag (如使用 FROM debian:jessie ,而不是 FROM debian )。
-
充分利用缓存。Docker 构建引擎会顺序执行 Dockerfile 中的指令,而且一旦缓存失效,后续命令将不能使用缓存。为了有效地利用缓存,需要尽量将所有的 Dockerfile 文件中相同的部分都放在前面,而将不同的部分放在后面。
-
优先使用 COPY 而非 ADD 指令,ADD 可变的行为会导致该指令的行为不清晰,不利于后期维护和理解。
-
推荐将 CMD 和 ENTRYPOINT 指令结合使用,使用 execl 格式的 ENTRYPOINT 指令设置固定的默认命令和参数,然后使用 CMD 指令设置可变的参数。
-
尽量使用 Dockerfile 共享镜像。通过共享 Dockerfile ,可以使开发者明确知道 Docker 镜像的构建过程并且将 Dockerfile 文件加入版本控制,方便跟踪。
-
使用 .dockerignore 忽略构建镜像时非必需的文件。忽略无用的文件,可以提高构建速度。
-
使用多阶段构建。多阶段构建可以大幅减小最终镜像的体积(如:COPY 指令中可能包含一些安装包,安装完成之后这些内容就废弃掉)。