镜像与容器
Docker 镜像
如果进行形象的表述,我们可以将 Docker 镜像理解为包含应用程序以及其相关依赖的一个基础文件系统,在 Docker 容器启动的过程中,它以只读的方式被用于创建容器的运行环境。
从另一个角度看,Docker 镜像其实是由基于 UnionFS 文件系统的一组镜像层依次挂载而得,而每个镜像层包含的其实是对上一镜像层的修改,这些修改其实是发生在容器运行的过程中的。所以,我们也可以反过来理解,镜像是对容器运行环境进行持久化存储的结果。
深入镜像实现
与其他虚拟机的镜像管理不同,Docker 将镜像管理纳入到了自身设计之中,也就是说,所有的 Docker 镜像都是按照 Docker 所设定的逻辑打包的,也是受到 Docker Engine 所控制的。Docker 的镜像我们必须通过 Docker 来打包,也必须通过 Docker 下载或导入后使用,不能单独直接恢复成容器中的文件系统。对于每一个记录文件系统修改的镜像层来说,Docker 都会根据它们的信息生成了一个 Hash 码,这是一个 64 长度的字符串,足以保证全球唯一性。这种编码的形式在 Docker 很多地方都有体现,之后我们会经常见到。由于镜像层都有唯一的编码,我们就能够区分不同的镜像层并能保证它们的内容与编码是一致的,这带来了另一项好处,就是允许我们在镜像之间共享镜像层。
查看镜像
镜像是由 Docker 进行管理的,所以它们的存储位置和存储方式等我们并不需要过多的关心,我们只需要利用 Docker 所提供的一些接口或命令对它们进行控制即可。
如果要查看当前连接的 docker daemon 中存放和管理了哪些镜像,我们可以使用 docker images
这个命令 ( Linux、macOS 还是 Windows 上都是一致的 )。
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
php 7-fpm f214b5c48a25 9 days ago 368MB
redis 3.2 2fef532eadb3 11 days ago 76MB
redis 4.0 e1a73233e3be 11 days ago 83.4MB
cogset/cron latest c01d5ac6fc8a 15 months ago 125MB
在 docker images
命令的结果中,我们可以看到镜像的 ID ( IMAGE ID)、构建时间 ( CREATED )、占用空间 ( SIZE ) 等数据。
这里需要注意一点, Docker 只显示了镜像 ID 的前 12 个字符,大部分情况下,它们已经能够让我们在单一主机中识别出不同的镜像了。
镜像命名
镜像层的 ID 既可以识别每个镜像层,也可以用来直接识别镜像 ( 因为根据最上层镜像能够找出所有依赖的下层镜像,所以最上层进行的镜像层 ID 就能表示镜像的 ID )
在 docker images
命令打印出的内容中,我们还能看到两个与镜像命名有关的数据:REPOSITORY 和 TAG,这两者其实就组成了 docker 对镜像的命名规则。
来看这个例子:
准确的来说,镜像的命名我们可以分成三个部分:username、repository 和 tag。
- username: 主要用于识别上传镜像的不同用户,与 GitHub 中的用户空间类似。
- repository:主要用于识别进行的内容,形成对镜像的表意描述。
- tag:主要用户表示镜像的版本,方便区分进行内容的不同细节
对于 username 来说,在上面我们展示的 docker images
结果中,有的镜像有 username 这个部分,而有的镜像是没有的。没有 username 这个部分的镜像,表示镜像是由 Docker 官方所维护和提供的,所以就不单独标记用户了。
另外,Docker 中还有一个约定,当我们在操作中没有具体给出镜像的 tag 时,Docker 会采用 latest 作为缺省 tag。
容器的生命周期
容器的生命周期:
图中展示了几种常见对 Docker 容器的操作命令,以及执行它们之后容器运行状态的变化。这里我们撇开命令,着重看看容器的几个核心状态,也就是图中色块表示的:Created、Running、Paused、Stopped、Deleted。
在这几种状态中,Running 是最为关键的状态,在这种状态中的容器,就是真正正在运行的容器了。我们可以看到,Docker 容器的生命周期里分为五种状态,其分别代表着:
- Created:容器已经被创建,容器所需的相关资源已经准备就绪,但容器中的程序还未处于运行状态。
- Running:容器正在运行,也就是容器中的应用正在运行。
- Paused:容器已暂停,表示容器中的所有程序都处于暂停 ( 不是停止 ) 状态。
- Stopped:容器处于停止状态,占用的资源和沙盒环境都依然存在,只是容器中的应用程序均已停止。
- Deleted:容器已删除,相关占用的资源及存储在 Docker 中的管理信息也都已释放和移除。
主进程
如果单纯去看容器的生命周期会有一些难理解的地方,而 Docker 中对容器生命周期的定义其实并不是独立存在的。
在 Docker 的设计中,容器的生命周期其实与容器中 PID 为 1 这个进程有着密切的关系。容器的启动,本质上可以理解为这个进程的启动,而容器的停止也就意味着这个进程的停止,反过来理解亦然。
当我们启动容器时,Docker 其实会按照镜像中的定义,启动对应的程序,并将这个程序的主进程作为容器的主进程 ( 也就是 PID 为 1 的进程 )。而当我们控制容器停止时,Docker 会向主进程发送结束信号,通知程序退出。
而当容器中的主进程主动关闭时 ( 正常结束或出错停止 ),也会让容器随之停止。
写时复制机制
Docker 的写时复制与编程中的相类似,也就是在通过镜像运行容器时,并不是马上就把镜像里的所有内容拷贝到容器所运行的沙盒文件系统中,而是利用 UnionFS 将镜像以只读的方式挂载到沙盒文件系统中。只有在容器中发生对文件的修改时,修改才会体现到沙盒环境上。
也就是说,容器在创建和启动的过程中,不需要进行任何的文件系统复制操作,也不需要为容器单独开辟大量的硬盘空间,与其他虚拟化方式对这个过程的操作进行对比,Docker 启动的速度可见一斑。
采用写时复制机制来设计的 Docker,既保证了镜像在生成为容器时,以及容器在运行过程中,不会对自身造成修改。又借助剔除常见虚拟化在初始化时需要从镜像中拷贝整个文件系统的过程,大幅提高了容器的创建和启动速度。可以说,Docker 容器能够实现秒级启动速度,写时复制机制在其中发挥了举足轻重的作用。
从镜像仓库获得镜像
镜像仓库
Docker 里集中存放镜像的一个概念,也就是镜像仓库。如果说我们把镜像的结构用 Git 项目的结构做类比,那么镜像仓库就可以看似 GitLab、GitHub 等的托管平台,只不过 Docker 的镜像仓库托管的不是代码项目,而是镜像。
当然,存储镜像并不是镜像仓库最值得炫耀的功能,其最大的作用是实现了 Docker 镜像的分发。借助镜像仓库,我们得到了一个镜像的中转站,我们可以将开发环境上所使用的镜像推送至镜像仓库,并在测试或生产环境上拉取到它们,而这个过程仅需要几个命令,甚至自动化完成。
获取镜像
要拉取镜像,我们可以使用 docker pull
命令,命令的参数就是我们之前所提到的镜像仓库名。
$ sudo docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
124c757242f8: Downloading [===============================================> ] 30.19MB/31.76MB
9d866f8bde2a: Download complete
fa3f2f277e67: Download complete
398d32b153e8: Download complete
afde35469481: Download complete
当我们运行这个命令后,Docker 就会开始从镜像仓库中拉取我们所指定的镜像了,在控制台中,我们可以看到镜像拉取的进度。下载进度会分为几行,其实每一行代表的就是一个镜像层。Docker 首先会拉取镜像所基于的所有镜像层,之后再单独拉取每一个镜像层并组合成这个镜像。当然,如果在本地已经存在相同的镜像层 ( 共享于其他的镜像 ),那么 Docker 就直接略过这个镜像层的拉取而直接采用本地的内容。Docker 会默认使用 latest 这个标签。
当然,我们也能够使用完整的镜像命名来拉取镜像。
$ sudo docker pull openresty/openresty:1.13.6.2-alpine
1.13.6.2-alpine: Pulling from openresty/openresty
ff3a5c916c92: Pull complete
ede0a2a1012b: Pull complete
0e0a11843023: Pull complete
246b2c6f4992: Pull complete
Digest: sha256:23ff32a1e7d5a10824ab44b24a0daf86c2df1426defe8b162d8376079a548bf2
Status: Downloaded newer image for openresty/openresty:1.13.6.2-alpine
镜像在被拉取之后,就存放到了本地,接受当前这个 Docker 实例管理了,我们可以通过 docker images
命令看到它们。
$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest cd6d8154f1e1 12 days ago 84.1MB
openresty/openresty 1.13.6.2-alpine 08d5c926e4b6 3 months ago 49.3MB
管理镜像
除了之前我们所提到的 docker images
可以列出本地 Docker 中的所有镜像外,如果我们要获得镜像更详细的信息,我们可以通过 docker inspect
这个命令。
$ sudo docker inspect redis:3.2
[
{
"Id": "sha256:2fef532eadb328740479f93b4a1b7595d412b9105ca8face42d3245485c39ddc",
"RepoTags": [
"redis:3.2"
],
"RepoDigests": [
"redis@sha256:745bdd82bad441a666ee4c23adb7a4c8fac4b564a1c7ac4454aa81e91057d977"
],
## ......
}
]
在 docker inspect
的结果中我们可以看到关于镜像相当完备的信息,由于条目分类比较多,这里我就不一一罗列展开了。
除了能够查看镜像的信息外,docker inspect
还能查看容器等之前我们所提到的 Docker 对象的信息,而传参的方式除了传递镜像或容器的名称外,还可以传入镜像 ID 或容器 ID。只要我们传入的镜像ID能唯一定位到镜像,那么即使它短到只有 1 个字符,Docker 都是可以处理的
$ sudo docker inspect redis:4.0
$ sudo docker inspect 2fef532e
删除镜像
虽然 Docker 镜像占用的空间比较小,但日渐冗杂的镜像和凌乱的镜像版本会让管理越来越困难,所以有时候我们需要清理一些无用的镜像,将它们从本地的 Docker Engine 中移除。
删除镜像的命令是 docker rmi
,参数是镜像的名称或 ID。
$ sudo docker rmi ubuntu:latest
Untagged: ubuntu:latest
Untagged: ubuntu@sha256:de774a3145f7ca4f0bd144c7d4ffb2931e06634f11529653b23eba85aef8e378
Deleted: sha256:cd6d8154f1e16e38493c3c2798977c5e142be5e5d41403ca89883840c6d51762
Deleted: sha256:2416e906f135eea2d08b4a8a8ae539328482eacb6cf39100f7c8f99e98a78d84
Deleted: sha256:7f8291c73f3ecc4dc9317076ad01a567dd44510e789242368cd061c709e0e36d
Deleted: sha256:4b3d88bd6e729deea28b2390d1ddfdbfa3db603160a1129f06f85f26e7bcf4a2
Deleted: sha256:f51700a4e396a235cee37249ffc260cdbeb33268225eb8f7345970f5ae309312
Deleted: sha256:a30b835850bfd4c7e9495edf7085cedfad918219227c7157ff71e8afe2661f63
删除镜像的过程其实是删除镜像内的镜像层,在删除镜像命令打印的结果里,我们可以看到被删除的镜像层以及它们的 ID。当然,如果存在两个镜像共用一个镜像层的情况,你也不需要担心 Docker 会删除被共享的那部分镜像层,只有当镜像层只被当前被删除的镜像所引用时,Docker 才会将它们从硬盘空间中移除。
docker rmi
命令也支持同时删除多个镜像,只需要通过空格传递多个镜像 ID 或镜像名即可。
$ sudo docker rmi redis:3.2 redis:4.0
Untagged: redis:3.2
Untagged: redis@sha256:745bdd82bad441a666ee4c23adb7a4c8fac4b564a1c7ac4454aa81e91057d977
Deleted: sha256:2fef532eadb328740479f93b4a1b7595d412b9105ca8face42d3245485c39ddc
## ......
Untagged: redis:4.0
Untagged: redis@sha256:b77926b30ca2f126431e4c2055efcf2891ebd4b4c4a86a53cf85ec3d4c98a4c9
Deleted: sha256:e1a73233e3beffea70442fc2cfae2c2bab0f657c3eebb3bdec1e84b6cc778b75
## ......
运行和管理容器
容器是基于容器技术所建立和运行的轻量级应用运行环境,它是 Docker 封装和管理应用程序或微服务的“集装箱”。在 Docker 中,容器算是最核心的部分
创建容器
当我们选择好镜像以后,就可以通过 docker create
这个命令来创建容器了。
$ sudo docker create nginx:1.12
34f277e22be252b51d204acbb32ce21181df86520de0c337a835de6932ca06c3
执行 docker create
后,Docker 会根据我们所给出的镜像创建容器,在控制台中会打印出 Docker 为容器所分配的容器 ID,此时容器是处于 Created 状态的。
之后我们对容器的操作可以通过这个容器 ID 或者它的缩略形式进行,但用容器 ID 操作容器就和用镜像 ID 操作镜像一样烦闷,所以我们更习惯于使用容器名来操作容器。要使用容器名操作容器,就先得给容器命名,在创建容器时,我们可以通过 --name
这个选项来配置容器名。
$ sudo docker create --name nginx nginx:1.12
启动容器
通过 docker create
创建的容器,是处于 Created 状态的,其内部的应用程序还没有启动,所以我们需要通过 docker start
命令来启动它。
$ sudo docker start nginx
由于我们为容器指定了名称,这样的操作会更加自然,所以我们非常推荐为每个被创建的容器都进行命名。
在 Docker 里,还允许我们通过 docker run
这个命令将 docker create
和 docker start
这两步操作合成为一步,进一步提高工作效率。
$ sudo docker run --name nginx -d nginx:1.12
89f2b769498a50f5c35a314ab82300ce9945cbb69da9cda4b022646125db8ca7
通过 docker run
创建的容器,在创建完成之后会直接启动起来,不需要我们再使用 docker start
去启动了。
这里需要注意的一点是,通常来说我们启动容器会期望它运行在“后台”,而 docker run
在启动容器时,会采用“前台”运行这种方式,这时候我们的控制台就会衔接到容器上,不能再进行其他操作了。我们可以通过 -d
或 --detach
这个选项告诉 Docker 在启动后将程序与控制台分离,使其进入“后台”运行。
管理容器
通过 docker ps
这个命令,我们可以罗列出 Docker 中的容器。
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
89f2b769498a nginx:1.12 "nginx -g 'daemon of…" About an hour ago Up About an hour 80/tcp nginx
默认情况下,docker ps
列出的容器是处于运行中的容器,如果要列出所有状态的容器,需要增加 -a
或 --all
选项。
$ sudo docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
425a0d3cd18b redis:3.2 "docker-entrypoint.s…" 2 minutes ago Created redis
89f2b769498a nginx:1.12 "nginx -g 'daemon of…" About an hour ago Up About an hour 80/tcp nginx
在 docker ps
的结果中,我们可以看到几项关于容器的信息。其中 CONTAINER ID、IMAGE、CREATED、NAMES 大家都比较容易理解,分别表示容器 ID,容器所基于的镜像,容器的创建时间和容器的名称。
结果中的 COMMAND 表示的是容器中主程序 ( 也就是与容器生命周期所绑定进程所关联的程序 ) 的启动命令,这条命令是在镜像内定义的,而容器的启动其实质就是启动这条命令
结果中的 STATUS 表示容器所处的状态,其值和我们之前所谈到的状态有所区别,主要是因为这里还记录了其他的一些信息。在这里,常见的状态表示有三种:
- Created 此时容器已创建,但还没有被启动过。
- Up [ Time ] 这时候容器处于正在运行状态,而这里的 Time 表示容器从开始运行到查看时的时间。
- Exited ([ Code ]) [ Time ] 容器已经结束运行,这里的 Code 表示容器结束运行时,主程序返回的程序退出码,而 Time 则表示容器结束到查看时的时间。
停止和删除容器
要将正在运行的容器停止,我们可以使用 docker stop
命令。
$ sudo docker stop nginx
容器停止后,其维持的文件系统沙盒环境还是存在的,内部被修改的内容也都会保留,我们可以通过 docker start
命令将这个容器再次启动。
当我们需要完全删除容器时,可以通过 docker rm
命令将容器进行删除。
$ sudo docker rm nginx
正在运行中的容器默认情况下是不能被删除的,我们可以通过增加 -f
或 --force
选项来让 docker rm
强制停止并删除容器,不过这种做法并不妥当。
进入容器
如果我们还会希望进一步了解容器或操作容器,这时候最佳的方式就是让我们进入到容器了。
我们知道,容器是一个隔离运行环境的东西,它里面除了镜像所规定的主进程外,其他的进程也是能够运行的,Docker 为我们提供了一个命令 docker exec
来让容器运行我们所给出的命令。
在 Linux 中,大家熟悉的控制台软件应该是 Shell 和 Bash 了,它们分别由 sh 和 bash 这两个程序启动。
说到这里,有读者一定想到了,既然有这两个控制台程序,我们只要在容器里执行它们,然后通过它们去控制容器内的环境,就是通过 docker exec
命令来启动 sh 或 bash,并通过它们实现对容器内的虚拟环境的控制。
由于 bash 的功能要比 sh 丰富,所以在能够使用 bash 的容器里,我们优先选择它作为控制台程序。
$ sudo docker exec -it nginx bash
root@83821ea220ed:/#
在借助 docker exec
进入容器的时候,我们需要特别注意命令中的两个选项不可或缺,即 -i
和 -t
( 它们俩可以利用简写机制合并成 -it
)。其中 -i
( --interactive
) 表示保持我们的输入流,只有使用它才能保证控制台程序能够正确识别我们的命令。而 -t
( --tty
) 表示启用一个伪终端,形成我们与 bash 的交互,如果没有它,我们无法看到 bash 内部的执行结果。
进入容器后,我们可以通过exit来退出进入容器的程序