Docker 运行容器前需要本地存在对应的镜像,如果镜像不存在,Docker 会尝试先从默认镜像仓库下载(默认使用 Docker Hub 公共注册服务器中的仓库),用户也可以通过配置,使用自定义的镜像仓库。

使用pull获取镜像

从 Docker Hub 镜像源来下载镜像的命令格式是:

  1. docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]
  • Docker 镜像仓库地址:地址的格式一般是 <域名/IP>[:端口号]。默认地址是 Docker Hub(docker.io),可以省略;
  • NAME 是镜像仓库名称(用来区分镜像);
  • TAG 是镜像的标签(往往用来表示版本信息)。如果不显式指定TAG,则默认会选择latest标签,这会下载仓库中最新版本的镜像;

通常情况下,描述一个镜像需要包括“名称+标签”信息。

例如,获取一个 Ubuntu 18.04 系统的基础镜像可以使用如下的命令:

  1. > docker pull ubuntu:18.04
  2. > docker pull xxx.com/ubuntu:18.04 # 从xxx.com源下载ubuntu:18.04镜像

image.png

上面的命令中没有给出 Docker 镜像仓库地址,因此将会从 Docker Hub 获取镜像。而镜像名称是 ubuntu:18.04,因此将会获取官方镜像 library/ubuntu 仓库中标签为 18.04 的镜像。

从下载过程中可以看到我们之前提及的分层存储的概念,镜像是由多层存储所构成,下载也是一层层的去下载,并非单一文件。

下载过程中给出了每一层的 ID 的前 12 位,bf5d46315322 这样的串是层的唯一id(实际上完整的id包括256比特,64个十六进制字符组成)。使用 docker pull 命令下载中会获取并输出镜像的各层信息。当不同的镜像包括相同的层时,本地只会存储层的一份内容,减小了存储空间;并且下载结束后,给出该镜像完整的 sha256 的摘要,以确保下载一致性。

pull 子命令支持的选项主要包括:

  • -a, —all-tags=true|false:是否获取仓库中的所有镜像,默认为否;
  • —disable-content-trust:取消镜像的内容校验,默认为真。

有时需要使用镜像代理服务来加速 Docker 镜像获取过程,可以在 Docker 服务启动配置中增加 —registry-mirror=proxy_URL 来指定镜像代理服务地址(如https://registry.docker-cn.com)。

下载镜像到本地后,即可随时使用该镜像了,例如利用该镜像创建一个容器,在其中运行 bash 应用,执行打印“Hello World”命令:

  1. > docker run -it --rm ubuntu:18.04 bash
  2. root@e7009c6ce357:/# echo "hello world"
  3. hello world
  4. root@e7009c6ce357:/# exit
  • docker run 就是运行容器的命令;
  • -it:这是两个参数,一个是 -i:交互式操作,一个是 -t 终端。我们这里打算进入 bash 执行一些命令并查看返回结果,因此我们需要交互式终端。
  • --rm:表示容器退出后随之将其删除。默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动 docker rm。我们这里只是随便执行个命令,看看结果,不需要排障和保留结果,因此使用 --rm 可以避免浪费空间。
  • ubuntu:18.04:这是指用 ubuntu:18.04 镜像为基础来启动容器。
  • bash:放在镜像名后的是 命令,这里我们希望有个交互式 Shell,因此用的是 bash

进入容器后,我们可以在 Shell 下操作,执行任何所需的命令。这里我们执行了 echo “hello world”,终端输出了“hello world”。

最后我们通过 exit 退出了这个容器。
image.png

使用ls查看镜像信息

查看已下载的镜像信息

**
使用 docker imagesdocker image ls 命令可以列出本地主机上已有镜像的基本信息。格式如下:

  1. docker images [OPTIONS] [REPOSITORY[:TAG]]

示例如下:

  1. > docker image
  2. # or
  3. > docker image ls

image.png

列表包含了 仓库名标签镜像 ID创建时间 以及 所占用的空间

镜像 ID 则是镜像的唯一标识,一个镜像可以对应多个标签。因此,在上面的例子中,我们可以看到 hello-world:latesthello-world:linux 拥有相同的 ID,因为它们对应的是同一个镜像。

镜像大小信息只是表示了该镜像的逻辑体积大小,实际上由于相同的镜像层本地只会存储一份,物理上占用的存储空间会小于各镜像逻辑体积之和。

images 子命令主要支持如下选项,用户可以自行进行尝试:

  • -a, —all=true|false:列出所有(包括临时文件)镜像文件,默认为否;
  • —digests=true|false:列出镜像的数字摘要值,默认为否;
  • -f, —filter=[]:过滤列出的镜像,如 dangling=true 只显示没有被使用的镜像;也可指定带有特定标注的镜像等;
  • —format “TEMPLATE”:控制输出格式,如 .ID代表ID信息,.Repository代表仓库信息等;
  • —no-trunc=true|false:对输出结果中太长的部分是否进行截断,如镜像的ID信息,默认为是;
  • -q, —quiet=true|false:仅输出ID信息,默认为否。

更多子命令选项还可以通过 man docker-images 来查看。

示例如下

  1. > docker image ls hello-world # 根据仓库名列出镜像
  2. > docker image ls ubuntu:18.04 # 列出特定的某个镜像(指定仓库名和标签)

docker image ls 还支持强大的过滤器参数 --filter。比如,我们希望看到在 hello-world 之后建立的镜像,可以用下面的命令:

  1. > docker image ls -f since=hello-world

想查看某个位置之前的镜像也可以,只需要把 since 换成 before 即可。

另外一些时候,我们可能只是对表格的结构不满意,希望自己组织列;或者不希望有标题,这样方便其它程序解析结果等,这就用到了 Go 的模板语法。比如,下面的命令会直接列出镜像结果,并且只包含镜像ID和仓库名:

  1. > docker image ls --format "{{.ID}}: {{.Repository}}"
  2. > docker image ls -q

或者打算以表格等距显示,并且有标题行,和默认一样,不过自己定义列:

  1. > docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}"

image.png

使用tag命令添加标签

为了方便在后续工作中使用特定镜像,还可以使用 docker tag 命令来为本地镜像任意添加新的标签。例如,添加一个新的 my-hello-world:linux 镜像标签:

  1. > docker tag hello-world:linux my-hello-world:linux
  2. > docker image ls

image.png

之后,用户就可以直接使用 my-hello-world:linux 来表示这个镜像了。

使用inspect命令查看详细信息

使用 docker[image] inspect 命令可以获取该镜像的详细信息,包括制作者、适应架构、各层的数字摘要等。

  1. > docker image inspect my-hello-world:linux
  2. > docker image inspect -f {{".RepoTags"}}

内容太多,这里就不上截图了。有兴趣可自行测试。

ocker image inspect 返回的是一个JSON格式的消息。如果我们只要其中一项内容时,可以使用 -f 来指定,例如,获取镜像的 RepoTags。
image.png

使用history命令查看镜像历史

既然镜像文件由多个层组成,那么怎么知道各个层的内容具体是什么呢?这时候可以使用 history 子命令,该命令将列出各层的创建信息。

例如,查看 hello-world:latest 镜像的创建过程,可以使用如下命令:

  1. > docker history hello-world:latest

image.png

使用search搜寻镜像

使用 docker search 命令可以搜索 Docker Hub 官方仓库中的镜像。语法为 :

  1. > docker search [option] keyword

支持的命令选项主要包括:

  • -f, —filter filter:过滤输出内容;
  • —format string:格式化输出内容;
  • —limit int:限制输出结果个数,默认为25个;
  • —no-trunc:不截断输出结果。

搜索官方提供的带 nginx 关键字的镜像,如下所示:

  1. > docker search --filter=is-official=true nginx

image.png

用rm删除和prune清理镜像

rm删除镜像

如果想要删除本地的镜像,可以使用:

  1. docker image rm [选项] <镜像1> [<镜像2> ...]
  2. or
  3. docker image rm [选项] <镜像ID1> [<镜像ID2> ...]

支持的选项:

  • -f, -force:强制删除镜像,即使有容器依赖它;
  • -no-prune:不要清理未带标签的父镜像。

:::info 提示:如果一个镜像有多个标签,执行 rm 删除只会删除该镜像指定标签;如果镜像只有一个标签,执行 rm 会删除该镜像。 :::

要删除掉 my-hello-world:linux 镜像,可以使用如下命令:

  1. > docker rmi my-hello-world:linux # 仅删除标签

image.png

注意,当有该镜像创建的容器存在时,镜像文件默认是无法被删除的,这时需要先删除依赖该镜像的所有容器,接着再删除镜像。

:::danger 虽然使用 -f 参数可以强制删除有容器依赖的镜像,但是并不推荐这种做法。 :::

image.png

如果观察上面这几个命令的运行输出信息的话,你会注意到删除行为分为两类,一类是 Untagged,另一类是 Deleted。前面说过,镜像的唯一标识是其 ID 和摘要,而一个镜像可以有多个标签。

而当我们执行命令删除时,首先需要做的是将满足我们要求的所有镜像标签都取消,这就是我们看到的 Untagged 的信息,这时可能还有别的标签指向了这个镜像,如果是这种情况,那么 Delete 行为就不会发生。

当该镜像所有的标签都被取消了,该镜像很可能会失去了存在的意义,因此会触发删除行为。镜像是多层存储结构,因此在删除的时候也是从上层向基础层方向依次进行判断删除。镜像的多层结构让镜像复用变得非常容易,因此很有可能某个其它镜像正依赖于当前镜像的某一层。这种情况,依旧不会触发删除该层的行为。直到没有任何层依赖当前层时,才会真实的删除当前层。
> 除了镜像依赖以外,还需要注意的是容器对镜像的依赖。如果有用这个镜像启动的容器存在(即使容器没有运行),那么同样不可以删除这个镜像。> 如果这些容器是不需要的,应该先将它们删除,然后再来删除镜像。

prune清理镜像

使用 Docker 一段时间后,系统中可能会遗留一些临时的镜像文件,以及一些没有被使用的镜像,可以通过 docker image prune 命令来进行清理。支持的选项:

  • -a, -all:删除所有无用镜像,不光是临时镜像;
  • -filter filter:只清理符合给定过滤器的镜像;
  • -f, -force:强制删除镜像,而不进行提示确认。

创建镜像

创建镜像的三种方式:基于已有镜像的容器创建(commit)、基于本地模板导入(import)、基于 Dockerfile 创建(build)。

commit方式创建


镜像是多层存储,每一层是在前一层的基础上进行的修改;而容器同样也是多层存储,是在以镜像为基础层,在其基础上加一层作为容器运行时的存储层。**

下面演示通过一个例子来说明如何通过 commit 构建镜像。

首先启动一个镜像,命令为 test,并进入 shell 界面,其中 4faca687ffd9 为容器ID,test 是容器名称。

  1. > docker run --name test -it ubuntu:18.04 /bin/bash # 启动镜像

接着我们往该容器中添加一个新文件,假设为 test.txt,执行 exit 退出。这时我们修改了容器的文件,也就是改动了容器的存储层。我们可以通过 docker diff 命令看到具体的改动。

  1. root@4faca687ffd9:/# cd /tmp/
  2. root@4faca687ffd9:/tmp# touch test.txt
  3. root@4faca687ffd9:/tmp# exit

image.png

这里可以使用 docker diff {containerName} 查看具体改动。
image.png

现在我们定制了变化,希望保存下来形成镜像。而上面我们说过,我们在容器上做的任何操作都是记录在存储层里。Docker 提供了一个 docker commit 命令,可以将容器的存储层保存下来成为镜像。也就是说,在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。

docker commit 语法格式为 docker [container] commit [OPTIONS] CONTAINER [REPOSITORY [:TAG]],主要选项包括:

  • -a, —author=””:作者信息;
  • -c, —change=[]:提交的时候执行 Dockerfile 指令,包括CMD|ENTRYPOINT|ENV|EXPOSE|LABEL|ONBUILD|USER|VOLUME|WORKDIR等;
  • -m, —message=””:提交消息;
  • -p, —pause=true:提交时暂停容器运行。
  1. > docker commit --author "cimi" --message "添加test.txt文件" test test:v2
  2. > docker image ls # 查看新镜像

image.png

还可以用 docker history 具体查看镜像内的历史记录,可以看到我们的新增层。之后我们就可以使用这个新的镜像了。
image.png

使用 docker commit 命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。

首先,如果仔细观察之前的 docker diff test 的结果,你会发现除了真正想要修改的 /tmp/test.txt 文件外,由于命令的执行,还有很多文件被改动或添加了。这还仅仅是最简单的操作,如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,将会导致镜像极为臃肿。

此外,使用 docker commit 意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为 黑箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具体的操作。这种黑箱镜像的维护工作是非常痛苦的。

而且,回顾之前提及的镜像所使用的分层存储的概念,除当前层外,之前的每一层都是不会发生改变的,换句话说,任何修改的结果仅仅是在当前层进行标记、添加、修改,而不会改动上一层。如果使用 docker commit 制作镜像,以及后期修改的话,每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,即使根本无法访问到。这会让镜像更加臃肿。

import方式导入

用户可以从一个操作系统模板文件导入一个镜像。命令格式为:

  1. docker [image] import [OPTIONS] file|URL|-[REPOSITORY [:TAG]]

可以使用 OpenVZ 或其他导出的镜像的模板来导入一个镜像。OPENVZ 模板的下载地址为http://openvz.org/Download/templates/precreated

例如,下载了 ubuntu-18.04 的模板压缩包,之后使用以下命令导入即可:

  1. > cd /tmp
  2. > wget http://download.openvz.org/template/precreated/ubuntu-14.04-x86_64-minimal.tar.gz
  3. > cat ubuntu-14.04-x86_64-minimal.tar.gz | docker import - ubuntu:14.04
  4. > docker image ls # 查看镜像

image.png

Dockerfile构建

docker commit 中我们了解镜像的定制就是定制每一层的配置。同样我们也可以将定制内容写入脚本,通过脚本来构建定制镜像,这个脚本就是 Dockerfile。

Dockerfile 是一个文本文件,其内包含了一条条的指令,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

还以 commit 的例子为例,实践如何构建镜像。创建一个目录,并新建一个 Dockerfile。

  1. > cd /tmp
  2. > mkdir docker-test
  3. > docker-test
  4. > touch Dockerfile

Dockerfile 内容如下:

  1. # 指定 Docker 镜像
  2. FROM ubuntu:14.04
  3. # 在test.txt中写入一些内容
  4. RUN echo 'test ubuntu:14.04' > /tmp/test.txt

测试如下:
image.png

关于 Dockerfile 的更多配置将在 Dockerfile文件配置 一节说明。

用save存出和load载入镜像

save存出镜像

如果要导出镜像到本地,使用 docker [image] save 。支持的选项是:

  • -o,—output:导出镜像到指定位置。

例如,将 test:v2 导出到 /tmp 目录下。

  1. > docker image ls
  2. > docker image save --output /tmp/ubuntu-test.tar test:v2
  3. > ls /tmp

image.png

load载入镜像

将导出的 tar 文件再导入到本地镜像库,使用 docker [image] load。支持选项:

  • -i、—input:从指定文件中读入镜像内容。

将上面例子存出的镜像载入。

  1. > docker image ls
  2. > docker image load --input /tmp/ubuntu-test.tar
  3. or
  4. > docker image load < /tmp/ubuntu-test.tar

导入成功后,可以使用 docker images 命令进行查看,与原镜像一致。
image.png

用push上传镜像

用户可以使用 docker [image] push 命令上传镜像到仓库,默认上传到 Docker Hub官方仓库(需要登录)。命令格式如下:

  1. docker [image] push NAME[:TAG] | [REGISTRY_HOST[:REGISTRY_PORT]/]NAME[:TAG]

在 Docker Hub 网站注册后可以上传自制的镜像。例如,先添加新的标签 ${username}/hello-world:v2,然后用 docker [image] push 命令上传镜像,这里以 hello-world:latest 为例:

  1. > docker images
  2. > docker tag hello-world:latest ${username}/hello-world:v2
  3. > docker login
  4. > docker push ${username}/hello-world:v2

最终就可以在网站上查看提交记录。
image.png
image.png

第一次上传时,会提示输入登录信息或进行注册,之后登录信息会记录到本地 ~/.docker 目录下。

【填坑记录】

  1. 1、报错一:denied: requested access to the resource is denied
  2. 解决方法思路:
  3. 必须打上用户信息tag,即 ${username}/hello-world:v2
  4. docker hub 上需要注册对应 tag名称的仓库
  5. docker push 之前需要先通过 docker login 登录
  6. 2、报错二:net/http: TLS handshake timeout
  7. 解决方式:修改 /etc/docker/daemon.json 文件并添加上 registry-mirrors 键值:
  8. {
  9. "registry-mirrors": [
  10. "https://registry.docker-cn.com",
  11. ...
  12. ]
  13. }

to be continue…