Docker从入门到实践(4-1)

使用 Docker 镜像

在以前的介绍中,咱们知道镜像是 Docker 的三大组件之一。php

Docker 运行容器前须要本地存在对应的镜像,若是本地不存在该镜像,Docker 会从镜像仓库下载该镜像。html

本章将介绍更多关于镜像的内容,包括:node

  • 从仓库获取镜像;python

  • 管理本地主机上的镜像;mysql

  • 介绍镜像实现的基本原理。nginx

获取镜像

以前提到过,Docker Hub 上有大量的高质量的镜像能够用,这里咱们就说一下怎么获取这些镜像。git

从 Docker 镜像仓库获取镜像的命令是 docker pull。其命令格式为:github

docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]

具体的选项能够经过 docker pull --help 命令看到,这里咱们说一下镜像名称的格式。golang

  • Docker 镜像仓库地址:地址的格式通常是 <域名/IP>[:端口号]。默认地址是 Docker Hub。
  • 仓库名:如以前所说,这里的仓库名是两段式名称,即 <用户名>/<软件名>。对于 Docker Hub,若是不给出用户名,则默认为 library,也就是官方镜像。

好比:web

$ docker pull ubuntu:18.04
18.04: Pulling from library/ubuntu
bf5d46315322: Pull complete
9f13e0ac480c: Pull complete
e8988b5b3097: Pull complete
40af181810e7: Pull complete
e6f7c7e5c03e: Pull complete
Digest: sha256:147913621d9cdea08853f6ba9116c2e27a3ceffecf3b492983ae97c3d643fbbe
Status: Downloaded newer image for ubuntu:18.04 

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

从下载过程当中能够看到咱们以前说起的分层存储的概念,镜像是由多层存储所构成。下载也是一层层的去下载,并不是单一文件。下载过程当中给出了每一层的 ID 的前 12 位。而且下载结束后,给出该镜像完整的 sha256 的摘要,以确保下载一致性。

在使用上面命令的时候,你可能会发现,你所看到的层 ID 以及 sha256 的摘要和这里的不同。这是由于官方镜像是一直在维护的,有任何新的 bug,或者版本更新,都会进行修复再以原来的标签发布,这样能够确保任何使用这个标签的用户能够得到更安全、更稳定的镜像。

若是从 Docker Hub 下载镜像很是缓慢,能够参照 镜像加速器 一节配置加速器。

运行

有了镜像后,咱们就可以以这个镜像为基础启动并运行一个容器。以上面的 ubuntu:18.04 为例,若是咱们打算启动里面的 bash 而且进行交互式操做的话,能够执行下面的命令。

$ docker run -it --rm \
    ubuntu:18.04 \
    bash

root@e7009c6ce357:/# cat /etc/os-release NAME="Ubuntu" VERSION="18.04.1 LTS (Bionic Beaver)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 18.04.1 LTS" VERSION_ID="18.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=bionic UBUNTU_CODENAME=bionic 

docker run 就是运行容器的命令,具体格式咱们会在 容器 一节进行详细讲解,咱们这里简要的说明一下上面用到的参数。

  • -it:这是两个参数,一个是 -i:交互式操做,一个是 -t 终端。咱们这里打算进入 bash 执行一些命令并查看返回结果,所以咱们须要交互式终端。
  • --rm:这个参数是说容器退出后随之将其删除。默认状况下,为了排障需求,退出的容器并不会当即删除,除非手动 docker rm。咱们这里只是随便执行个命令,看看结果,不须要排障和保留结果,所以使用 --rm 能够避免浪费空间。
  • ubuntu:18.04:这是指用 ubuntu:18.04 镜像为基础来启动容器。
  • bash:放在镜像名后的是 命令,这里咱们但愿有个交互式 Shell,所以用的是 bash

进入容器后,咱们能够在 Shell 下操做,执行任何所需的命令。这里,咱们执行了 cat /etc/os-release,这是 Linux 经常使用的查看当前系统版本的命令,从返回的结果能够看到容器内是 Ubuntu 18.04.1 LTS 系统。

最后咱们经过 exit 退出了这个容器。

列出镜像

要想列出已经下载下来的镜像,可使用 docker image ls 命令。

$ docker image ls
REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
redis                latest              5f515359c7f8        5 days ago          183 MB
nginx                latest              05a60462f8ba        5 days ago          181 MB
mongo                3.2                 fe9198c04d62        5 days ago          342 MB
<none>               <none>              00285df0df87        5 days ago          342 MB
ubuntu               18.04               f753707788c5        4 weeks ago         127 MB
ubuntu               latest              f753707788c5        4 weeks ago         127 MB

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

其中仓库名、标签在以前的基础概念章节已经介绍过了。镜像 ID 则是镜像的惟一标识,一个镜像能够对应多个 标签。所以,在上面的例子中,咱们能够看到 ubuntu:18.04 和 ubuntu:latest 拥有相同的 ID,由于它们对应的是同一个镜像。

镜像体积

若是仔细观察,会注意到,这里标识的所占用空间和在 Docker Hub 上看到的镜像大小不一样。好比,ubuntu:18.04 镜像大小,在这里是 127 MB,可是在 Docker Hub 显示的倒是 50 MB。这是由于 Docker Hub 中显示的体积是压缩后的体积。在镜像下载和上传过程当中镜像是保持着压缩状态的,所以 Docker Hub 所显示的大小是网络传输中更关心的流量大小。而 docker image ls 显示的是镜像下载到本地后,展开的大小,准确说,是展开后的各层所占空间的总和,由于镜像到本地后,查看空间的时候,更关心的是本地磁盘空间占用的大小。

另一个须要注意的问题是,docker image ls 列表中的镜像体积总和并不是是全部镜像实际硬盘消耗。因为 Docker 镜像是多层存储结构,而且能够继承、复用,所以不一样镜像可能会由于使用相同的基础镜像,从而拥有共同的层。因为 Docker 使用 Union FS,相同的层只须要保存一份便可,所以实际镜像硬盘占用空间极可能要比这个列表镜像大小的总和要小的多。

你能够经过如下命令来便捷的查看镜像、容器、数据卷所占用的空间。

$ docker system df

TYPE                TOTAL               ACTIVE              SIZE                RECLAIMABLE
Images              24                  0                   1.992GB             1.992GB (100%)
Containers          1                   0                   62.82MB             62.82MB (100%)
Local Volumes       9                   0                   652.2MB             652.2MB (100%)
Build Cache                                                 0B                  0B

虚悬镜像

上面的镜像列表中,还能够看到一个特殊的镜像,这个镜像既没有仓库名,也没有标签,均为 <none>。:

<none>               <none>              00285df0df87        5 days ago          342 MB

这个镜像本来是有镜像名和标签的,原来为 mongo:3.2,随着官方镜像维护,发布了新版本后,从新 docker pull mongo:3.2 时,mongo:3.2 这个镜像名被转移到了新下载的镜像身上,而旧的镜像上的这个名称则被取消,从而成为了 <none>。除了 docker pull 可能致使这种状况,docker build 也一样能够致使这种现象。因为新旧镜像同名,旧镜像名称被取消,从而出现仓库名、标签均为 <none> 的镜像。这类无标签镜像也被称为 虚悬镜像(dangling image) ,能够用下面的命令专门显示这类镜像:

$ docker image ls -f dangling=true REPOSITORY TAG IMAGE ID CREATED SIZE <none> <none> 00285df0df87 5 days ago 342 MB 

通常来讲,虚悬镜像已经失去了存在的价值,是能够随意删除的,能够用下面的命令删除。

$ docker image prune

中间层镜像

为了加速镜像构建、重复利用资源,Docker 会利用 中间层镜像。因此在使用一段时间后,可能会看到一些依赖的中间层镜像。默认的 docker image ls 列表中只会显示顶层镜像,若是但愿显示包括中间层镜像在内的全部镜像的话,须要加 -a 参数。

$ docker image ls -a 

这样会看到不少无标签的镜像,与以前的虚悬镜像不一样,这些无标签的镜像不少都是中间层镜像,是其它镜像所依赖的镜像。这些无标签镜像不该该删除,不然会致使上层镜像由于依赖丢失而出错。实际上,这些镜像也不必删除,由于以前说过,相同的层只会存一遍,而这些镜像是别的镜像的依赖,所以并不会由于它们被列出来而多存了一份,不管如何你也会须要它们。只要删除那些依赖它们的镜像后,这些依赖的中间层镜像也会被连带删除。

列出部分镜像

不加任何参数的状况下,docker image ls 会列出全部顶层镜像,可是有时候咱们只但愿列出部分镜像。docker image ls 有好几个参数能够帮助作到这个事情。

根据仓库名列出镜像

$ docker image ls ubuntu
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              18.04               f753707788c5        4 weeks ago         127 MB
ubuntu              latest              f753707788c5        4 weeks ago         127 MB

列出特定的某个镜像,也就是说指定仓库名和标签

$ docker image ls ubuntu:18.04
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              18.04               f753707788c5        4 weeks ago         127 MB

除此之外,docker image ls 还支持强大的过滤器参数 --filter,或者简写 -f。以前咱们已经看到了使用过滤器来列出虚悬镜像的用法,它还有更多的用法。好比,咱们但愿看到在 mongo:3.2 以后创建的镜像,能够用下面的命令:

$ docker image ls -f since=mongo:3.2 REPOSITORY TAG IMAGE ID CREATED SIZE redis latest 5f515359c7f8 5 days ago 183 MB nginx latest 05a60462f8ba 5 days ago 181 MB 

想查看某个位置以前的镜像也能够,只须要把 since 换成 before 便可。

此外,若是镜像构建时,定义了 LABEL,还能够经过 LABEL 来过滤。

$ docker image ls -f label=com.example.version=0.1 ... 

以特定格式显示

默认状况下,docker image ls 会输出一个完整的表格,可是咱们并不是全部时候都会须要这些内容。好比,刚才删除虚悬镜像的时候,咱们须要利用 docker image ls 把全部的虚悬镜像的 ID 列出来,而后才能够交给 docker image rm 命令做为参数来删除指定的这些镜像,这个时候就用到了 -q 参数。

$ docker image ls -q
5f515359c7f8
05a60462f8ba
fe9198c04d62
00285df0df87
f753707788c5
f753707788c5
1e0c3dd64ccd

--filter 配合 -q 产生出指定范围的 ID 列表,而后送给另外一个 docker 命令做为参数,从而针对这组实体成批的进行某种操做的作法在 Docker 命令行使用过程当中很是常见,不只仅是镜像,未来咱们会在各个命令中看到这类搭配以完成很强大的功能。所以每次在文档看到过滤器后,能够多注意一下它们的用法。

另一些时候,咱们可能只是对表格的结构不满意,但愿本身组织列;或者不但愿有标题,这样方便其它程序解析结果等,这就用到了 Go 的模板语法

好比,下面的命令会直接列出镜像结果,而且只包含镜像ID和仓库名:

$ docker image ls --format "{{.ID}}: {{.Repository}}" 5f515359c7f8: redis 05a60462f8ba: nginx fe9198c04d62: mongo 00285df0df87: <none> f753707788c5: ubuntu f753707788c5: ubuntu 1e0c3dd64ccd: ubuntu 

或者打算以表格等距显示,而且有标题行,和默认同样,不过本身定义列:



$ docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}" IMAGE ID REPOSITORY TAG 5f515359c7f8 redis latest 05a60462f8ba nginx latest fe9198c04d62 mongo 3.2 00285df0df87 <none> <none> f753707788c5 ubuntu 18.04 f753707788c5 ubuntu latest

删除本地镜像

若是要删除本地的镜像,可使用 docker image rm 命令,其格式为:

$ docker image rm [选项] <镜像1> [<镜像2> ...]

用 ID、镜像名、摘要删除镜像

其中,<镜像> 能够是 镜像短 ID镜像长 ID镜像名 或者 镜像摘要

好比咱们有这么一些镜像:

$ docker image ls
REPOSITORY                  TAG                 IMAGE ID            CREATED             SIZE
centos                      latest              0584b3d2cf6d        3 weeks ago         196.5 MB
redis                       alpine              501ad78535f0        3 weeks ago         21.03 MB
docker                      latest              cf693ec9b5c7        3 weeks ago         105.1 MB
nginx                       latest              e43d811ce2f4        5 weeks ago         181.5 MB

咱们能够用镜像的完整 ID,也称为 长 ID,来删除镜像。使用脚本的时候可能会用长 ID,可是人工输入就太累了,因此更多的时候是用 短 ID 来删除镜像。docker image ls 默认列出的就已是短 ID 了,通常取前3个字符以上,只要足够区分于别的镜像就能够了。

好比这里,若是咱们要删除 redis:alpine 镜像,能够执行:

$ docker image rm 501
Untagged: redis:alpine
Untagged: redis@sha256:f1ed3708f538b537eb9c2a7dd50dc90a706f7debd7e1196c9264edeea521a86d
Deleted: sha256:501ad78535f015d88872e13fa87a828425117e3d28075d0c117932b05bf189b7
Deleted: sha256:96167737e29ca8e9d74982ef2a0dda76ed7b430da55e321c071f0dbff8c2899b
Deleted: sha256:32770d1dcf835f192cafd6b9263b7b597a1778a403a109e2cc2ee866f74adf23
Deleted: sha256:127227698ad74a5846ff5153475e03439d96d4b1c7f2a449c7a826ef74a2d2fa
Deleted: sha256:1333ecc582459bac54e1437335c0816bc17634e131ea0cc48daa27d32c75eab3
Deleted: sha256:4fc455b921edf9c4aea207c51ab39b10b06540c8b4825ba57b3feed1668fa7c7 

咱们也能够用镜像名,也就是 <仓库名>:<标签>,来删除镜像。

$ docker image rm centos
Untagged: centos:latest
Untagged: centos@sha256:b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c
Deleted: sha256:0584b3d2cf6d235ee310cf14b54667d889887b838d3f3d3033acd70fc3c48b8a Deleted: sha256:97ca462ad9eeae25941546209454496e1d66749d53dfa2ee32bf1faabd239d38 

固然,更精确的是使用 镜像摘要 删除镜像。

$ docker image ls --digests
REPOSITORY                  TAG                 DIGEST                                                                    IMAGE ID            CREATED             SIZE
node                        slim                sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228   6e0c4c8e3913        3 weeks ago         214 MB

$ docker image rm node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228
Untagged: node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228

Untagged 和 Deleted

若是观察上面这几个命令的运行输出信息的话,你会注意到删除行为分为两类,一类是 Untagged,另外一类是 Deleted。咱们以前介绍过,镜像的惟一标识是其 ID 和摘要,而一个镜像能够有多个标签。

所以当咱们使用上面命令删除镜像的时候,其实是在要求删除某个标签的镜像。因此首先须要作的是将知足咱们要求的全部镜像标签都取消,这就是咱们看到的 Untagged 的信息。由于一个镜像能够对应多个标签,所以当咱们删除了所指定的标签后,可能还有别的标签指向了这个镜像,若是是这种状况,那么 Delete 行为就不会发生。因此并不是全部的 docker image rm 都会产生删除镜像的行为,有可能仅仅是取消了某个标签而已。

当该镜像全部的标签都被取消了,该镜像极可能会失去了存在的意义,所以会触发删除行为。镜像是多层存储结构,所以在删除的时候也是从上层向基础层方向依次进行判断删除。镜像的多层结构让镜像复用变得很是容易,所以颇有可能某个其它镜像正依赖于当前镜像的某一层。这种状况,依旧不会触发删除该层的行为。直到没有任何层依赖当前层时,才会真实的删除当前层。这就是为何,有时候会奇怪,为何明明没有别的标签指向这个镜像,可是它仍是存在的缘由,也是为何有时候会发现所删除的层数和本身 docker pull 看到的层数不同的缘由。

除了镜像依赖之外,还须要注意的是容器对镜像的依赖。若是有用这个镜像启动的容器存在(即便容器没有运行),那么一样不能够删除这个镜像。以前讲过,容器是以镜像为基础,再加一层容器存储层,组成这样的多层存储结构去运行的。所以该镜像若是被这个容器所依赖的,那么删除必然会致使故障。若是这些容器是不须要的,应该先将它们删除,而后再来删除镜像。

用 docker image ls 命令来配合

像其它能够承接多个实体的命令同样,可使用 docker image ls -q 来配合使用 docker image rm,这样能够成批的删除但愿删除的镜像。咱们在“镜像列表”章节介绍过不少过滤镜像列表的方式均可以拿过来使用。

好比,咱们须要删除全部仓库名为 redis 的镜像:

$ docker image rm $(docker image ls -q redis)

或者删除全部在 mongo:3.2 以前的镜像:

$ docker image rm $(docker image ls -q -f before=mongo:3.2) 

充分利用你的想象力和 Linux 命令行的强大,你能够完成不少很是赞的功能。

CentOS/RHEL 的用户须要注意的事项

如下内容仅适用于 Docker CE 18.09 如下版本,在 Docker CE 18.09 版本中默认使用的是 overlay2驱动。

在 Ubuntu/Debian 上有 UnionFS 可使用,如 aufs 或者 overlay2,而 CentOS 和 RHEL 的内核中没有相关驱动。所以对于这类系统,通常使用 devicemapper 驱动利用 LVM 的一些机制来模拟分层存储。这样的作法除了性能比较差外,稳定性通常也很差,并且配置相对复杂。Docker 安装在 CentOS/RHEL 上后,会默认选择 devicemapper,可是为了简化配置,其 devicemapper 是跑在一个稀疏文件模拟的块设备上,也被称为 loop-lvm。这样的选择是由于不须要额外配置就能够运行 Docker,这是自动配置惟一能作到的事情。可是 loop-lvm 的作法很是很差,其稳定性、性能更差,不管是日志仍是 docker info 中都会看到警告信息。官方文档有明确的文章讲解了如何配置块设备给 devicemapper 驱动作存储层的作法,这类作法也被称为配置 direct-lvm

除了前面说到的问题外,devicemapper + loop-lvm 还有一个缺陷,由于它是稀疏文件,因此它会不断增加。用户在使用过程当中会注意到 /var/lib/docker/devicemapper/devicemapper/data 不断增加,并且没法控制。不少人会但愿删除镜像或者能够解决这个问题,结果发现效果并不明显。缘由就是这个稀疏文件的空间释放后基本不进行垃圾回收的问题。所以每每会出现即便删除了文件内容,空间却没法回收,随着使用这个稀疏文件一直在不断增加。

因此对于 CentOS/RHEL 的用户来讲,在没有办法使用 UnionFS 的状况下,必定要配置 direct-lvm 给 devicemapper,不管是为了性能、稳定性仍是空间利用率。

或许有人注意到了 CentOS 7 中存在被 backports 回来的 overlay 驱动,不过 CentOS 里的这个驱动达不到生产环境使用的稳定程度,因此不推荐使用。

利用 commit 理解镜像构成

注意:若是您是初学者,您能够暂时跳事后面的内容,直接学习 容器 一节。

注意: docker commit 命令除了学习以外,还有一些特殊的应用场合,好比被入侵后保存现场等。可是,不要使用 docker commit 定制镜像,定制镜像应该使用 Dockerfile 来完成。若是你想要定制镜像请查看下一小节。

镜像是容器的基础,每次执行 docker run 的时候都会指定哪一个镜像做为容器运行的基础。在以前的例子中,咱们所使用的都是来自于 Docker Hub 的镜像。直接使用这些镜像是能够知足必定的需求,而当这些镜像没法直接知足需求时,咱们就须要定制这些镜像。接下来的几节就将讲解如何定制镜像。

回顾一下以前咱们学到的知识,镜像是多层存储,每一层是在前一层的基础上进行的修改;而容器一样也是多层存储,是在以镜像为基础层,在其基础上加一层做为容器运行时的存储层。

如今让咱们以定制一个 Web 服务器为例子,来说解镜像是如何构建的。

$ docker run --name webserver -d -p 80:80 nginx 

这条命令会用 nginx 镜像启动一个容器,命名为 webserver,而且映射了 80 端口,这样咱们能够用浏览器去访问这个 nginx 服务器。

若是是在 Linux 本机运行的 Docker,或者若是使用的是 Docker Desktop for Mac/Windows,那么能够直接访问:http://localhost;若是使用的是 Docker Toolbox,或者是在虚拟机、云服务器上安装的 Docker,则须要将 localhost 换为虚拟机地址或者实际云服务器地址。

直接用浏览器访问的话,咱们会看到默认的 Nginx 欢迎页面。

如今,假设咱们很是不喜欢这个欢迎页面,咱们但愿改为欢迎 Docker 的文字,咱们可使用 docker exec命令进入容器,修改其内容。

$ docker exec -it webserver bash root@3729b97e8226:/# echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html root@3729b97e8226:/# exit exit 

咱们以交互式终端方式进入 webserver 容器,并执行了 bash 命令,也就是得到一个可操做的 Shell。

而后,咱们用 <h1>Hello, Docker!</h1> 覆盖了 /usr/share/nginx/html/index.html 的内容。

如今咱们再刷新浏览器的话,会发现内容被改变了。

咱们修改了容器的文件,也就是改动了容器的存储层。咱们能够经过 docker diff 命令看到具体的改动。

$ docker diff webserver
C /root
A /root/.bash_history C /run C /usr C /usr/share C /usr/share/nginx C /usr/share/nginx/html C /usr/share/nginx/html/index.html C /var C /var/cache C /var/cache/nginx A /var/cache/nginx/client_temp A /var/cache/nginx/fastcgi_temp A /var/cache/nginx/proxy_temp A /var/cache/nginx/scgi_temp A /var/cache/nginx/uwsgi_temp 

如今咱们定制好了变化,咱们但愿能将其保存下来造成镜像。

要知道,当咱们运行一个容器的时候(若是不使用卷的话),咱们作的任何文件修改都会被记录于容器存储层里。而 Docker 提供了一个 docker commit 命令,能够将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。之后咱们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。

docker commit 的语法格式为:

docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]

咱们能够用下面的命令将容器保存为镜像:

$ docker commit \
    --author "Tao Wang <twang2218@gmail.com>" \ --message "修改了默认网页" \ webserver \ nginx:v2 sha256:07e33465974800ce65751acc279adc6ed2dc5ed4e0838f8b86f0c87aa1795214 

其中 --author 是指定修改的做者,而 --message 则是记录本次修改的内容。这点和 git 版本控制类似,不过这里这些信息能够省略留空。

咱们能够在 docker image ls 中看到这个新定制的镜像:

$ docker image ls nginx
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               v2                  07e334659748        9 seconds ago       181.5 MB
nginx               1.11                05a60462f8ba        12 days ago         181.5 MB
nginx               latest              e43d811ce2f4        4 weeks ago         181.5 MB

咱们还能够用 docker history 具体查看镜像内的历史记录,若是比较 nginx:latest 的历史记录,咱们会发现新增了咱们刚刚提交的这一层。

$ docker history nginx:v2 IMAGE CREATED CREATED BY SIZE COMMENT 07e334659748 54 seconds ago nginx -g daemon off; 95 B 修改了默认网页 e43d811ce2f4 4 weeks ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon 0 B <missing> 4 weeks ago /bin/sh -c #(nop) EXPOSE 443/tcp 80/tcp 0 B <missing> 4 weeks ago /bin/sh -c ln -sf /dev/stdout /var/log/nginx/ 22 B <missing> 4 weeks ago /bin/sh -c apt-key adv --keyserver hkp://pgp. 58.46 MB <missing> 4 weeks ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.11.5-1 0 B <missing> 4 weeks ago /bin/sh -c #(nop) MAINTAINER NGINX Docker Ma 0 B <missing> 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B <missing> 4 weeks ago /bin/sh -c #(nop) ADD file:23aa4f893e3288698c 123 MB 

新的镜像定制好后,咱们能够来运行这个镜像。

docker run --name web2 -d -p 81:80 nginx:v2 

这里咱们命名为新的服务为 web2,而且映射到 81 端口。若是是 Docker Desktop for Mac/Windows 或 Linux 桌面的话,咱们就能够直接访问 http://localhost:81 看到结果,其内容应该和以前修改后的 webserver 同样。

至此,咱们第一次完成了定制镜像,使用的是 docker commit 命令,手动操做给旧的镜像添加了新的一层,造成新的镜像,对镜像多层存储应该有了更直观的感受。

慎用 docker commit

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

首先,若是仔细观察以前的 docker diff webserver 的结果,你会发现除了真正想要修改的 /usr/share/nginx/html/index.html 文件外,因为命令的执行,还有不少文件被改动或添加了。这还仅仅是最简单的操做,若是是安装软件包、编译构建,那会有大量的无关内容被添加进来,若是不当心清理,将会致使镜像极为臃肿。

此外,使用 docker commit 意味着全部对镜像的操做都是黑箱操做,生成的镜像也被称为 黑箱镜像,换句话说,就是除了制做镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。并且,即便是这个制做镜像的人,过一段时间后也没法记清具体在操做的。虽然 docker diff 或许能够告诉获得一些线索,可是远远不到能够确保生成一致镜像的地步。这种黑箱镜像的维护工做是很是痛苦的。

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

使用 Dockerfile 定制镜像

从刚才的 docker commit 的学习中,咱们能够了解到,镜像的定制实际上就是定制每一层所添加的配置、文件。若是咱们能够把每一层修改、安装、构建、操做的命令都写入一个脚本,用这个脚原本构建、定制镜像,那么以前说起的没法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。

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

还以以前定制 nginx 镜像为例,此次咱们使用 Dockerfile 来定制。

在一个空白目录中,创建一个文本文件,并命名为 Dockerfile

$ mkdir mynginx
$ cd mynginx $ touch Dockerfile 

其内容为:

FROM nginx RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html 

这个 Dockerfile 很简单,一共就两行。涉及到了两条指令,FROM 和 RUN

FROM 指定基础镜像

所谓定制镜像,那必定是以一个镜像为基础,在其上进行定制。就像咱们以前运行了一个 nginx 镜像的容器,再进行修改同样,基础镜像是必须指定的。而 FROM 就是指定 基础镜像,所以一个 Dockerfile 中 FROM 是必备的指令,而且必须是第一条指令。

在 Docker Hub 上有很是多的高质量的官方镜像,有能够直接拿来使用的服务类的镜像,如 nginxredismongomysqlhttpdphptomcat 等;也有一些方便开发、构建、运行各类语言应用的镜像,如 nodeopenjdkpythonrubygolang 等。能够在其中寻找一个最符合咱们最终目标的镜像为基础镜像进行定制。

若是没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操做系统镜像,如 ubuntudebiancentosfedoraalpine 等,这些操做系统的软件库为咱们提供了更广阔的扩展空间。

除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

FROM scratch ... 

若是你以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将做为镜像第一层开始存在。

不以任何系统为基础,直接将可执行文件复制进镜像的作法并不罕见,好比 swarmetcd。对于 Linux 下静态编译的程序来讲,并不须要有操做系统提供运行时支持,所需的一切库都已经在可执行文件里了,所以直接 FROM scratch 会让镜像体积更加小巧。使用 Go 语言 开发的应用不少会使用这种方式来制做镜像,这也是为何有人认为 Go 是特别适合容器微服务架构的语言的缘由之一。

RUN 执行命令

RUN 指令是用来执行命令行命令的。因为命令行的强大能力,RUN 指令在定制镜像时是最经常使用的指令之一。其格式有两种:

  • shell 格式:RUN <命令>,就像直接在命令行中输入的命令同样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式。
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html 
  • exec 格式:RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式。

既然 RUN 就像 Shell 脚本同样能够执行命令,那么咱们是否就能够像 Shell 脚本同样把每一个命令对应一个 RUN 呢?好比这样:

FROM debian:stretch RUN apt-get update RUN apt-get install -y gcc libc6-dev make wget RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" RUN mkdir -p /usr/src/redis RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 RUN make -C /usr/src/redis RUN make -C /usr/src/redis install 

以前说过,Dockerfile 中每个指令都会创建一层,RUN 也不例外。每个 RUN 的行为,就和刚才咱们手工创建镜像的过程同样:新创建一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。

而上面的这种写法,建立了 7 层镜像。这是彻底没有意义的,并且不少运行时不须要的东西,都被装进了镜像里,好比编译环境、更新的软件包等等。结果就是产生很是臃肿、很是多层的镜像,不只仅增长了构建部署的时间,也很容易出错。 这是不少初学 Docker 的人常犯的一个错误。

Union FS 是有最大层数限制的,好比 AUFS,曾经是最大不得超过 42 层,如今是不得超过 127 层。

上面的 Dockerfile 正确的写法应该是这样:

FROM debian:stretch RUN buildDeps='gcc libc6-dev make wget' \ && apt-get update \ && apt-get install -y $buildDeps \ && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \ && mkdir -p /usr/src/redis \ && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \ && make -C /usr/src/redis \ && make -C /usr/src/redis install \ && rm -rf /var/lib/apt/lists/* \ && rm redis.tar.gz \ && rm -r /usr/src/redis \ && apt-get purge -y --auto-remove $buildDeps 

首先,以前全部的命令只有一个目的,就是编译、安装 redis 可执行文件。所以没有必要创建不少层,这只是一层的事情。所以,这里没有使用不少个 RUN 对一一对应不一样的命令,而是仅仅使用一个 RUN 指令,并使用 && 将各个所需命令串联起来。将以前的 7 层,简化为了 1 层。在撰写 Dockerfile 的时候,要常常提醒本身,这并非在写 Shell 脚本,而是在定义每一层该如何构建。

而且,这里为了格式化还进行了换行。Dockerfile 支持 Shell 类的行尾添加 \ 的命令换行方式,以及行首 # 进行注释的格式。良好的格式,好比换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。

此外,还能够看到这一组命令的最后添加了清理工做的命令,删除了为了编译构建所须要的软件,清理了全部下载、展开的文件,而且还清理了 apt 缓存文件。这是很重要的一步,咱们以前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。所以镜像构建时,必定要确保每一层只添加真正须要添加的东西,任何无关的东西都应该清理掉。

不少人初学 Docker 制做出了很臃肿的镜像的缘由之一,就是忘记了每一层构建的最后必定要清理掉无关文件。

构建镜像

好了,让咱们再回到以前定制的 nginx 镜像的 Dockerfile 来。如今咱们明白了这个 Dockerfile 的内容,那么让咱们来构建这个镜像吧。

在 Dockerfile 文件所在目录执行:

$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM nginx
 ---> e43d811ce2f4
Step 2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html ---> Running in 9cdc27646c7b ---> 44aa4490ce2c Removing intermediate container 9cdc27646c7b Successfully built 44aa4490ce2c 

从命令的输出结果中,咱们能够清晰的看到镜像的构建过程。在 Step 2 中,如同咱们以前所说的那样,RUN 指令启动了一个容器 9cdc27646c7b,执行了所要求的命令,并最后提交了这一层 44aa4490ce2c,随后删除了所用到的这个容器 9cdc27646c7b

这里咱们使用了 docker build 命令进行镜像构建。其格式为:

docker build [选项] <上下文路径/URL/->

在这里咱们指定了最终镜像的名称 -t nginx:v3,构建成功后,咱们能够像以前运行 nginx:v2 那样来运行这个镜像,其结果会和 nginx:v2 同样。

镜像构建上下文(Context)

若是注意,会看到 docker build 命令最后有一个 .. 表示当前目录,而 Dockerfile 就在当前目录,所以很多初学者觉得这个路径是在指定 Dockerfile 所在路径,这么理解实际上是不许确的。若是对应上面的命令格式,你可能会发现,这是在指定 上下文路径。那么什么是上下文呢?

首先咱们要理解 docker build 的工做原理。Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如 docker 命令这样的客户端工具,则是经过这组 API 与 Docker 引擎交互,从而完成各类功能。所以,虽然表面上咱们好像是在本机执行各类 docker 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也由于这种 C/S 设计,让咱们操做远程服务器的 Docker 引擎变得垂手可得。

当咱们进行镜像构建的时候,并不是全部定制都会经过 RUN 指令完成,常常会须要将一些本地文件复制进镜像,好比经过 COPY 指令、ADD 指令等。而 docker build 命令构建镜像,其实并不是在本地构建,而是在服务端,也就是 Docker 引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端得到本地文件呢?

这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的全部内容打包,而后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会得到构建镜像所需的一切文件。

若是在 Dockerfile 中这么写:

COPY ./package.json /app/ 

这并非要复制执行 docker build 命令所在的目录下的 package.json,也不是复制 Dockerfile 所在目录下的 package.json,而是复制 上下文(context) 目录下的 package.json

所以,COPY 这类指令中的源文件的路径都是相对路径。这也是初学者常常会问的为何 COPY ../package.json /app 或者 COPY /opt/xxxx /app 没法工做的缘由,由于这些路径已经超出了上下文的范围,Docker 引擎没法得到这些位置的文件。若是真的须要那些文件,应该将它们复制到上下文目录中去。

如今就能够理解刚才的命令 docker build -t nginx:v3 . 中的这个 .,其实是在指定上下文的目录,docker build 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。

若是观察 docker build 输出,咱们其实已经看到了这个发送上下文的过程:

$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
...

理解构建上下文对于镜像构建是很重要的,避免犯一些不该该的错误。好比有些初学者在发现 COPY /opt/xxxx /app 不工做后,因而干脆将 Dockerfile 放到了硬盘根目录去构建,结果发现 docker build执行后,在发送一个几十 GB 的东西,极为缓慢并且很容易构建失败。那是由于这种作法是在让 docker build 打包整个硬盘,这显然是使用错误。

通常来讲,应该会将 Dockerfile 置于一个空目录下,或者项目根目录下。若是该目录下没有所需文件,那么应该把所需文件复制一份过来。若是目录下有些东西确实不但愿构建时传给 Docker 引擎,那么能够用 .gitignore 同样的语法写一个 .dockerignore,该文件是用于剔除不须要做为上下文传递给 Docker 引擎的。

那么为何会有人误觉得 . 是指定 Dockerfile 所在目录呢?这是由于在默认状况下,若是不额外指定 Dockerfile 的话,会将上下文目录下的名为 Dockerfile 的文件做为 Dockerfile。

这只是默认行为,实际上 Dockerfile 的文件名并不要求必须为 Dockerfile,并且并不要求必须位于上下文目录中,好比能够用 -f ../Dockerfile.php 参数指定某个文件做为 Dockerfile

固然,通常你们习惯性的会使用默认的文件名 Dockerfile,以及会将其置于镜像构建上下文目录中。

其它 docker build 的用法

直接用 Git repo 进行构建

或许你已经注意到了,docker build 还支持从 URL 构建,好比能够直接从 Git repo 中构建:

$ docker build https://github.com/twang2218/gitlab-ce-zh.git#:11.1 Sending build context to Docker daemon 2.048 kB Step 1 : FROM gitlab/gitlab-ce:11.1.0-ce.0 11.1.0-ce.0: Pulling from gitlab/gitlab-ce aed15891ba52: Already exists 773ae8583d14: Already exists ... 

这行命令指定了构建所需的 Git repo,而且指定默认的 master 分支,构建目录为 /11.1/,而后 Docker 就会本身去 git clone 这个项目、切换到指定分支、并进入到指定目录后开始构建。

用给定的 tar 压缩包构建

$ docker build http://server/context.tar.gz

若是所给出的 URL 不是个 Git repo,而是个 tar 压缩包,那么 Docker 引擎会下载这个包,并自动解压缩,以其做为上下文,开始构建。

从标准输入中读取 Dockerfile 进行构建

docker build - < Dockerfile

cat Dockerfile | docker build -

若是标准输入传入的是文本文件,则将其视为 Dockerfile,并开始构建。这种形式因为直接从标准输入中读取 Dockerfile 的内容,它没有上下文,所以不能够像其余方法那样能够将本地文件 COPY 进镜像之类的事情。

从标准输入中读取上下文压缩包进行构建

$ docker build - < context.tar.gz

若是发现标准输入的文件格式是 gzipbzip2 以及 xz 的话,将会使其为上下文压缩包,直接将其展开,将里面视为上下文,并开始构建。

相关文章
相关标签/搜索