docker 镜像管理

概述

镜像是 docker 的精髓,只有了解 docker 镜像,才能真正理解 docker 的内涵。这篇文章介绍了镜像相关的操作和对镜像的操作。

什么是镜像

镜像是一个应用程序的依赖包以及其运行环境的完整拷贝,用于为创建容器提供模板。

每个 docker 镜像都有一个唯一的标识 ImageID,与容器 ID 类似,默认为 128 位,可以使用前 16 位的缩略形式,也可以使用镜像的名字(NAME)和版本号(TAG)组合成唯一标识,如果省略版本号则使用最新版本(latest)。如:redis:latest 、redis:6.0.7

镜像的分层概念

docker 镜像采用分层机制,相同部分独立成层,只需要存储一份就可以了,多个镜像可以使用相同的底层镜像,大大节省了镜像空间。

对于分层的 docker 镜像有两个特性:

  • 已有的分层只能读不能修改,如果要修改已有层上的文件,只能将文件拷贝到新层上,然后修改新层中的文件。
  • 上层镜像的优先级高于底层镜像,如果不同层有相同的文件,则上层的文件将覆盖下层的文件,以上层文件为准。

容器是在镜像的基础上创建的,从文件系统的角度看,它是在镜像的基础上增加一个新的空白层,这个新分层是可读写的。容器的所有写操作都会存储在最上面的可读写层。

使用 docker commit 命令提交对镜像的修改,会将修改的层叠加到已有镜像层之上,形成新的镜像,最终用户看到的是所有层叠加的结果,所看到的文件也是所有层文件的集合。如:

第一层文件:

1
2
3
a.txt
b.txt
c.txt

第二层文件:

1
2
d.txt
e.txt

第三层文件:

1
f.txt

最终:

1
2
3
4
5
6
a.txt
b.txt
c.txt
d.txt
e.txt
f.txt

体验

在 docker 客户端启动一个容器时,docker 会自动获取镜像。首先会在本地镜像中查找所需镜像,如果有则直接启动;如果本地没有该镜像,docker 客户端会从远程仓库中查找,找到后会下载到本地并启动,如果都没有找到则会报错。所以我们只需要运行容器就可以自动获取镜像,如运行 redis 容器:

1
docker run hello-world

运行结果:

1
2
3
4
5
6
7
8
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
1b930d010525: Pull complete
Digest: sha256:f9dfddf63636d84ef479d645ab5885156ae030f611a56f3a7ac7f2fdd86d7e4e
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

从上述运行结果可以看出,由于本地没有 hello-world:latest 镜像,所以从远端仓库下载了该镜像。

查看镜像

使用 docker images -a 或 docker image ls 命令查看本地所有镜像。

使用 docker inspect 命令查看镜像详情。

使用 docker history 查看镜像提交历史。

获取镜像

我们可以从 docker 仓库中查找并下载镜像。

Docker 官方的镜像仓库地址:https://hub.docker.com/

检索镜像

  • 登录 docker 镜像仓库的 web 页面,可以搜索自己想要的镜像,并可以查看镜像相关信息,如版本特性、使用说明等。

  • 在客户端使用 docker search 命令搜索镜像,用法如下:

    1
    docker search [OPTIONS] TERM

    参数:

    • -f ,--filter 按指定条件过滤输出结果
    • --format 使用 Go 模板格式化输出结果
    • --limit 限制最大搜索结果数量,默认 25
    • --no-trunc 不删减输出

    例如:检索 redis 镜像,执行命令 docker search redis:

    1
    2
    3
    4
    5
    6
    7
    NAME                             DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
    redis Redis is an open source key-value store that… 8542 [OK]
    bitnami/redis Bitnami Redis Docker Image 160 [OK]
    sameersbn/redis 80 [OK]
    grokzen/redis-cluster Redis cluster 3.0, 3.2, 4.0, 5.0, 6.0 70
    rediscommander/redis-commander Alpine image for redis-commander - Redis man… 46 [OK]
    kubeguide/redis-master redis-master with "Hello World!" 32

    结果中有五个数据项:

    • NAME 镜像名
    • DESCRIPTION 镜像描述,只显示一部分,如果需要看完整描述,可以加上参数 --no-trunc
    • STARS 获得的 star 数
    • OFFICIAL 是否官方版本
    • AUTOMATED 是否自动构建

拉取镜像

在客户端可以使用 docker pull 或 docker image pull 从仓库中拉取镜像,使用方法说明:

1
2
docker pull [OPTIONS] NAME[:TAG|@DIGEST]
docker image pull [OPTIONS] NAME[:TAG|@DIGEST]

拉取镜像时需要指定镜像名,如拉取 redis 镜像: docker pull redis。可以指定版本,如果不指定版本,默认使用最新版本(latest),如拉取 6.0.7 版本的 redis:docker pull redis:6.0.7 。

参数:

  • -a , --all-tags 下载所有版本的镜像
  • --disable-content-trust 跳过镜像校验,默认为 true
  • --platform 指定平台
  • -q , --quiet 禁止冗余输出

创建镜像

通过提交容器创建镜像

使用 docker commit 命令可以将 docker 容器提交成为镜像。

使用 docker image history 命令查看镜像的提交历史。

通过 Dockerfile 创建

Dockerfile 是一个描述了 docker 镜像编译顺序、编译规则的脚本文件,是一个文本文件,跟 Makefile 的概念类似。

示例:

这是一个最简单的 Dockerfile 示例,用来将一个打包好的 springboot 项目 jar 包构建成一个 docker 镜像。

1
2
3
4
5
6
7
8
# 使用 openjdk8 镜像,添加 java 环境
FROM openjdk:8-jre-alpine
# 将打包后的项目文件添加到镜像中,并改名为 app.jar
ADD docker-demo-0.0.1-SNAPSHOT.jar app.jar
# 对外暴露的端口
EXPOSE 8080
# 启动时运行的命令
ENTRYPOINT ["java","-jar","/app.jar"]
Dockerfile 语法

每一行都以一个关键字为行首,如果一行内容过长,它使用 \ 把多行连接到一起。以 # 开头表示单行注释。

dockerfile 语法是 command argument argument ... ,就是 命令 参数 参数 ... ,如要输出一句 hello world :

1
RUN echo "hello world"
Dockerfile 内置命令
命令 功能 是否必须
FROM 指定基础镜像名称 是
MAINTAINER 指定镜像创建者,也可同时填写版本及其他备注信息 否
RUN 执行指定 shell 命令(多条命令使用 ``),执行完成后将产生一个新文件层 否
CMD 指定此镜像启动时默认执行命令 否
LABEL 为镜像添加元数据,如版本号、构建日期 否
EXPOSE 指定镜像需要暴露的网络端口号 否
ENV 添加环境变量 否
ADD 向镜像中添加新文件或新目录 否
COPY 从编译主机向镜像复制文件 否
ENTRYPOINT 在镜像中设定默认执行的二进制程序 否
VOLUME 向镜像中挂载一个卷组 否
USER 在镜像构建过程中切换到另一个用户 否
WORKDIR 设置此镜像后续操作的默认工作目录 否
ONBUILD 配置构建触发指令集 否
FROM

命令是 Dockerfile 命令中唯一一个必填命令,在有效的 Dockerfile 中,FROM 永远是第一个出现的命令。当在一个 Dockerfile 中构建多个镜像时,FROM 可以出现多次。

RUN

命令后面的命令都是由 /bin/sh -C 负责执行的,当镜像中没有 /bin/sh 时,需要使用 RUN ["executable", "param1", "param2"] 方式来执行镜像中的二进制程序。如使用 bash 来执行程序:RUN ["/bin/bash", "-c", "hello"] ,这里的参数会按照 JSON 字符串格式解析,因此只能使用双引号,不能使用单引号和其他符号。

CMD

设定的命令将作为容器启动时的默认执行命令,用法有三种:

  • CMD ["executable","param1","param2"] 推荐用法

  • CMD ["param2","param2"] 其中的参数将作为 ENTERPOINT 的默认参数使用`

  • CMD command param1 param2 将后面的命令作为 shell 命令执行,依靠 /bin/sh -C 执行

CMD 和 RUN 都是用于执行命令,但 RUN 是在镜像构建过程中执行的命令,CMD 只是设定好需要执行的命令,等到容器启动时才会真正执行。

LABEL

使用键值对方式给镜像添加元数据信息,用法:

1
LABEL <key>=<value> <key2>=<vlaue2> ...

如果 value 中有空格,需要使用双引号包括。

可以一次性设置多个信息,也可以拆分成多个命令,但考虑到镜像文件层数限制,不建议这么做。

EXPOSE

设定镜像需要暴露的网络端口,在启动容器时需要使用 -p 或 -P 来使外部能够访问。用法:

1
EXPOSE <port> <port> ...
ENV

设置环境变量,用法:

1
2
ENV key value
ENV <key>=<value> <key>=<value> ...

第一种用法时,第一个字符串作为 key,后面的所有字符串作为 value。

第二种用法时,如果value中有空格,需要使用双引号包括,或添加 \ 进行转义,如 ENV myName="Zhang San" myFriend=Li\ Si

ADD

将 src 标记的文件添加到镜像添加容器中 dest 标记的路径中取,src 标记的文件可以是本地文件、目录或远程 URL 链接。用法:

  • ADD <src>... <dest>
  • ADD ["<src>",... "<dest>"] 这种方式添加的内容用双引号包括,所以可以用来处理有空格的文件路径。

src 标记的是本地文件或目录,其相对路径是相对与 Dockerfile 所在目录的路径,可以使用通配符。添加文件时需要确认文件存在于 Dockerfile 目录。

dest 是容器中的目录,如果这个目录不存在,当 ADD 命令执行时,将会在容器中自动创建此目录。dest 必须使用绝对路径,或相对与 WORKDIR 的相对路径。

COPY

用于向容器中指定路径下添加文件,在添加文件时也需要确认文件存在于 Dockerfile 目录中。用法:

  • COPY <src> ... <dest>
  • COPY ["<src>",... "<dest>"] 可处理路径带空格的文件路径。

src 、dest 的用法与 ADD命令 的相同。

ENTRYPOINT

用来设定容器运行时默认执行程序的命令。用法:

  • ENTRYPOINT ["executable","param1","param2"] 官方推荐用法,可以指定执行的二进制程序和参数。
  • ENTRYPOINT command param1 param2 命令将使用 /bin/sh -C 执行。

CMD 指令和 ENTRYPOINT 指令都是为镜像指定容器启动后的命令,都可以指定 shell 或 exec 函数调用的方式执行命令,当存在多个 CMD 指令或 ENTRYPOINT 指令时,只有最后一个生效。

他们的差异是:

  • CMD 指令指定的容器启动时命令可被 docker run 指定的命令覆盖,而 ENTRYPOINT 指令指定的命令不能被覆盖,而是将 docker run 指定的参数当作 ENTRYPOINT 指定命令的参数。
  • CMD 指令可以为 ENTRYPOINT 指令设置默认参数,而且可以被 docker run 指定的参数覆盖。
VOLUME

在容器内部创建指定名称的挂载点。用法:

1
VOLUME ["/data"]

如果在 Dockerfile 中已经声明了某个挂载点,那么以后对此挂载点中文件的操作将不会生效,因此,一般只会在 Dockerfile 的结尾处声明挂载点。

USER

切换用户身份,执行完成后,之后的所有命令将会使用新用户身份来执行。用法:

1
USER daemon
WORKDIR

切换当前工作目录,切换工作目录将会影响后续的 RUN、CMD、ENTRYPOINT、COPY 和 ADD 指令中使用的路径。

1
WORKDIR /path/to/workdir
ONBUILD

创建触发命令基,由 ONBUILD 创建的命令集在当前 Dockerfile 执行过程中不会触发,当此镜像被其他镜像当作基础镜像使用时,将会触发执行。

编译镜像

进入 Dockerfile 文件目录,使用 docker build 命令编译 Dockerfile ,通过 -t 选项给镜像指定名字(带版本号)。如:

1
docker build -t image_redis:v1.0 .

注意这个命令要指定编译的路径,最后面的 . 表示当前路径,不能忽略。

编译过程有九步,每一步对应 Dockerfile 的一个关键字,每执行完一步,都会生成一个临时文件层,当所有命令都执行成功后,docker 会记录执行过程中用到的所有文件层,并用一个名字来标记这一组文件层。这一组文件层就是镜像。

注意事项
  • 制作自己的应用镜像时,应尽量考虑使用相同的底层镜像,这样可以极大地降低后续维护的成本。

  • 在选择基础镜像时,尽可能选择适合自己的基础镜像,也可以在已有的基础镜像上进行改造,提交新镜像作为自己项目的基础镜像。

  • 基础镜像可以从 docker-hub 上寻找,如果没有合适的,就需要自己构建基础镜像。

    自定义基础镜像时,可以使用 debootstrap 工具来定制最小化的 Linux 基础镜像。

  • docker 镜像的文件层数是有上限的,如果超过上限将会导致构建失败,因此应该尽量将多个命令放到一起执行,减少文件层数。

删除镜像

删除指定镜像

1
docker rmi 镜像ID

参数说明:

  • 镜像ID 表示要删除的镜像的 ID,多个镜像 ID 之间空格隔开。

删除所有镜像

1
docker rmi -f $(docker images -q)

参数说明:

  • -f 表示强制删除,不提示确认(注意:即使有依赖容器也会尝试删除)
  • $(docker images -q) 表示获取所有镜像的 ID

删除未被容器引用的镜像

1
docker image prune -a

删除悬空镜像(即仓库名和标签都是 的废弃镜像,通常是由于构建失败或重新构建产生的残留):

1
docker image prune

删除指定标签(tag_name)的所有镜像

1
docker rmi $(docker images --filter "reference=*:tag_name" -q)

提交镜像

当镜像更新后,可以使用 docker commit 命令将修改内容提交,提交时更新镜像版本。这样就获得了新版本的镜像。

本地镜像提交后,可以使用 docker push 命令将镜像推送到远端仓库,供其他人拉取使用。

导出镜像

导出镜像是指将镜像连同所有的元数据、标签、历史记录打包成一个 .tar 文件。

导出镜像

1
docker save 镜像名:版本 -o 镜像文件名.tar

参数说明:

  • 镜像名:版本 表示要导出的镜像的名称和版本号,多个镜像 ID 之间空格隔开。
  • 镜像文件名.tar 表示导出的镜像文件名,不指定时默认导出为 镜像名.tar。

导出并压缩

1
2
3
4
5
6
7
8
9
10
11
12
13
docker save 镜像名:版本 | gzip > 镜像文件名.tar.gz
````

> 参数说明:
> - `镜像名:版本` 表示要导出的镜像的名称和版本号。
> - `镜像文件名.tar.gz` 表示导出的镜像压缩文件名。



#### 把多个镜像导出到一个 tar 文件中

```bash
docker save 镜像名1:版本1 镜像名2:版本2 -o 镜像文件名.tar

参数说明:

  • 镜像名1:版本1、镜像名2:版本2 表示要导出的镜像的名称和版本号,多个镜像名称和版本号之间用空格隔开。
  • 镜像文件名.tar 表示导出的镜像文件名。

导入镜像

从tar文件导入镜像

1
docker load -i 镜像文件名.tar

参数说明:

  • 镜像文件名.tar 表示要导入的镜像文件名,不指定时默认导入为 镜像名.tar。

导入压缩镜像文件:

导入压缩镜像文件

1
docker load -i 镜像文件名.tar.gz

参数说明:

  • 镜像文件名.tar.gz 表示要导入的镜像压缩文件名,不指定时默认导入为 镜像名.tar.gz。

参考资料

总结

镜像是 docker 的基础,所有容器都是基于镜像创建的。虽然官方仓库中有各种第三方的开源镜像供我们使用,可以满足基本的需求,但如果要深入使用 docker 来辅助开发、测试与实施,就需要了解镜像的概念与特性,学习维护镜像。