第1章 容器发展之路
第2章 走进Docker
Docker是一种运行于Linux和Windows上的软件,用于创建、管理和编排容器。Docker是在GitHub上开发的Moby开源项目的一部分。
1.Docker运行时与编排引擎
技术人员在谈到Docker时,主要是指Docker引擎。
Docker引擎是用于运行和编排容器的基础设施工具。有VMware管理经验的读者可以将其类比为ESXi。ESXi是运行虚拟机的核心管理程序,而Docker引擎是运行容器的核心容器运行时。Docker引擎可以从Docker网站下载,也可以基于GitHub上的源码进行构建。无论是开源版本还是商业版本,都有Linux和Windows版本。Docker引擎主要有两个版本:企业版(EE)和社区版(CE)。
第3章 Docker安装
1.Linux上安装Docker
在Linux上安装Docker是常见的安装场景,并且安装过程非常简单。
下面的例子使用wget
命令来运行一个Shell脚本,完成Docker CE的安装。更多其他在Linux上安装Docker的方式,可以打开Docker主页面,单击页面中Get Started
按钮来获取。
(1)在Linux机器上打开一个新的Shell。
(2)使用wget
从https://get.docker.com获取并运行Docker安装脚本,然后采用Shell中管道(pipe)的方式来执行这个脚本。
$ wget -qO- https://get.docker.com/ | sh
modprobe: FATAL: Module aufs not found /lib/modules/4.4.0-36-generic
+ sh -c 'sleep 3; yum -y -q install docker-engine'
<Snip>
If you would like to use Docker as a non-root user, you should
now consider adding your user to the "docker" group with
something like:
sudo usermod -aG docker your-user
Remember that you will have to log out and back in...
2.Docker引擎(Engine)升级
需要重视升级操作的每个前置条件,包括确保容器配置了正确的重启策略;在Swarm Mode模式下使用服务时,需要确保正确配置了draining node。当完成了上述前置条件的检查之后,可以通过如下步骤完成升级操作。
(1)停止Docker守护程序。
(2)移除旧版本Docker。
(3)安装新版本Docker。
(4)配置新版本的Docker为开机自启动。
(5)确保容器重启成功。
3.在Ubuntu 16.04上升级Docker CE
(1)更新APT包列表。
$ apt-get update
(2)卸载当前Docker。
$ apt-get remove docker docker-engine docker-ce docker.io -y
在之前的版本中,Docker引擎的包名可能有多个。这条命令能够确保已经安装的Docker包全部被删除。
(3)安装新版本Docker。
接下来的命令会使用get.docker.com的脚本完成最新版本Docker CE的安装和配置。
$ wget -qO- https://get.docker.com/ | sh
(4)将Docker配置为开机自启动。
$ systemctl enable docker
Synchronizing state of docker.service...
Executing /lib/systemd/systemd-sysv-install enable docker
$ systemctl is-enabled docker
enabled
(5)检查并确保每一个容器和服务都已经重启成功。
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS
97e599aca9f5 alpine "sleep 1d" 14 minutes ago Up 1 minute
$ docker service ls
ID NAME MODE REPLICAS IMAGE
ibyotlt1ehjy prod-equus1 replicated 1/1 alpine:latest
第4章 纵观Docker
1.运维视角
安装Docker的时候,会涉及两个主要组件:Docker客户端和Docker daemon
1.镜像
在Docker主机上运行docker image ls
命令。
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
在Docker主机上获取镜像的操作被称为拉取(pulling)。如果使用Linux,那么会拉取ubuntu:latest
镜像
再次运行docker image ls
命令来查看刚刚拉取的镜像。
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 00fd29ccc6f1 3 weeks ago 111MB
Docker的每个镜像都有自己的唯一ID。用户可以通过引用镜像的ID
或名称来使用镜像。如果用户选择使用镜像ID,通常只需要输入ID开头的几个字符即可——因为ID是唯一的,Docker知道用户想引用的具体镜像是哪个。
2.容器
在Linux中启动容器的命令如下。
$ docker container run -it ubuntu:latest /bin/bash
root@6dc20d508db0:/#
docker container run
告诉Docker daemon启动新的容器。其中-it
参数告诉Docker开启容器的交互模式并将读者当前的Shell连接到容器终端(在容器章节中会详细介绍)。
在容器内部运行ps
命令查看当前正在运行的全部进程。
Linux示例如下。
root@6dc20d508db0:/# ps -elf
F S UID PID PPID NI ADDR SZ WCHAN STIME TTY TIME CMD
4 S root 1 0 0 - 4560 wait 13:38 ? 00:00:00 /bin/bash
0 R root 9 1 0 - 8606 - 13:38 ? 00:00:00 ps -elf
Linux容器中仅包含两个进程。
- PID 1:代表
/bin/bash
进程,该进程是通过docker container run
命令来通知容器运行的。 - PID 9:代表
ps -elf
进程,查看当前运行中进程所使用的命令/程序。
命令输出中展示的ps -elf
进程存在一定的误导,因为这个程序在ps
命令退出后就结束了。这意味着容器内长期运行的进程其实只有/bin/bash
。
按Ctrl-PQ组合键
,可以在退出容器的同时还保持容器运行。这样Shell就会返回到Docker主机终端。可以通过查看Shell提示符来确认。
现在读者已经返回到Docker主机的Shell提示符,再次运行ps
命令。
Linux示例如下。
$ ps -elf
F S UID PID PPID NI ADDR SZ WCHAN TIME CMD
4 S root 1 0 0 - 9407 - 00:00:03 /sbin/init
使用Ctrl-PQ组合键
来退出容器的。在容器内部使用该操作可以退出当前容器,但不会杀死容器进程。可以通过docker container ls
命令查看系统内全部处于运行状态的容器。
执行docker container exec
命令,可以将Shell连接到一个运行中的容器终端。因为之前示例中的容器仍在运行,所以下面的示例会创建到该容器的新连接。
Linux示例如下。
$ docker container exec -it vigilant_borg bash
root@e2b69eeb55cb:/#
示例中的容器名为“vigilant_brog”。读者环境中的容器名称会不同,所以请记得将“vigilant_brog”替换为自己Docker主机上运行中的容器名称或者ID。
通过docker container stop
和docker container rm
命令来停止并杀死容器。
通过运行docker container ls
命令,并指定-a
参数来确认容器已经被成功删除。添加-a
的作用是让Docker列出所有容器,甚至包括那些处于停止状态的。
2.开发视角
容器即应用!应用代码中的Dockerfile并将其容器化,最终以容器的方式运行。Dockerfile是一个纯文本文件,其中描述了如何将应用构建到Docker镜像当中。
查看Dockerfile的全部内容。
$ cat Dockerfile
FROM alpine
LABEL maintainer="nigelpoulton@hotmail.com"
RUN apk add --update nodejs nodejs-npm
COPY . /src
WORKDIR /src
RUN npm install
EXPOSE 8080
ENTRYPOINT ["node", "./app.js"]
现在,只需要知道Dockerfile的每一行都代表一个用于构建镜像的指令即可。
使用docker image build
命令,根据Dockerfile中的指令来创建新的镜像。示例中新建的Docker镜像名为test:latest
。
一定要在包含应用代码和Dockerfile的目录下执行这些命令。
$ docker image build -t test:latest .
Sending build context to Docker daemon 74.75kB
Step 1/8 : FROM alpine
latest: Pulling from library/alpine
88286f41530e: Pull complete
Digest: sha256:f006ecbb824...0c103f4820a417d
Status: Downloaded newer image for alpine:latest
---> 76da55c8019d
<Snip>
Successfully built f154cb3ddbd4
Successfully tagged test:latest
一旦构建完成,就可以确认主机上是否存在test:latest
镜像。
$ docker image ls
REPO TAG IMAGE ID CREATED SIZE
Test latest f154cb3ddbd4 1 minute ago 55.6MB
读者现在已经拥有一个新的Docker镜像,其中包含了应用程序。
从镜像启动容器,并测试应用。
Linux代码如下。
$ docker container run -d \
--name web1 \
--publish 8080:8080 \
test:latest
打开Web浏览器,在地址栏中输入容器运行所在的Docker主机的DNS名称或者IP地址,并在后面加上端口号8080
Docker技术
第5章 Docker引擎
Docker 引擎是用来运行和管理容器的核心软件。通常人们会简单地将其代指为 Docker或Docker平台。如果有读者对VMware略知一二,那么可以将Docker引擎理解为ESXi的角色。
基于开放容器计划(OCI)相关标准的要求,Docker引擎采用了模块化的设计原则,其组件是可替换的。
从多个角度来看,Docker引擎就像汽车引擎——二者都是模块化的,并且由许多可交换的部件组成。
- 汽车引擎由许多专用的部件协同工作,从而使汽车可以行驶,例如进气管、节气门、气缸、火花塞、排气管等。
- Docker引擎由许多专用的工具协同工作,从而可以创建和运行容器,例如API、执行驱动、运行时、shim进程等。Docker引擎由如下主要的组件构成:Docker客户端(Docker Client)、Docker守护进程(Docker daemon)、containerd以及runc。它们共同负责容器的创建和运行。
第6章 Docker镜像
镜像和容器
图6.1从顶层设计层面展示了镜像和容器间的关系。通常使用docker container run
和docker service create
命令从某个镜像启动一个或多个容器。一旦容器从镜像启动后,二者之间就变成了互相依赖的关系,并且在镜像上启动的容器全部停止之前,镜像是无法被删除的。尝试删除镜像而不停止或销毁使用它的容器,会导致下面的错误。
$ docker image rm
Error response from daemon: conflict: unable to remove repository reference \
"" (must force) - container is using its referenc\
ed image
容器目的就是运行应用或者服务,这意味着容器的镜像中必须包含应用/服务运行所必需的操作系统和应用文件。但是,容器又追求快速和小巧,这意味着构建镜像的时候通常需要裁剪掉不必要的部分,保持较小的体积。
Docker主机安装之后,本地并没有镜像。
Linux Docker主机本地镜像仓库通常位于/var/lib/docker/<storage-driver>
,
Linux示例如下。
$ docker image pull ubuntu:latest
latest: Pulling from library/ubuntu
b6f892c0043b: Pull complete
55010f332b04: Pull complete
2955fb827c94: Pull complete
3deef3fcbd30: Pull complete
cf9722e506aa: Pull complete
Digest: sha256:38245....44463c62a9848133ecb1aa8
Status: Downloaded newer image for ubuntu:latest
$ docker image pull alpine:latest
latest: Pulling from library/alpine cfc728c1c558:
Pull complete
Digest: sha256:c0537...497c0a7726c88e2bb7584dc96
Status: Downloaded newer image for alpine:latest
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest ebcd9d4fca80 3 days ago 118MB
alpine latest 02674b9cb179 8 days ago 3.99MB
Docker镜像存储在镜像仓库服务(Image Registry)当中。Docker客户端的镜像仓库服务是可配置的,默认使用Docker Hub。镜像仓库服务包含多个镜像仓库(Image Repository)。同样,一个镜像仓库中可以包含多个镜像。
镜像命名和标签
只需要给出镜像的名字和标签,就能在官方仓库中定位一个镜像(采用“:”分隔)。从官方仓库拉取镜像时,docker image pull
命令的格式如下。
docker image pull <repository>:<tag>
在之前的Linux示例中,通过下面的两条命令完成Alpine
和Ubuntu
镜像的拉取。
docker image pull alpine:latest
docker image pull ubuntu:latest
这两条命令从alpine和ubuntu仓库拉取了标有“latest”标签的镜像。
下面的示例展示了如何从官方仓库拉取不同的镜像。
$ docker image pull mongo:3.3.11
//该命令会从官方Mongo库拉取标签为3.3.11的镜像
$ docker image pull redis:latest
//该命令会从官方Redis库拉取标签为latest的镜像
$ docker image pull alpine
//该命令会从官方Alpine库拉取标签为latest的镜像
标签为latest
的镜像没有什么特殊魔力!标有latest
标签的镜像不保证这是仓库中最新的镜像!例如,Alpine
仓库中最新的镜像通常标签是edge
。通常来讲,使用latest
标签时需要谨慎!
从非官方仓库拉取镜像也是类似的,读者只需要在仓库名称面前加上Docker Hub的用户名或者组织名称。下面的示例展示了如何从tu-demo
仓库中拉取v2
这个镜像,其中镜像的拥有者是Docker Hub账户nigelpoulton
,一个不应该被信任的账户。
$ docker image pull nigelpoulton/tu-demo:v2
//该命令会从以我自己的Docker Hub账号为命名空间的tu-demo库中下载标签为v2的镜像
为镜像打多个标签
关于镜像有一点不得不提,一个镜像可以根据用户需要设置多个标签。这是因为标签是存放在镜像元数据中的任意数字或字符串。一起来看下面的示例。
在docker image pull
命令中指定-a
参数来拉取仓库中的全部镜像。接下来可以通过运行docker image ls
查看已经拉取的镜像。如果读者使用Windows示例,则可以将Linux示例中的镜像仓库nigelpoulton/tu-demo
替换为microsoft/nanoserver
。
过滤docker image ls
的输出内容
Docker提供--filter
参数来过滤docker image ls
命令返回的镜像列表内容。
下面的示例只会返回悬虚(dangling)镜像。
$ docker image ls --filter dangling=true
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 4fd34165afe0 7 days ago 14.5MB
那些没有标签的镜像被称为悬虚镜像,在列表中展示为<none>:<none>
。通常出现这种情况,是因为构建了一个新镜像,然后为该镜像打了一个已经存在的标签。当此情况出现,Docker会构建新的镜像,然后发现已经有镜像包含相同的标签,接着Docker会移除旧镜像上面的标签,将该标签标在新的镜像之上。例如,首先基于alpine:3.4
构建一个新的镜像,并打上dodge:challenger
标签。然后更新Dockerfile,将alpine:3.4
替换为alpine:3.5
,并且再次执行docker image build
命令。该命令会构建一个新的镜像,并且标签为dodge:challenger
,同时移除了旧镜像上面对应的标签,旧镜像就变成了悬虚镜像。
可以通过docker image prune
命令移除全部的悬虚镜像。如果添加了-a
参数,Docker会额外移除没有被使用的镜像(那些没有被任何容器使用的镜像)。
Docker目前支持如下的过滤器。
dangling
:可以指定true
或者false
,仅返回悬虚镜像(true),或者非悬虚镜像(false)。before
:需要镜像名称或者ID作为参数,返回在之前被创建的全部镜像。since
:与before
类似,不过返回的是指定镜像之后创建的全部镜像。label
:根据标注(label)的名称或者值,对镜像进行过滤。docker image ls
命令输出中不显示标注内容。
其他的过滤方式可以使用reference
。
下面就是使用reference
完成过滤并且仅显示标签为latest
的示例。
$ docker image ls --filter=reference="*:latest"
REPOSITORY TAG IMAGE ID CREATED SIZE
alpine latest 3fd9065eaf02 8 days ago 4.15MB
test latest 8426e7efb777 3 days ago 122MB
读者也可以使用--format
参数来通过Go模板对输出内容进行格式化。例如,下面的指令将只返回Docker主机上镜像的大小属性。
$ docker image ls --format "{{.Size}}"
99.3MB
111MB
82.6MB
88.8MB
4.15MB
108MB
使用下面命令返回全部镜像,但是只显示仓库、标签和大小信息。
$ docker image ls --format "{{.Repository}}: {{.Tag}}: {{.Size}}"
dodge: challenger: 99.3MB
ubuntu: latest: 111MB
python: 3.4-alpine: 82.6MB
python: 3.5-alpine: 88.8MB
alpine: latest: 4.15MB
nginx: latest: 108MB
docker search
命令允许通过CLI的方式搜索Docker Hub。读者可以通过“NAME
”字段的内容进行匹配,并且基于返回内容中任意列的值进行过滤。
简单模式下,该命令会搜索所有“NAME”字段中包含特定字符串的仓库。例如,下面的命令会查找所有“NAME”包含“nigelpoulton”的仓库。
$ docker search nigelpoulton
NAME DESCRIPTION STARS AUTOMATED
nigelpoulton/pluralsight.. Web app used in... 8 [OK]
nigelpoulton/tu-demo 7
nigelpoulton/k8sbook Kubernetes Book web app 1
nigelpoulton/web-fe1 Web front end example 0
nigelpoulton/hello-cloud Quick hello-world image 0
“NAME”字段是仓库名称,包含了Docker ID,或者非官方仓库的组织名称。例如,下面的命令会列出所有仓库名称中包含“alpine”的镜像。
$ docker search alpine
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
alpine A minimal Docker.. 2988 [OK]
mhart/alpine-node Minimal Node.js.. 332
anapsix/alpine-java Oracle Java 8... 270 [OK]
<Snip>
需要注意,上面返回的镜像中既有官方的也有非官方的。读者可以使用--filter "is-official=true"
,使命令返回内容只显示官方镜像。
$ docker search alpine --filter "is-official=true"
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
alpine A minimal Docker.. 2988 [OK]
重复前面的操作,但这次只显示自动创建的仓库。
$ docker search alpine --filter "is-automated=true"
NAME DESCRIPTION OFFICIAL AUTOMATED
anapsix/alpine-java Oracle Java 8 (and 7).. [OK]
frolvlad/alpine-glibc Alpine Docker image.. [OK]
kiasaki/alpine-postgres PostgreSQL docker.. [OK]
zzrot/alpine-caddy Caddy Server Docker.. [OK]
<Snip>
关于docker search
需要注意的最后一点是,默认情况下,Docker只返回25行结果。但是,可以指定--limit
参数来增加返回内容行数,最多为100行。
Docker镜像
Docker负责堆叠这些镜像层,并且将它们表示为单个统一的对象。
有多种方式可以查看和检查构成某个镜像的分层,本书在前面已经展示了其中一种。接下来再回顾一下docker image pull ubuntu:latest
命令的输出内容。
$ docker image pull ubuntu:latest
latest: Pulling from library/ubuntu
952132ac251a: Pull complete
82659f8f1b76: Pull complete
c19118ca682d: Pull complete
8296858250fe: Pull complete
24e0251a0e2c: Pull complete
Digest: sha256:f4691c96e6bbaa99d...28ae95a60369c506dd6e6f6ab
Status: Downloaded newer image for ubuntu:latest
在上面输出内容中,以Pull complete
结尾的每一行都代表了镜像中某个被拉取的镜像层。可以看到,这个镜像包含5个镜像层。
另一种查看镜像分层的方式是通过docker image inspect
命令。下面同样以ubuntu:latest
镜像为例。
$ docker image inspect ubuntu:latest
[
{
"Id": "sha256:bd3d4369ae.......fa2645f5699037d7d8c6b415a10",
"RepoTags": [
"ubuntu:latest"
<Snip>
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:c8a75145fc...894129005e461a43875a094b93412",
"sha256:c6f2b330b6...7214ed6aac305dd03f70b95cdc610",
"sha256:055757a193...3a9565d78962c7f368d5ac5984998",
"sha256:4837348061...12695f548406ea77feb5074e195e3",
"sha256:0cad5e07ba...4bae4cfc66b376265e16c32a0aae9"
]
}
}
]
缩减之后的输出也显示该镜像包含5个镜像层。只不过这次的输出内容中使用了镜像的SHA256散列值来标识镜像层。不过,两中命令都显示了镜像包含5个镜像层。
docker history
命令显示了镜像的构建历史记录,但其并不是严格意义上的镜像分层。例如,有些Dockerfile中的指令并不会创建新的镜像层。比如ENV、EXPOSE、CMD以及ENTRY- POINT。不过,这些命令会在镜像中添加元数据。
下面的例子首先在Docker主机上删除alpine:latest
镜像,然后显示如何通过摘要(而不是标签)来再次拉取该镜像。
$ docker image rm alpine:latest
Untagged: alpine:latest
Untagged: alpine@sha256:c0537...7c0a7726c88e2bb7584dc96
Deleted: sha256:02674b9cb179d...abff0c2bf5ceca5bad72cd9
Deleted: sha256:e154057080f40...3823bab1be5b86926c6f860
$ docker image pull alpine@sha256:c0537...7c0a7726c88e2bb7584dc96
sha256:c0537...7726c88e2bb7584dc96: Pulling from library/alpine
cfc728c1c558: Pull complete
Digest: sha256:c0537ff6a5218...7c0a7726c88e2bb7584dc96
Status: Downloaded newer image for alpine@sha256:c0537...bb7584dc96
6.2.13 镜像散列值(摘要)
从Docker 1.10版本开始,镜像就是一系列松耦合的独立层的集合。
镜像本身就是一个配置对象,其中包含了镜像层的列表以及一些元数据信息。
镜像层才是实际数据存储的地方(比如文件等,镜像层之间是完全独立的,并没有从属于某个镜像集合的概念)。
镜像的唯一标识是一个加密ID,即配置对象本身的散列值。每个镜像层也由一个加密ID区分,其值为镜像层本身内容的散列值。
这意味着修改镜像的内容或其中任意的镜像层,都会导致加密散列值的变化。所以,镜像和其镜像层都是不可变的,任何改动都能很轻松地被辨别。
这就是所谓的内容散列(Content Hash)。
到目前为止,事情都很简单。但是接下来的内容就有点儿复杂了。
在推送和拉取镜像的时候,都会对镜像层进行压缩来节省网络带宽以及仓库二进制存储空间。
但是压缩会改变镜像内容,这意味着镜像的内容散列值在推送或者拉取操作之后,会与镜像内容不相符!这显然是个问题。
例如,在推送镜像层到Docker Hub的时候,Docker Hub会尝试确认接收到的镜像没有在传输过程中被篡改。为了完成校验,Docker Hub会根据镜像层重新计算散列值,并与原散列值进行比较。因为镜像在传输过程中被压缩(发生了改变),所以散列值的校验也会失败。
为避免该问题,每个镜像层同时会包含一个分发散列值(Distribution Hash)。这是一个压缩版镜像的散列值,当从镜像仓库服务拉取或者推送镜像的时候,其中就包含了分发散列值,该散列值会用于校验拉取的镜像是否被篡改过。
这个内容寻址存储模型极大地提升了镜像的安全性,因为在拉取和推送操作后提供了一种方式来确保镜像和镜像层数据是一致的。该模型也解决了随机生成镜像和镜像层ID这种方式可能导致的ID冲突问题。
第七章容器
启动容器的一个简单的方式是通过docker container run
命令。
下面的命令启动了一个简单的容器,其中运行了容器化版本的Ubuntu Linux。
$ docker container run -it ubuntu:latest /bin/bash
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
952132ac251a: Pull complete
82659f8f1b76: Pull complete
c19118ca682d: Pull complete
8296858250fe: Pull complete
24e0251a0e2c: Pull complete
Digest: sha256:f4691c96e6bbaa99d9...e95a60369c506dd6e6f6ab
Status: Downloaded newer image for ubuntu:latest
root@3027eb644874:/#
启动Ubuntu容器之时,让容器运行Bash Shell(/bin/bash
)。这使得Bash Shell成为容器中运行的且唯一运行的进程。读者可以通过ps -elf
命令在容器内部查看。
root@3027eb644874:/# ps -elf
F S UID 4 PID PPID NI ADDR SZ WCHAN STIME TTY TIME CMD
S root 0 1 0 0 - 4558 wait 00:47 ? 00:00:00 /bin/bash
R root 11 1 0 - 8604 - 00:52 ? 00:00:00 ps -elf
上面的输出中看起来好像有两个正在运行的进程,其实并非如此。列表中PID为1的进程,是容器被告知要运行的Bash Shell;第二个进程是ps -elf
命令产生的,这是个临时进程,并且在输出后就已经退出了。也就是说,这个容器当前只运行了一个进程——/bin/bash
。
这意味着如果通过输入exit退出Bash Shell,那么容器也会退出(终止)。原因是容器如果不运行任何进程则无法存在——杀死Bash Shell即杀死了容器唯一运行的进程,导致这个容器也被杀死。这对于Windows容器来说也是一样的——杀死容器中的主进程,则容器也会被杀死。
按下Ctrl-PQ
组合键则会退出容器但并不终止容器运行。这样做会切回到Docker主机的Shell,并保持容器在后台运行。可以使用docker container ls
命令来观察当前系统正在运行的容器列表。
$ docker container ls
CNTNR ID IMAGE COMMAND CREATED STATUS NAMES
302...74 ubuntu:latest /bin/bash 6 mins Up 6mins sick_montalcini
当前容器仍然在运行,并且可以通过docker container exec
命令将终端重新连接到Docker,理解这一点很重要。
$ docker container exec -it 3027eb644874 bash
root@3027eb644874:/#
正如读者所见,Shell提示符切换到了容器。如果读者再次运行ps
命令,会看到两个Bash或者PowerShell进程,这是因为docker container exec
命令创建了新的Bash或者PowerShell进程并且连接到容器。这意味着在当前Shell输入exit
并不会导致容器终止,因为原Bash或者PowerShell进程还在运行当中。
输入exit退出容器,并通过命令docker container ps
来确认容器依然在运行中。果然容器还在运行。
如果在自己的Docker主机上运行示例,则需要使用下面两个命令来停止并删除容器(读者需要将ID替换为自己容器的ID)。
$ docker container stop 3027eb64487
3027eb64487
$ docker container rm 3027eb64487
3027eb64487
前面介绍了如何使用docker container run
命令来启动容器。接下来会重新启动一个新的容器,这样就可以观察期完整的生命周期。下面的示例中会采用Linux Docker主机来运行Ubuntu容器。但同时,示例内容在前面例子中使用过的Windows PowerShell容器中也是生效的——尽管读者需要将Linux命令替换为对应的Windows命令。
$ docker container run --name percy -it ubuntu:latest /bin/bash
root@9cb2d2fd1d65:/#
这就是新建的容器,名称为“percy”,意指持久化(persistent)。
接下来把该容器投入使用,将一部分数据写入其中。
在新容器内部Shell中,执行下面的步骤来将部分数据写入到tmp
目录下的某个文件中,并确认数据是否写入成功。
root@9cb2d2fd1d65:/# cd tmp
root@9cb2d2fd1d65:/tmp# ls -l
total 0
root@9cb2d2fd1d65:/tmp# echo "DevOps FTW" > newfile
root@9cb2d2fd1d65:/tmp# ls -l
total 4
-rw-r--r-- 1 root root 14 May 23 11:22 newfile
root@9cb2d2fd1d65:/tmp# cat newfile
DevOps FTW
按Ctrl-PQ
组合键退出当前容器。
现在使用docker container stop
命令来停止容器运行,切换到暂停(vacation)状态。
$ docker container stop percy
percy
读者可以在docker container stop
命令中指定容器的名称或者ID。具体格式为docker container stop <container-id or container-name>
。
现在运行docker container ls
命令列出全部处于运行中状态的容器。
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
新建的容器没有在上面的列表中出现,原因是读者通过docker container stop
命令使该容器停止运行。加上-a
参数再次运行前面的命令,就会显示出全部的容器,包括处于停止状态的。
$ docker container ls -a
CNTNR ID IMAGE COMMAND CREATED STATUS NAMES
9cb...65 ubuntu:latest /bin/bash 4 mins Exited (0) percy
现在可以看到该容器显示当前状态为Exited(0)
。停止容器就像停止虚拟机一样。尽管已经停止运行,容器的全部配置和内容仍然保存在Docker主机的文件系统之中,并且随时可以重新启动。
使用docker container start
命令可以将容器重新启动。
$ docker container start percy
percy
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS NAMES
9cb2d2fd1d65 ubuntu:latest "/bin/bash" 4 mins Up 3 secs percy
现在停止的容器已经重新启动了,此时可以确认之前创建的文件是否还存在。使用docker container exec
命令连接到重启后的容器。
$ docker container exec -it percy bash
root@9cb2d2fd1d65:/#
Shell提示符发生变化,提示正在容器内部空间进行操作。
确认之前创建的文件依然存在,并且文件中仍包含之前写入的数据。
root@9cb2d2fd1d65:/# cd tmp
root@9cb2d2fd1d65:/# ls -l
-rw-r--r-- 1 root root 14 Sep 13 04:22 newfile
root@9cb2d2fd1d65:/#
root@9cb2d2fd1d65:/# cat newfile
DevOps FTW
像是魔术一般,之前创建的文件依然存在,并且文件中包含的数据正是离开的方式!这证明停止容器运行并不会损毁容器或者其中的数据。
尽管上面的示例阐明了容器的持久化特性,还是需要指出卷(volume)才是在容器中存储持久化数据的首选方式。但是在当前阶段,这个示例用于说明容器的持久化特性已经足够了。
到目前为止,读者应该对容器和虚拟机之间的主要区别有了深刻的印象。
现在停止该容器并从系统中删除它。
通过在docker container rm
命令后面添加-f
参数来一次性删除运行中的容器是可行的。但是,删除容器的最佳方式还是分两步,先停止容器然后删除。这样可以给容器中运行的应用/进程一个停止运行并清理残留数据的机会。
在下一个示例中会停止percy
容器,删除它并确认操作成功。如果读者终端仍连接到percy
容器,则需要按下Ctrl-PQ组合键
先返回Docker主机终端。
$ docker container stop percy
percy
$ docker container rm percy
percy
$ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
现在容器已经删除了——在系统中消失。如果这是一个有用的容器,那么之后可以作为无服务的工具使用;如果没有用处,则充其量也就是一个蹩脚的终端。
总结一下容器的生命周期。可以根据需要多次停止、启动、暂停以及重启容器,并且这些操作执行得很快。但是容器及其数据是安全的。直至明确删除容器前,容器都不会丢弃其中的数据。就算容器被删除了,如果将容器数据存储在卷中,数据也会被保存下来。
通常建议在运行容器时配置好重启策略。这是容器的一种自我修复能力,可以在指定事件或者错误后重启来完成自我修复。
重启策略应用于每个容器,可以作为参数被强制传入docker-container run
命令中,或者在Compose文件中声明(在使用Docker Compose以及Docker Stacks的情况下)。
截至本书撰写时,容器支持的重启策略包括always
、unless-stopped
和on-failed
。
always
策略是一种简单的方式。除非容器被明确停止,比如通过docker container stop
命令,否则该策略会一直尝试重启处于停止状态的容器。一种简单的证明方式是启动一个新的交互式容器,并在命令后面指定--restart always
策略,同时在命令中指定运行Shell进程。当容器启动的时候,会登录到该Shell。退出Shell时会杀死容器中PID为1的进程,并且杀死这个容器。但是因为指定了--restart always
策略,所以容器会自动重启。如果运行docker container ls
命令,就会看到容器的启动时间小于创建时间。下面请看示例。
$ docker container run --name neversaydie -it --restart always alpine sh
//等待几秒后输入exit
/# exit
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS
0901afb84439 alpine "sh" 35 seconds ago Up 1 second
注意,容器于35s前被创建,但却在1s前才启动。这是因为在容器中输入退出命令的时候,容器被杀死,然后Docker又重新启动了该容器。
--restart always
策略有一个很有意思的特性,当daemon重启的时候,停止的容器也会被重启。例如,新创建一个容器并指定--restart always
策略,然后通过docker container stop
命令停止该容器。现在容器处于Stopped (Exited)
状态。但是,如果重启Docker daemon,当daemon启动完成时,该容器也会重新启动。
always
和unless-stopped
的最大区别,就是那些指定了--restart unless-stopped
并处于Stopped (Exited)
状态的容器,不会在Docker daemon重启的时候被重启。这个说法可能令人有点迷惑,接下来通过示例进行演示。
下面创建两个新容器,其中“always”容器指定--restart always
策略,另一个“unless- stopped”容器指定了--restart unless-stopped
策略。两个容器均通过docker container stop
命令停止,接着重启Docker。结果“always”容器会重启,但是“unless-stopped”容器不会。
(1)创建两个新容器。
$ docker container run -d --name always \
--restart always \
alpine sleep 1d
$ docker container run -d --name unless-stopped \
--restart unless-stopped \
alpine sleep 1d
$ docker container ls
CONTAINER ID IMAGE COMMAND STATUS NAMES
3142bd91ecc4 alpine "sleep 1d" Up 2 secs unless-stopped
4f1b431ac729 alpine "sleep 1d" Up 17 secs always
现在有两个运行的容器了。一个叫作“always”,另一个叫作“unless-stopped”。
(2)停止两个容器。
$ docker container stop always unless-stopped
$ docker container ls -a
CONTAINER ID IMAGE STATUS NAMES
3142bd91ecc4 alpine Exited (137) 3 seconds ago unless-stopped
4f1b431ac729 alpine Exited (137) 3 seconds ago always
(3)重启Docker。
重启Docker的过程在不同的操作系统上可能不同。下面的示例中展示了如何在Linux上使用systemd重启Docker,在Windows Server 2016上可以使用restart-service重启。
$ systemlctl restart docker
(4)一旦Docker重启成功,检查两个容器的状态。
$ docker container ls -a
CONTAINER CREATED STATUS NAMES
314..cc4 2 minutes ago Exited (137) 2 minutes ago unless-stopped
4f1..729 2 minutes ago Up 9 seconds always
注意到“always”容器(启动时指定了--restart always
策略)已经重启了,但是“unless-stopped”容器(启动时指定了--restart unless-stopped
策略)并没有重启。
on-failure策略会在退出容器并且返回值不是0的时候,重启容器。就算容器处于stopped
状态,在Docker daemon重启的时候,容器也会被重启。
如果读者使用Docker Compose或者Docker Stack,可以在service对象中配置重启策略,示例如下。
version: "3.5"
services:
myservice:
<Snip>
restart_policy:
condition: always | unless-stopped | on-failure
了解一种简单且快速的清理Docker主机上全部运行容器的方法。有言在先,这种处理方式会强制删除所有的容器,并且不会给容器完成清理的机会。这种操作一定不能在生产环境系统或者运行着重要容器的系统上执行。
在Docker主机的Shell中运行下面的命令,可以删除全部容器。
$ docker container rm $(docker container ls -aq) -f
第八章 应用的容器化
Docker的核心思想就是如何将应用整合到容器中,并且能在容器中实际运行。
将应用整合到容器中并且运行起来的这个过程,称为“容器化”(Containerizing),有时也叫作“Docker化”(Dockerizing)。
容器是为应用而生!具体来说,容器能够简化应用的构建、部署和运行过程。
完整的应用容器化过程主要分为以下几个步骤。
(1)编写应用代码。
(2)创建一个Dockerfile,其中包括当前应用的描述、依赖以及该如何运行这个应用。
(3)对该Dockerfile执行docker image build
命令。
(4)等待Docker将应用程序构建到Docker镜像中。
一旦应用容器化完成(即应用被打包为一个Docker镜像),就能以镜像的形式交付并以容器的方式运行了。
单体应用容器化
在接下来的内容中,本书会向读者逐步展示如何将一个简单的单节点Node.js Web应用容器化。如果是Windows操作系统的话,处理过程也是大同小异。
接下来通过以下几个步骤,来介绍具体的过程。
(1)获取应用代码。
(2)分析Dockerfile。
(3)构建应用镜像。
(4)运行该应用。
(5)测试应用。
(6)容器应用化细节。
(7)生产环境中的多阶段构建。
(8)最佳实践。
虽然本章将指导读者完成单节点应用的容器化,但在接下来的章节中读者可以了解到如何采用Docker Compose去完成多节点应用容器化。之后,本书会继续指导读者使用Docker Stack去处理更复杂应用的容器化场景。
1.获取应用代码
应用代码可以从我的GitHub主页获取,读者需要从GitHub将代码克隆到本地。
克隆操作会创建一个名为psweb的文件夹。可以进入该文件夹,并查看其中的内容。
$ cd psweb
$ ls -l
total 28
-rw-r--r-- 1 root root 341 Sep 29 16:26 app.js
-rw-r--r-- 1 root root 216 Sep 29 16:26 circle.yml
-rw-r--r-- 1 root root 338 Sep 29 16:26 Dockerfile
-rw-r--r-- 1 root root 421 Sep 29 16:26 package.json
-rw-r--r-- 1 root root 370 Sep 29 16:26 README.md
drwxr-xr-x 2 root root 4096 Sep 29 16:26 test
drwxr-xr-x 2 root root 4096 Sep 29 16:26 views
该目录下包含了全部的应用源码,以及包含界面和单元测试的子目录。这个应用结构非常简单,读者可以很方便地理解其源码内容。本章暂时不会涉及单元测试相关的内容。
目前应用的代码已就绪,接下来分析一下Dockerfile的具体内容。
2.分析Dockfile
在代码目录当中,有个名称为Dockerfile的文件。这个文件包含了对当前应用的描述,并且能指导Docker完成镜像的构建。
在Docker当中,包含应用文件的目录通常被称为构建上下文(Build Context)。通常将Dockerfile放到构建上下文的根目录下。
另外很重要的一点是,文件开头字母是大写D,这里是一个单词。像“dockerfile”或者“Docker file”这种写法都是不允许的。
接下来了解一下Dockerfile文件当中都包含哪些具体内容。
$ cat Dockerfile
FROM alpine
LABEL maintainer="nigelpoulton@hotmail.com"
RUN apk add --update nodejs nodejs-npm
COPY . /src
WORKDIR /src
RUN npm install
EXPOSE 8080
ENTRYPOINT ["node", "./app.js"]
Dockerfile主要包括两个用途。
- 对当前应用的描述。
- 指导Docker完成应用的容器化(创建一个包含当前应用的镜像)。
不要因Dockerfile就是一个描述文件而对其有所轻视!Dockerfile能实现开发和部署两个过程的无缝切换。同时Dockerfile还能帮助新手快速熟悉这个项目。Dockerfile对当前的应用及其依赖有一个清晰准确的描述,并且非常容易阅读和理解。因此,要像重视你的代码一样重视这个文件,并且将它纳入到源控制系统当中。
下面是这个文件中的一些关键步骤概述:以alpine
镜像作为当前镜像基础,指定维护者(maintainer)为“nigelpoultion@hotmail.com”,安装Node.js
和NPM
,将应用的代码复制到镜像当中,设置新的工作目录,安装依赖包,记录应用的网络端口,最后将app.js
设置为默认运行的应用。
具体分析一下每一步的作用。
每个Dockerfile文件第一行都是FROM
指令。FROM
指令指定的镜像,会作为当前镜像的一个基础镜像层,当前应用的剩余内容会作为新增镜像层添加到基础镜像层之上。本例中的应用基于Linux操作系统,所以在FROM
指令当中所引用的也是一个Linux基础镜像
接下来,Dockerfile中通过标签(LABLE)方式指定了当前镜像的维护者为“nigelpoulton@hotmail. com”。每个标签其实是一个键值对(Key-Value),在一个镜像当中可以通过增加标签的方式来为镜像添加自定义元数据。备注维护者信息有助于为该镜像的潜在使用者提供沟通途径,这是一种值得提倡的做法。
RUN apk add --update nodejs nodejs-npm
指令使用alpine
的apk
包管理器将nodejs
和nodejs-npm
安装到当前镜像之中。RUN
指令会在FROM
指定的alpine
基础镜像之上,新建一个镜像层来存储这些安装内容。
COPY. / src
指令将应用相关文件从构建上下文复制到了当前镜像中,并且新建一个镜像层来存储。COPY
执行结束之后,当前镜像共包含3层
下一步,Dockerfile通过WORKDIR
指令,为Dockerfile中尚未执行的指令设置工作目录。该目录与镜像相关,并且会作为元数据记录到镜像配置中,但不会创建新的镜像层。
然后,RUN npm install
指令会根据package.json
中的配置信息,使用npm
来安装当前应用的相关依赖包。npm
命令会在前文设置的工作目录中执行,并且在镜像中新建镜像层来保存相应的依赖文件。目前镜像一共包含4层
因为当前应用需要通过TCP端口8080对外提供一个Web服务,所以在Dockerfile中通过EXPOSE 8080
指令来完成相应端口的设置。这个配置信息会作为镜像的元数据被保存下来,并不会产生新的镜像层。
最终,通过ENTRYPOINT
指令来指定当前镜像的入口程序。ENTRYPOINT
指定的配置信息也是通过镜像元数据的形式保存下来,而不是新增镜像层。
3.容器化当前应用/构建具体的镜像
到目前为止,读者应该已经了解基本的原理和流程,接下来是时候尝试构建自己的镜像了!
下面的命令会构建并生成一个名为web:latest
的镜像。命令最后的点(.)表示Docker在进行构建的时候,使用当前目录作为构建上下文。
一定要在命令最后包含这个点,并且在执行命令前,读者要确认当前目录是psweb(包含Dockerfile和应用代码的目录)。
$ docker image build -t web:latest . << don't forget the period (.)
Sending build context to Docker daemon 76.29kB
Step 1/8 : FROM alpine
latest: Pulling from library/alpine
ff3a5c916c92: Pull complete
Digest: sha256:7df6db5aa6...0bedab9b8df6b1c0
Status: Downloaded newer image for alpine:latest
---> 76da55c8019d
Step 8/8 : ENTRYPOINT node ./app.js
---> Running in 13977a4f3b21
---> fc69fdc4c18e
Removing intermediate container 13977a4f3b21
Successfully built fc69fdc4c18e
Successfully tagged web:latest
命令执行结束后,检查本地Docker镜像库是否包含了刚才构建的镜像。
$ docker image ls
REPO TAG IMAGE ID CREATED SIZE
web latest fc69fdc4c18e 10 seconds ago 64.4MB
恭喜,应用容器化已经成功了!
读者可以通过docker image inspect web:latest
来确认刚刚构建的镜像配置是否正确。这个命令会列出Dockerfile中设置的所有配置项。
4.推送镜像到仓库
在创建一个镜像之后,将其保存在一个镜像仓库服务是一个不错的方式。这样存储镜像会比较安全,并且可以被其他人访问使用。Docker Hub就是这样的一个开放的公共镜像仓库服务,并且这也是docker image push
命令默认的推送地址。
在推送镜像之前,需要先使用Docker ID登录Docker Hub。除此之外,还需要为待推送的镜像打上合适的标签。
接下来本书会介绍如何登录Docker Hub,并将镜像推送到其中。
在后续的例子中,读者需要用自己的Docker ID替换本书中例子所使用的ID。所以每当读者看到“nigelpoulton”时,记得替换为自己的Docker ID。
$ docker login
Login with **your** Docker ID to push and pull images from Docker Hub...
Username: nigelpoulton
Password:
Login Succeeded
推送Docker镜像之前,读者还需要为镜像打标签。这是因为Docker在镜像推送的过程中需要如下信息。
- Registry(镜像仓库服务)。
- Repository(镜像仓库)。
- Tag(镜像标签)。
读者无须为Registry和Tag指定值。当读者没有为上述信息指定具体值的时候,Docker会默认Registry=docker.io、Tag=latest
。但是Docker并没有给Repository
提供默认值,而是从被推送镜像中的REPOSITORY
属性值获取。这一点可能不好理解,下面会通过一个完整的例子来介绍如何向Docker Hub中推送一个镜像。
在本章节前面的例子中执行了docker image ls
命令。在该命令对应的输出内容中可以看到,镜像仓库的名称是web。这意味着执行docker image push
命令,会尝试将镜像推送到docker.io/web:latest
中。但是其实nigelpoulton
这个用户并没有web
这个镜像仓库的访问权限,所以只能尝试推送到nigelpoulton
这个二级命名空间(Namespace)之下。因此需要使用nigelpoulton这个ID,为当前镜像重新打一个标签。
$ docker image tag web:latest nigelpoulton/web:latest
为镜像打标签命令的格式是docker image tag <current-tag> <new-tag>
,其作用是为指定的镜像添加一个额外的标签,并且不需要覆盖已经存在的标签。
再次执行docker image ls
命令,可以看到这个镜像现在有了两个标签,其中一个包含Docker ID nigelpoulton
。
$ docker image ls
REPO TAG IMAGE ID CREATED SIZE
web latest fc69fdc4c18e 10 secs ago 64.4MB
nigelpoulton/web latest fc69fdc4c18e 10 secs ago 64.4MB
现在将该镜像推送到Docker Hub。
$ docker image push nigelpoulton/web:latest
The push refers to repository [docker.io/nigelpoulton/web]
2444b4ec39ad: Pushed
ed8142d2affb: Pushed
d77e2754766d: Pushed
cd7100a72410: Mounted from library/alpine
latest: digest: sha256:68c2dea730...f8cf7478 size: 1160
前文中容器化的这个应用程序其实很简单,从app.js
这个文件内容中可以看出,这其实就是一个在8080
端口提供Web服务的应用程序。
下面的命令会基于web:latest
这个镜像,启动一个名为c1
的容器。该容器将内部的8080
端口与Docker主机的80
端口进行映射。这意味读者可以打开一个浏览器,在地址栏输入Docker主机的DNS名称或者IP地址,然后就能直接访问这个Web应用了。
注:
如果Docker主机已经运行了某个使用80端口的应用程序,读者可以在执行
docker container run
命令时指定一个不同的映射端口。例如,可以使用-p 5000:8000
参数,将Docker内部应用程序的8080端口映射到主机的5000端口。
$ docker container run -d --name c1 \
-p 80:8080 \
web:latest
-d
参数的作用是让应用程序以守护线程的方式在后台运行。-p 80:8080
参数的作用是将主机的80端口与容器内的8080端口进行映射。
接下来验证一下程序是否真的成功运行,并且对外提供服务的端口是否正常工作。
$ docker container ls
ID IMAGE COMMAND STATUS PORTS
49.. web:latest "node ./app.js" UP 6 secs 0.0.0.0:80->8080/tcp
为了方便阅读,本书只截取了命令输出内容的一部分。从上面的输出内容中可以看到,容器已经正常运行。需要注意的是,80端口已经成功映射到了8080之上,并且任意外部主机(0.0.0.0:80
)均可以通过80端口访问该容器。
6.APP测试
打开浏览器,在地址栏输入DNS名称或者IP地址,就能访问到正在运行的应用程序了。
如果没有出现这样的界面,尝试执行下面的检查来确认原因所在。
- 使用
docker container ls
指令来确认容器已经启动并且正常运行。容器名称是c1
,并且从输出内容中能看到0.0.0.0:80->8080/tcp
。 - 确认防火墙或者其他网络安全设置没有阻止访问Docker主机的
80
端口。
如此,应用程序已经容器化并成功运行了,庆祝一下吧!
下面是其中一些细节部分的回顾和总结。
Dockerfile中的注释行,都是以#开头的。
除注释之外,每一行都是一条指令(Instruction)。指令的格式是指令参数如下。
INSTRUCTION argument
指令是不区分大小写的,但是通常都采用大写的方式。这样Dockerfile的可读性会高一些。
Docker image build
命令会按行来解析Dockerfile中的指令并顺序执行。
部分指令会在镜像中创建新的镜像层,其他指令只会增加或修改镜像的元数据信息。
在上面的例子当中,新增镜像层的指令包括FROM
、RUN
以及COPY
,而新增元数据的指令包括EXPOSE
、WORKDIR
、ENV
以及ENTERPOINT
。关于如何区分命令是否会新建镜像层,一个基本的原则是,如果指令的作用是向镜像中增添新的文件或者程序,那么这条指令就会新建镜像层;如果只是告诉Docker如何完成构建或者如何运行应用程序,那么就只会增加镜像的元数据。
可以通过docker image history
来查看在构建镜像的过程中都执行了哪些指令。
$ docker image history web:latest
IMAGE CREATED BY SIZE
fc6..18e /bin/sh -c #(nop) ENTRYPOINT ["node" "./a... 0B
334..bf0 /bin/sh -c #(nop) EXPOSE 8080/tcp 0B
b27..eae /bin/sh -c npm install 14.1MB
932..749 /bin/sh -c #(nop) WORKDIR /src 0B
052..2dc /bin/sh -c #(nop) COPY dir:2a6ed1703749e80... 22.5kB
c1d..81f /bin/sh -c apk add --update nodejs nodejs-npm 46.1MB
336..b92 /bin/sh -c #(nop) LABEL maintainer=nigelp... 0B
3fd..f02 /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
/bin/sh -c #(nop) ADD file:093f0723fa46f6c... 4.15MB
在上面的输出内容当中,有两点是需要注意的。
首先,每行内容都对应了Dockerfile中的一条指令(顺序是自下而上)。CREATE BY
这一列中还展示了当前行具体对应Dockerfile中的哪条指令。
其次,从这个输出内容中,可以观察到只有4条指令会新建镜像层(就是那些SIZE列对应的数值不为零的指令),分别对应Dockerfile中的FROM
、RUN
以及COPY
指令。虽然其他指令看上去跟这些新建镜像层的指令并无区别,但实际上它们只在镜像中新增了元数据信息。这些指令之所以看起来没有区别,是因为Docker对之前构建镜像层方式的兼容。
读者可以通过执行docker image inspect
指令来确认确实只有4个层被创建了。
$ docker image inspect web:latest
<Snip>
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:cd7100...1882bd56d263e02b6215",
"sha256:b3f88e...cae0e290980576e24885",
"sha256:3cfa21...cc819ef5e3246ec4fe16",
"sha256:4408b4...d52c731ba0b205392567"
]
},
使用FROM
指令引用官方基础镜像是一个很好的习惯,这是因为官方的镜像通常会遵循一些最佳实践,并且能帮助使用者规避一些已知的问题。除此之外,使用FROM
的时候选择一个相对较小的镜像文件通常也能避免一些潜在的问题。
读者也可以观察docker image build
命令具体的输出内容,了解镜像构建的过程。在下面的片段中,可以看到基本的构建过程是,运行临时容器>在该容器中运行Dockerfile中的指令>将指令运行结果保存为一个新的镜像层>删除临时容器。
Step 3/8 : RUN apk add --update nodejs nodejs-npm
---> Running in e690ddca785f << Run inside of temp container
fetch http://dl-cdn...APKINDEX.tar.gz
fetch http://dl-cdn...APKINDEX.tar.gz
(1/10) Installing ca-certificates (20171114-r0)
<Snip>
OK: 61 MiB in 21 packages
---> c1d31d36b81f << Create new layer
Removing intermediate container << Remove temp container
Step 4/8 : COPY . /src
1.利用构建缓存
Docker的构建过程利用了缓存机制。观察缓存效果的一个方法,就是在一个干净的Docker主机上构建一个新的镜像,然后再重复同样的构建。第一次构建会拉取基础镜像,并构建镜像层,构建过程需要花费一定时间;第二次构建几乎能够立即完成。这就是因为第一次构建的内容(如镜像层)能够被缓存下来,并被后续的构建过程复用。
docker image build
命令会从顶层开始解析Dockerfile中的指令并逐行执行。而对每一条指令,Docker都会检查缓存中是否已经有与该指令对应的镜像层。如果有,即为缓存命中(Cache Hit),并且会使用这个镜像层;如果没有,则是缓存未命中(Cache Miss),Docker会基于该指令构建新的镜像层。缓存命中能够显著加快构建过程。
下面通过实例演示其效果。
示例用的Dockerfile如下。
FROM alpine
RUN apk add --update nodejs nodejs-npm
COPY . /src
WORKDIR /src
RUN npm install
EXPOSE 8080
ENTRYPOINT ["node", "./app.js"]
第一条指令告诉Docker使用alpine:latest
作为基础镜像。如果主机中已经存在这个镜像,那么构建时会直接跳到下一条指令;如果镜像不存在,则会从Docker Hub(docker.io)拉取。
下一条指令(RUN apk...
)对镜像执行一条命令。此时,Docker会检查构建缓存中是否存在基于同一基础镜像,并且执行了相同指令的镜像层。在此例中,Docker会检查缓存中是否存在一个基于alpine:latest
镜像且执行了RUN apk add --update nodejs nodejs-npm
指令构建得到的镜像层。
如果找到该镜像层,Docker会跳过这条指令,并链接到这个已经存在的镜像层,然后继续构建;如果无法找到符合要求的镜像层,则设置缓存无效并构建该镜像层。此处“设置缓存无效”作用于本次构建的后续部分。也就是说Dockerfile中接下来的指令将全部执行而不会再尝试查找构建缓存。
假设Docker已经在缓存中找到了该指令对应的镜像层(缓存命中),并且假设这个镜像层的ID是AAA
。
下一条指令会复制一些代码到镜像中(COPY . /src
)。因为上一条指令命中了缓存,Docker会继续查找是否有一个缓存的镜像层也是基于AAA
层并执行了COPY . /src
命令。如果有,Docker会链接到这个缓存的镜像层并继续执行后续指令;如果没有,则构建镜像层,并对后续的构建操作设置缓存无效。
假设Docker已经有一个对应该指令的缓存镜像层(缓存命中),并且假设这个镜像层的ID是BBB
。
那么Docker将继续执行Dockerfile中剩余的指令。
理解以下几点很重要。
首先,一旦有指令在缓存中未命中(没有该指令对应的镜像层),则后续的整个构建过程将不再使用缓存。在编写Dockerfile时须特别注意这一点,尽量将易于发生变化的指令置于Dockerfile文件的后方执行。这意味着缓存未命中的情况将直到构建的后期才会出现——从而构建过程能够尽量从缓存中获益。
通过对docker image build
命令加入--nocache=true
参数可以强制忽略对缓存的使用。
还有一点也很重要,那就是COPY
和ADD
指令会检查复制到镜像中的内容自上一次构建之后是否发生了变化。例如,有可能Dockerfile中的COPY . /src
指令没有发生变化,但是被复制的目录中的内容已经发生变化了。
为了应对这一问题,Docker会计算每一个被复制文件的Checksum值,并与缓存镜像层中同一文件的checksum进行对比。如果不匹配,那么就认为缓存无效并构建新的镜像层。
2.合并镜像
合并镜像并非一个最佳实践,因为这种方式利弊参半。
总体来说,Docker会遵循正常的方式构建镜像,但之后会增加一个额外的步骤,将所有的内容合并到一个镜像层中。
当镜像中层数太多时,合并是一个不错的优化方式。例如,当创建一个新的基础镜像,以便基于它来构建其他镜像的时候,这个基础镜像就最好被合并为一层。
缺点是,合并的镜像将无法共享镜像层。这会导致存储空间的低效利用,而且push和pull操作的镜像体积更大。
执行docker image build
命令时,可以通过增加--squash
参数来创建一个合并的镜像。
3.使用no-install-recommends
在构建Linux镜像时,若使用的是APT包管理器,则应该在执行apt-get install
命令时增加no-install-recommends
参数。这能够确保APT仅安装核心依赖(Depends
中定义)包,而不是推荐和建议的包。这样能够显著减少不必要包的下载数量。
4.不要安装MSI包(Windows)
在构建Windows镜像时,尽量避免使用MSI包管理器。因其对空间的利用率不高,会大幅增加镜像的体积。
应用的容器化——命令
docker image build
命令会读取Dockerfile,并将应用程序容器化。使用-t
参数为镜像打标签,使用-f
参数指定Dockerfile的路径和名称,使用-f
参数可以指定位于任意路径下的任意名称的Dockerfile。构建上下文是指应用文件存放的位置,可能是本地Docker主机上的一个目录或一个远程的Git库。- Dockerfile中的FROM指令用于指定要构建的镜像的基础镜像。它通常是Dockerfile中的第一条指令。
- Dockerfile中的RUN指令用于在镜像中执行命令,这会创建新的镜像层。每个 RUN指令创建一个新的镜像层。
- Dockerfile中的COPY指令用于将文件作为一个新的层添加到镜像中。通常使用 COPY指令将应用代码赋值到镜像中。
- Dockerfile中的EXPOSE指令用于记录应用所使用的网络端口。
- Dockerfile中的ENTRYPOINT指令用于指定镜像以容器方式启动后默认运行的程序。
- 其他的Dockerfile指令还有LABEL、ENV、ONBUILD、HEALTHCHECK、CMD等。
第9章 使用Docker Compose部署应用
Docker Compose与Docker Stack非常类似。本章主要介绍Docker Compose,它能够在Docker节点上,以单引擎模式(Single-Engine Mode)进行多容器应用的部署和管理。
在Linux上安装Docker Compose分为两步。首先使用curl
命令下载二进制文件,然后使用chmod
命令将其置为可运行。
Docker Compose在Linux上的使用,同样需要先安装有Docker引擎。
如下命令会下载1.18.0
版本的Docker Compose到/usr/bin/local
。请在GitHub上查找想安装的版本,并替换URL中的1.18.0
。
下面的示例是一条写成多行的命令,如果要将其合并为一行,请删掉反斜杠(\
)。
$ curl -L \
https://github.com/docker/compose/releases/download/1.18.0/docker-compose-`\
uname -s`-`uname -m` \
-o /usr/local/bin/docker-compose
% Total % Received Time Time Time Current
Total Spent Left Speed
100 617 0 617 0 --:--:-- --:--:-- --:--:-- 1047
100 8280k 100 8280k 0 0:00:03 0:00:03 --:--:-- 4069k
下载docker-compose二进制文件后,使用如下命令使其可执行。
$ chmod +x /usr/local/bin/docker-compose
检查安装情况以及版本。
$ docker-compose --version
docker-compose version 1.18.0, build 8dd22a9
现在就可以在Linux上使用Docker Compose了。
Compose文件
Docker Compose使用YAML文件来定义多服务的应用。YAML是JSON的一个子集,因此也可以使用JSON。不过本章中的例子将全部采用YAML。
Docker Compose默认使用文件名docker-compose.yml
。当然,用户也可以使用-f
参数指定具体文件。
如下是一个简单的Compose文件的示例,它定义了一个包含两个服务(web-fe
和redis
)的小型Flask应用。这是一个能够对访问者进行计数并将其保存到Redis的简单的Web服务。本书中将其命名为counter-app
,并将其作为后续章节的示例应用程序。
version: "3.5"
services:
web-fe:
build: .
command: python app.py
ports:
- target: 5000
published: 5000
networks:
- counter-net
volumes:
- type: volume
source: counter-vol
target: /code
redis:
image: "redis:alpine"
networks:
counter-net:
networks:
counter-net:
volumes:
counter-vol:
在深入研究之前粗略观察文件的基本结构,首先可以注意到,它包含4个一级key:version
、services
、networks
、volumes
。
除此之外的其他key,这里暂时不展开讨论。
version
是必须指定的,而且总是位于文件的第一行。它定义了Compose文件格式(主要是API)的版本。建议使用最新版本。
注意,version
并非定义Docker Compose或Docker引擎的版本号。如果希望了解关于Docker引擎、Docker Compose以及Compose文件之间的版本兼容性信息,请搜索“Compose file versions and upgrading”。
第10章 Docker Swarm
概括来说,Swarm有两个核心组件。
- 安全集群。
- 编排引擎。
- Docker Swarm包含两方面:一个企业级的Docker安全集群,以及一个微服务应用编排引擎。
-
集群方面,Swarm将一个或多个Docker节点组织起来,使得用户能够以集群方式管理它们。Swarm默认内置有加密的分布式集群存储(encrypted distributed cluster store)、加密网络(Encrypted Network)、公用TLS(Mutual TLS)、安全集群接入令牌Secure Cluster Join Token)以及一套简化数字证书管理的PKI(Public Key Infrastructure)。用户可以自如地添加或删除节点,这非常棒!
编排方面,Swarm提供了一套丰富的API使得部署和管理复杂的微服务应用变得易如反掌。通过将应用定义在声明式配置文件中,就可以使用原生的Docker命令完成部署。此外,甚至还可以执行滚动升级、回滚以及扩缩容操作,同样基于简单的命令即可完成。
-
Docker Swarm——命令
docker swarm init
命令用户创建一个新的Swarm。执行该命令的节点会成为第一个管理节点,并且会切换到Swarm模式。docker swarm join-token
命令用于查询加入管理节点和工作节点到现有Swarm时所使用的命令和Token。要获取新增管理节点的命令,请执行docker swarm join-token manager
命令;要获取新增工作节点的命令,请执行docker swarm join-token worker
命令。docker node ls
命令用于列出Swarm中的所有节点及相关信息,包括哪些是管理节点、哪个是主管理节点。docker service create
命令用于创建一个新服务。docker service ls
命令用于列出Swarm中运行的服务,以及诸如服务状态、服务副本等基本信息。docker service ps <service>
命令会给出更多关于某个服务副本的信息。docker service inspect
命令用于获取关于服务的详尽信息。附加--pretty
参数可限制仅显示重要信息。docker service scale
命令用于对服务副本个数进行增减。docker service update
命令用于对运行中的服务的属性进行变更。docker service logs
命令用于查看服务的日志。docker service rm
命令用于从Swarm中删除某服务。该命令会在不做确认的情况下删除服务的所有副本,所以使用时应保持警惕。-
第11章 Docker网络
- 基础理论。
- 单机桥接网络。
- 多机覆盖网络。
- 接入现有网络。
- 服务发现。
- Ingress网络。
-
在顶层设计中,Docker网络架构由3个主要部分构成:CNM、Libnetwork和驱动。
CNM是设计标准。在CNM中,规定了Docker网络架构的基础组成要素。
Libnetwork是CNM的具体实现,并且被Docker采用。Libnetwork通过Go语言编写,并实现了CNM中列举的核心组件。
驱动通过实现特定网络拓扑的方式来拓展该模型的能力。
-
CNM定义了3个基本要素:沙盒(Sandbox)、终端(Endpoint)和网络(Network)。
-
沙盒是一个独立的网络栈。其中包括以太网接口、端口、路由表以及DNS配置。
终端就是虚拟网络接口。就像普通网络接口一样,终端主要职责是负责创建连接。在CNM中,终端负责将沙盒连接到网络。
网络是802.1d网桥(类似大家熟知的交换机)的软件实现。因此,网络就是需要交互的终端的集合,并且终端之间相互独立。
-
终端与常见的网络适配器类似,这意味着终端只能接入某一个网络。因此,如果容器需要接入到多个网络,就需要多个终端。
-
Docker网络——命令
- Docker网络有自己的子命令,主要包括以下几种。
docker network ls
用于列出运行在本地Docker主机上的全部网络。docker network create
创建新的Docker网络。默认情况下,在Windows上会采用NAT
驱动,在Linux上会采用Bridge
驱动。读者可以使用-d
参数指定驱动(网络类型)。docker network create -d overlay overnet
会创建一个新的名为overnet的覆盖网络,其采用的驱动为Docker Overlay
。docker network inspect
提供Docker网络的详细配置信息。docker network prune
删除Docker主机上全部未使用的网络。docker network rm
删除Docker主机上指定网络。