1. Docker 简介
1.1 Docker 是什么
Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到其他机器上。容器可以简单地理解为是一个修改过的运行时环境,可以隔离所有的资源(除非明确允许)。UNIX 中使用容器隔离资源已经有很长一段时间了,但是直接使用容器难度大,配置复杂,容易出错。Docker 使用现有的容器技术,根据最佳实践提供一致的 Docker 构建方案,让大家可以很轻松的使用容器隔离资源,提供更强的安全性。目前,Docker 可原生运行在 Linux 上,在 OS X 和 Windows 环境中通过单独的虚拟机也可以运行。
1.2 在隔离的容器中运行软件
Docker 在 linux 上运行容器的结构如下图所示:
命令行工具或 CLI 在被称为用户空间的内存中运行,就像是在操作系统上运行的其他程序。运行 Docker 可以认为是在用户空间运行着两个程序:一个是 Docker 守护进程,另一个是 DockerCLI,DockerCLI 是与用户交互的 Docker 程序。上图也显示了三个运行着的容器,每个都是以 Docker 守护程序的子进程运行,封装在容器中。在容器中运行的程序只能访问该容器内部的内存空间和资源(除非特别规定可以访问容器外部资源)。
1.3 分发容器
Docker 可以执行、复制和轻松的分发容器,Docker 通过一种打包和分发的软件完成传统容器的封装,这个用来充当容器(Container)分发角色的组件被称为 <font color=red> 镜像(Image)</font>。镜像与容器的关系,个人认为类似类(Class)和实例的关系。基于一个类可以创建多个实例,每个实例都有独立的资源。而基于一个镜像可以创建多个容器,每个容器也互不干扰(如果不考虑容器连接等特殊情况)
2. Docker 镜像
2.1 Docker 镜像简介
Docker 镜像是一个存在于系统上的实体,镜像内部是一个精简的操作系统和应用运行所需要的文件以及所有依赖(镜像不包含内核,容器都是共享所在 Docker 主机的内核)。镜像仓库(Image Repository)是一个有名字的镜像桶,用于存放镜像,将镜像集中存放就是为了方便大家获取所需的镜像。镜像仓库又有一个镜像仓库服务(Image Registry)来管理,Docker 客户端的镜像仓库服务是可配置的,默认是 Docker Hub。在每个镜像仓库中,标签是唯一指定镜像的重要途径,也是一种创建有用别名的遍历方法。一个标签在一个仓库中只能被应用到单个镜像,但一个镜像可以有多个标签。下图展示了镜像仓库服务、镜像仓库、镜像之间的关系:镜像仓库服务管理多个镜像仓库,每个镜像仓库又可以包含多个镜像。
2.2 Docker 镜像常见操作
2.2.1 搜索拉取镜像
Docker 镜像存储在镜像仓库中,可以从中搜索和拉去镜像。
如下为搜索镜像示例,其参数说明:
- NAME: 镜像仓库源的名称
- DESCRIPTION: 镜像的描述
- OFFICIAL: 是否 docker 官方发布
- STARS: 类似 Github 里面的 star,表示点赞、喜欢的意思。
- AUTOMATED: 自动构建。
$ docker search ubuntu
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
ubuntu Ubuntu is a Debian-based Linux operating sys… 10873 [OK]
dorowu/ubuntu-desktop-lxde-vnc Docker image to provide HTML5 VNC interface … 422 [OK]
rastasheep/ubuntu-sshd Dockerized SSH service, built on top of offi… 244 [OK]
consol/ubuntu-xfce-vnc Ubuntu container with "headless" VNC session… 217 [OK]
ubuntu-upstart Upstart is an event-based replacement for th… 108 [OK]
...
拉取镜像,可以指定标签拉取,不指定镜像标签默认拉取标签为 lastest 的镜像
$ docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
d51af753c3d3: Downloading [========================================>] 23.13MB/28.56MB
fc878cd0a91c: Download complete
6154df8ff988: Download complete
fee5db0ff82f: Waiting
latest: Pulling from library/ubuntu
d51af753c3d3: Pull complete
fc878cd0a91c: Pull complete
6154df8ff988: Pull complete
fee5db0ff82f: Pull complete
Digest: sha256:747d2dbbaaee995098c9792d99bd333c6783ce56150d1b11e333bbceed5c54d7
Status: Downloaded newer image for ubuntu:latest
docker.io/library/ubuntu:latest
指定标签拉取镜像:
$ docker pull ubuntu:18.04
18.04: Pulling from library/ubuntu
23884877105a: Pull complete
bc38caa0f5b9: Pull complete
2910811b6c42: Pull complete
36505266dcc6: Pull complete
Digest: sha256:3235326357dfb65f1781dbc4df3b834546d8bf914e82cce58e6e6b676e23ce8f
Status: Downloaded newer image for ubuntu:18.04
docker.io/library/ubuntu:18.04
2.2.2 查看镜像
查看有哪些镜像:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 1d622ef86b13 2 weeks ago 73.9MB
ubuntu 18.04 c3c304cb4f22 2 weeks ago 64.2MB
查看镜像详细信息:
$ docker inspect ubuntu:18.04
[
{
"Id": "sha256:c3c304cb4f22ceb8a6fcc29a0cd6d3e4383ba9eb9b5fb552f87de7c0ba99edac",
"RepoTags": ["ubuntu:18.04"],
"RepoDigests": ["ubuntu@sha256:3235326357dfb65f1781dbc4df3b834546d8bf914e82cce58e6e6b676e23ce8f"],
"Parent": "","Comment":"",
"Created": "2020-04-24T01:07:05.743682549Z",
"Container": "f607979929fd999f71996754275dc5058e7345748f52d58ba72b6baf449c1fb2",
... # 内容较长,这里省略了
}
]
2.2.3 制作镜像
镜像的制作有两种方法:从本地容器生成镜像和用 Dockerfile 生成镜像,涉及内容较多,详见第 6 节。
2.2.4 镜像标签
# 给 ubuntu:18.04 添加一个新标签 ubuntu:v18
$ docker tag ubuntu:18.04 ubuntu:v18
# 查看镜像,发现新标签 ubuntu:v18 和 ubuntu:18.04 的镜像 ID 相同,实际上 docker tag 只是创建一个标签,和原始标签指向同一个镜像。$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 1d622ef86b13 2 weeks ago 73.9MB
ubuntu 18.04 c3c304cb4f22 2 weeks ago 64.2MB
ubuntu v18 c3c304cb4f22 2 weeks ago 64.2MB
2.2.5 删除镜像
删除镜像要注意在基于镜像启动的所有容器全部停止之前,镜像是无法被删除的。
# 删除镜像要指定标签,不然默认删除标签为 latest 的镜像。可以指指定镜像 ID 进行删除。$ docker image rm ubuntu:18.04
Untagged: ubuntu:18.04
# 查看镜像确实删除了
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 1d622ef86b13 2 weeks ago 73.9MB
ubuntu v18 c3c304cb4f22 2 weeks ago 64.2MB
2.3 镜像分发
2.3.1 通过镜像仓库分发
把镜像上传到镜像仓库,使用者从镜像仓库拉取镜像。
Usage: docker push [OPTIONS] NAME[:TAG]
Push an image or a repository to a registry
Options:
--disable-content-trust Skip image signing (default true)
2.3.2 手动分发
镜像是个实体,可以存储在磁盘上,可以通过 U 盘传递。
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 1d622ef86b13 2 weeks ago 73.9MB
# 将镜像 ubuntu:latest 保存为 ubuntu.tar。这样就可以通过传递 ubuntu.tar 来分发镜像了。$ docker save -o ubuntu.tar ubuntu:latest
# 删除镜像 ubuntu:lastest
$ docker image rm ubuntu:latest
Untagged: ubuntu:latest
Untagged: ubuntu@sha256:747d2dbbaaee995098c9792d99bd333c6783ce56150d1b11e333bbceed5c54d7
Deleted: sha256:1d622ef86b138c7e96d4f797bf5e4baca3249f030c575b9337638594f2b63f01
Deleted: sha256:279e836b58d9996b5715e82a97b024563f2b175e86a53176846684f0717661c3
Deleted: sha256:39865913f677c50ea236b68d81560d8fefe491661ce6e668fd331b4b680b1d47
Deleted: sha256:cac81188485e011e56459f1d9fc9936625a1b62cacdb4fcd3526e5f32e280387
Deleted: sha256:7789f1a3d4e9258fbe5469a8d657deb6aba168d86967063e9b80ac3e1154333f
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
# 从 ubuntu.tar 加载镜像
$ docker load -i ubuntu.tar
7789f1a3d4e9: Loading layer [==================================================>] 75.22MB/75.22MB
9e53fd489559: Loading layer [==================================================>] 1.011MB/1.011MB
2a19bd70fcd4: Loading layer [==================================================>] 15.36kB/15.36kB
8891751e0a17: Loading layer [==================================================>] 3.072kB/3.072kB
Loaded image: ubuntu:latest
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 1d622ef86b13 2 weeks ago 73.9MB
2.4 镜像分层
Docker 镜像由一些松耦合的只读镜像层组成,Docker 负责堆叠这些镜像层,并且将它们表示为单个统一的对象。所有的 Docker 镜像都起始于一个基础镜像层,当进行修改或增加新的内容时,就会在当前镜像层之上创建新的镜像层。在添加额外的镜像层的同时,镜像始终保持是当前所有镜像层的组合。举个例子,假如一个镜像由两层,第一层包含文件 1、2、3,第二层包含文件 4、5、6,那么从系统角度看,这个镜像就包含 1、2、3、4、5、6 这六个文件。下图展示一个稍微复杂的三层镜像,其中文件 7 是文件 5 的一个更新版本。这种情况下,上层镜像层中的文件覆盖了底层镜像层中的文件。这样就使得文件的更新版本作为一个新镜像层添加到镜像当中(个人觉得最底层镜像层是一个基础层,然后上面的每层镜像层就像是一个步骤,从下往上一次执行每个步骤,最终的结果就是整个镜像对外显示的信息)。
时刻记着:镜像层是只读的。这样的话多个镜像之间就可以共享镜像层了,这样可以节省空间并提升性能。最直观的就是拉取镜像的时候,如果该镜像包含的某些镜像层已经存在了,就不会再拉取这些镜像层了。
镜像本身就是一个配置对象,其中包含了镜像层的列表以及一些元数据信息。镜像层才是实际数据存储的地方(比如文件等,镜像层之间是完全独立的,并没有从属于某个镜像集合的概念)。镜像的唯一标识是一个加密 ID,即配置对象本身的散列值。每个镜像层也有一个加密 ID 区分,其值为镜像层本身内容的散列值。这意味着修改镜像的内容或其中任意的镜像层都会导致加密散列值的变化。所以,镜像和其镜像层都是不可变的,任何改动都能很轻松的被辨别(这就是所谓的内容散列)。
2.5 多架构镜像
多架构镜像(Multi-architecture Image)是为了解决镜像支持不同架构(Linux、Windows、ARM 等)的问题。为了实现这个特性,镜像仓库服务 API 支持两种重要的结构:Manifest 列表和 Manifest。Manifest 列表是指某个镜像标签支持的架构列表,其支持的每种架构都有自己的 Manifest 定义,其中列举了镜像的构成。如下图所示,图中左侧是 Manifest 列表,其中包含了该镜像支持的每种架构。Manifest 列表的每一项都有一个箭头,指向具体的 Manifest,其中包含了镜像配置和镜像层数据。
多架构镜像原理:在拉取镜像的时候,Docker 客户端会调用 Docker 镜像仓库服务的 API 完成拉取。如果该镜像有 Manifest 列表,则 Docker 客户端会找到当前主机架构对应的 Manifest 并解析出组成该镜像的镜像层加密 ID,然后从镜像仓库中拉取每个镜像层。有些软件是不能跨平台的(可能别人不需要),所以 Manifest 列表是可选的,在没有 Manifest 列表的情况下,镜像仓库服务会返回普通的 Manifest。
3. Docker 容器
3.1 Docker 容器简介
容器是镜像的运行时实例,可以从单个镜像启动一个或多个容器。相对于虚拟机,容器较轻量级,而且容器启动非常快————与虚拟机运行在完整的操作系统之上相比,<font color=red> 容器会共享其所在主机的操作系统 / 内核 </font>(一个很简单是证明方法就是在容器内部查看下进程,在容器所在主机的操作系统上也能找到容器内部运行的进程(由于容器拥有隔离的 PID,所以进程号会不同,但是进程确是相同的))。Docker 在创建容器的时候会为每个容器分配一个唯一的标识符,也会分配一个人性化的名称(如果用户没有指定名称的话)。Docker 容器有四种状态:运行中(Up)、暂停中(Paused)、已退出(Exited)、重新启动中(一种临时状态),状态转移图如下:
3.2 Docker 容器常见操作
3.2.1 启动停止 Docker 容器
启动容器时的常用选项介绍:-i
: 保持标准输入打开;-t
:分配一个 tty 终端;-d
:后台运行容器;-e MYENV=123
:注入环境变量;-p 9000:80
:发布到主机 9000 端口的请求都会映射到容器的 80 端口;-v /opt/soft:/soft
:将主机的 /opt/soft 目录挂载到容器文件系统的 /soft 目录(第 5 节有详细介绍)。这里有一个小技巧:如果是前台启动的容器,按下 Ctrl-PQ 组合键则会退出容器但不会终止容器运行。
# docker 容器运行、停止 / 启动、暂停 / 重启
docker container run
docker container stop
docker container start
docker container pause
docker container restart
3.2.2 查看 Docker 容器
# 列出运行中的容器
docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b601d970cd1d web:v1 "node app.js" 4 seconds ago Up 2 seconds zealous_leakey
# 列出所有容器
$ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b601d970cd1d web:v1 "node app.js" 27 seconds ago Up 26 seconds zealous_leakey
9e3328fa1308 ubuntu:latest "/bin/bash" 16 minutes ago Exited (0) 16 minutes ago silly_nash
3e45ce78a31d ubuntu:latest "/bin/bash" 17 minutes ago Exited (130) 16 minutes ago beautiful_easle
# 查看容器配置细节和运行时信息
docker inspect zealous_leakey
[
{
"Id": "b601d970cd1d051df92f8dcb2f8b9acd39d0a1e9a0138db6e597b690f134b57b",
"Created": "2020-05-11T03:01:21.064780355Z",
"Path": "node",
"Args": ["app.js"],
"State": {
"Status": "running",
"Running": true,
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": 23692,
"ExitCode": 0,
"Error": "","StartedAt":"2020-05-11T03:01:22.4655386Z","FinishedAt":"0001-01-01T00:00:00Z"},"Image":"sha256:84f04d8b5d32a6d5b6dee7a67d2b25dcf9e12a5c6e36039353baf75d551c4dd1",
...
}
]
3.2.3 连接到容器的 Shell
docker container exec 允许用户在运行状态的容器中启动一个新进程。该命令在将 Docker 主机 Shell 连接到一个运行中容器终端时非常有用。docker container exec -it <container-name or container-id> bash
命令会在容器内部启动一个 Bash Shell 进程,并连接到该 Shell。为了是该命令生效,用于创建容器的镜像必须包含 Bash shell。
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b601d970cd1d web:v1 "node app.js" 2 minutes ago Up 2 minutes zealous_leakey
$ docker container exec -it zealous_leakey bash
root@b601d970cd1d:/#
3.2.4 删除容器
# 需要先停止容器才能删除
docker rm
# 如果加 - f 表示强杀容器然后删除
docker rm -f
3.3 Docker 容器的重启策略
3.3.1 Docker 容器的自动重启策略
通常建议在运行容器时配置好重启策略。这是容器的一种自我修复能力,可以在指定事件或者错误后重启来完成自我修复。重启策略应用于每个容器,在启动容器的时候传入 --restart < 重启策略 >
就可以了。常见的容器重启策略有三种:always、unless-stopped、on-failure。always 策略是一种简单的方式,除非容器被明确停止(比如通过 docker container stop 命令),否则该策略会一直尝试重启处于停止状态的容器。使用 always 策略要注意,当 Docker daemon 重启的时候,停止的容器也会被重启。always 和 unless-stopped 的最大区别,就是那些使用 unless-stopped 策略并处于已退出状态的容器,不会在 Docker daemon 重启的时候被重启。on-failure 策略会在退出容器并且返回值不是 0 的时候重启容器,就算容器处于已退出状态,在 Docker daemon 重启的时候,容器也会重启。
3.3.2 使用 init/systemctl 和 supervisor 监控容器内的进程
利用操作系统自带的守护进程(service、systemctl 等)或者第三方进程监控软件(supervisor)来监控容器内的进程。创建镜像的时候配置好这些服务,启动容器的时候只需要启动这些监控程序就好,由监控程序去负责启动应用程序。
4. Docker 网络
4.1 Docker 网络容器模型(CNM)介绍
Docker 有四种网络容器模型:Closed 容器、Bridged 容器(默认)、Joined 容器、Open 容器,所有的 Docker 容器都要符合这四种模型中的一种。这些模型定义了一个容器如何与其他的本地容器、主机网络进行通信。下图形象地描绘了每一个模型,最强大(隔离程度最高)的在最左边,最脆弱的在最右边:
4.2 Closed 容器
启动容器的时候添加 --network none
就会创建一个 Closed 容器。Closed 容器不允许任何的网络流量,运行在这种容器中的进程只能访问本地回环接口。
# 可以看到,Closed 容器只有一个回环接口
$ docker run --name closed-container --network none node:7 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
4.3 Bridged 容器
Bridged 容器是 Docker 运行容器时的默认网络容器模型,也可以在启动容器的时候添加 --network bridge
明确创建一个 Closed 容器。Bridged 容器有两个接口,一个是本地回环接口,另一个接口通过网桥连接到主机网络。Bridged 容器可以通过主机网络访问任何主机网络能访问的外部网络。
# 一个本地回环接口,一个桥接到主机网络的接口
$ docker run --network bridge node:7 ip addr; ping -c 2 www.baidu.com
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
255: eth0@if256: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
PING www.a.shifen.com (180.101.49.12) 56(84) bytes of data.
64 bytes from 180.101.49.12 (180.101.49.12): icmp_seq=1 ttl=50 time=29.0 ms
64 bytes from 180.101.49.12 (180.101.49.12): icmp_seq=2 ttl=50 time=28.4 ms
--- www.a.shifen.com ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 28.441/28.761/29.082/0.362 ms
4.4 Joined 容器
启动容器的时候添加 --network container:< 需要共享网络的容器 >
就会创建一个 Joined 容器。注意这里在 --network
选项后面需要添加一个 container:
表示从现有的容器创建一个 Joined 容器,如果不加 container:
则 Docker 守护进程会认为是要从一个 network
创建容器,关于 network
目前使用不多,这里暂不介绍。使用 Joined 容器模型的所有容器共享一个网络栈,在这种情况下,这些容器之间没有任何网络隔离。Joined 容器通过将某一个容器接口的访问权限提供给另外一个新的容器来构建。
# 创建一个 closed 容器,看下其内部网络状态,可以看到容器内部监听的是 39439 端口
$ docker run --name join-base-container --network none -d alpine:latest nc -l 8000
e2907c7a889d209734f63309a5351687ac2761489e129cd6a7d6a392234a3cde
$ docker exec join-base-container netstat -al
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:39439 0.0.0.0:* LISTEN
Active UNIX domain sockets (servers and established)
Proto RefCnt Flags Type State I-Node Path
# 创建一个基于上面容器的 joined 容器,发现其共享了上面的 close 容器中的网络
$ docker run --network container:join-base-container alpine:latest netstat -al
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:39439 0.0.0.0:* LISTEN
Active UNIX domain sockets (servers and established)
Proto RefCnt Flags Type State I-Node Path
4.5 Open 容器
启动容器的时候添加 --network host
就会创建一个 Open 容器。Open 容器没有网络容器,共享主机网络,并且对主机网络有完全的访问权。如下,启动一个 Open 容器,在容器内部可以看到主机所有的网络接口。
# 先看下主机有哪些网络接口
$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 8c:ec:4b:ab:6d:38 brd ff:ff:ff:ff:ff:ff
inet 10.55.2.40/24 brd 10.55.2.255 scope global dynamic noprefixroute eno1
valid_lft 522699sec preferred_lft 522699sec
inet6 fe80::7b57:66c8:6d06:833d/64 scope link noprefixroute
valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:cc:a9:3a:68 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:ccff:fea9:3a68/64 scope link
valid_lft forever preferred_lft forever
4: vboxnet0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 0a:00:27:00:00:00 brd ff:ff:ff:ff:ff:ff
inet 192.168.99.1/24 brd 192.168.99.255 scope global vboxnet0
valid_lft forever preferred_lft forever
inet6 fe80::800:27ff:fe00:0/64 scope link
valid_lft forever preferred_lft forever
# 发现 open 容器里能访问主机所有的网络接口
$ docker run --network host node:7 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 8c:ec:4b:ab:6d:38 brd ff:ff:ff:ff:ff:ff
inet 10.55.2.40/24 brd 10.55.2.255 scope global noprefixroute dynamic eno1
valid_lft 522693sec preferred_lft 522693sec
inet6 fe80::7b57:66c8:6d06:833d/64 scope link noprefixroute
valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:cc:a9:3a:68 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:ccff:fea9:3a68/64 scope link
valid_lft forever preferred_lft forever
4: vboxnet0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 0a:00:27:00:00:00 brd ff:ff:ff:ff:ff:ff
inet 192.168.99.1/24 brd 192.168.99.255 scope global vboxnet0
valid_lft forever preferred_lft forever
inet6 fe80::800:27ff:fe00:0/64 scope link
valid_lft forever preferred_lft forever
5. 卷和持久化数据
5.1 存储卷简介
一个主机或容器的目录树是由一组挂载点创建而成,这些挂载点描述了如何能构建出一个或多个文件系统。存储卷是容器目录树上的挂载点,存储卷提供了容器无关的数据管理方式。书上术语解释的比较模糊,个人理解如下:将主机磁盘的某个目录挂载到容器的文件系统(主机上的这个目录就称为存储卷),那么在容器内对这个挂载点的操作实际上都是对主机上那个目录的操作。比如将主机目录 /opt/soft 挂载到容器的 /soft 目录,那么容器中对 /soft 的所有操作实际上都是对主机目录 /opt/soft 的操作。
5.2 存储卷类型
存储卷有两种类型:绑定挂载存储卷和管理存储卷。绑定挂载存储卷使用用户提供的主机目录或文件,在启动容器时需要加入选项-v < 主机位置 >:< 容器挂载点 >
。管理存储卷使用有 Docker 守护进程控制的位置,被称为 Docker 管理空间,在启动容器时需要加入命令-v < 容器挂载点 >
,Docker 守护进程会自动在主机文件系统中创建一个目录来挂载到容器中指定的挂载掉点。存储卷类型如下图:
5.3 共享存储卷
多个容器共享同一个存储卷有两种方法,第一种方法是在启动容器时挂载到同一个主机目录。第二种方法启动新容器是加入选项--volume-from < 其他容器名称或 ID>
,下面演示下这种方法:
$ docker run --name fowler -v ~/example-books:/library/PoEAA -v /library/DSL alpine:latest echo "OK"
OK
$ docker run --name knuth -v /library/test1 -v /library/test2 -v /library/test3 alpine:latest echo "OK"
OK
# 下面使用 --volumes-from 基于容器 fowler 和容器 knuth 创建新容器,可以在容器内部访问容器 fowler 和容器 knuth 内部的存储卷
$ docker run --volumes-from fowler --volumes-from knuth alpine:latest ls -l /library
total 20
drwxr-xr-x 2 root root 4096 May 11 03:41 DSL
drwxr-xr-x 2 root root 4096 May 7 02:18 PoEAA
drwxr-xr-x 2 root root 4096 May 11 03:43 test1
drwxr-xr-x 2 root root 4096 May 11 03:43 test2
drwxr-xr-x 2 root root 4096 May 11 03:43 test3
5.4 管理卷删除
删除容器时加入 -v
选项即可删除管理卷。使用 Docker 命令是不能删除绑定存储卷的,因为绑定存储卷不在 Docker 守护进程管理范围内。
6. 应用的容器化
6.1 从本地容器创建镜像
从本地容器创建镜像比较简单,直接采用命令 docker commit < 容器名称或 ID> < 生成的镜像的名称 >
。但是要注意, 启动原始容器时附带的命令会被提交到新镜像中 ,下面将解释这句话。首先,我们拉取 ubuntu 镜像,然后执行docker run -it --name git-container ubuntu:latest /bin/bash
运行容器,在容器里面安装 git 然后退出容器,接着执行生成镜像。
$ docker commit git-container ubuntu-git:v1
sha256:c1f13209eb865c7b726a80cc570e13cf1ad2e37b6d614b54b21017ecb3881920
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu-git v1 c1f13209eb86 14 seconds ago 197MB
然后我们用新镜像 ubuntu-git 运行容器,发现好像什么都没发生。
$ docker run --rm ubuntu-git:v1
$
出现上面的现象是因为启动原始容器时附带的命令会被提交到新镜像中,而之前启动创建新镜像的容器时附带的命令是 /bin/bash。因此,当使用这个默认命令从新镜像启动一个容器时,他会启动一个 shell 并且立马停止它。
我们在创建新镜像时可以设置入口点(entrypoint),为了设置入口点,需要使用 --entrypoint < 入口点命令 >
选项重新运行一个容器,并从这个容器创建新的镜像。
# 基于 ubuntu-git:v1 镜像启动一个带入口点的容器
$ docker run --name base-entrypoint --entrypoint git ubuntu-git:v1
usage: git [--version] [--help] [-C <path>] [-c <name>=<value>]
[--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
[-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
[--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
<command> [<args>]
...
# 基于带入库点的容器创建镜像,保持镜像名称不变
$ docker commit base-entrypoint ubuntu-git:v1
sha256:7ab43fc4a1c911f4f2eeeb62f5a946f2b687303a423ae64451a7a846db7cf036
# 清除容器
$ docker rm base-entrypoint
base-entrypoint
# 从新镜像启动容器,可以看到已经自带了入口点
$ docker run --rm ubuntu-git:v1 version
git version 2.25.1
6.2 由 Dockerfile 创建镜像
6.2.1 Dockerfile 使用
先介绍下如何用 Dockerfile 来创建上一小节中从本地容器创建的 git 镜像。创建一个名为 Dockerfile
的文件,将下面的代码复制到文件中保存:
FROM ubuntu:latest
RUN apt-get update \
&& apt-get install -y git
ENTRYPOINT ["git"]
执行命令 docker build -t ubuntu-git:auto .
创建新镜像,这时候 docker image ls
可以看到刚刚创建的镜像。启动容器来验证新镜像:
$ docker build -t ubuntu-git:auto .
Sending build context to Docker daemon 2.048kB
Step 1/3 : FROM ubuntu:latest
---> 1d622ef86b13
Step 2/3 : RUN apt-get update && apt-get install -y git
---> Running in 24ca27336db7
Removing intermediate container 24ca27336db7
---> 6e0f2d7b38e1
Step 3/3 : ENTRYPOINT ["git"]
---> Running in bd1435f0e46d
Removing intermediate container bd1435f0e46d
---> 88475690ecbc
Successfully built 88475690ecbc
Successfully tagged ubuntu-git:auto
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu-git auto 88475690ecbc 3 minutes ago 197MB
ubuntu latest 1d622ef86b13 2 weeks ago 73.9MB
$ docker run --rm ubuntu-git:auto version
git version 2.25.1
6.2.2 常用 Dockerfile 指令
使用 Dockerfile 应注意:每个 Dockerfile 指令都会导致一个新镜像层被创建,所以指令应该尽可能合并。镜像的创建是由 Docker 守护进程完成的,而不是 Docker 客户端。Docker 客户端会将上下文发送到 Docker 守护进程,并且由 Docker 守护进程负责创建镜像,所以在写 Dockerfile 的时候不要把无关的数据添加到镜像。
RUN、ENTRYPOINT、CMD 等部分指令有两种格式:shell 格式和 exec 格式。shell 格式类似于一个 shell 命令,例如入口点设置为 ENTRYPOINT pyhon /app/run.py
,其中的参数以空格为界限分隔开来。exec 格式是一个字符串数组,其中第一个值是要执行的命令,剩下的是参数。shell 格式指定的命令将会作为默认 shell 的一个参数来执行。具体点说,指定的命令在运行时会以/bin/sh -c "python /app/run.py"
的形式执行。最重要的是,如果 ENTRYPOINT 使用的 shell 格式,那么 CMD 指令提供的所有参数和运行容器时指定的额外参数都会被忽略。尽可能使用 exec 格式是个最佳实践。
- FROM
FROM 指令用于指定基础镜像层。
格式:FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]
示例:FROM ubuntu:latest
- RUN
在当前镜像层之上创建一个新的镜像层,在新层中执行命令。
格式:RUN <command>
或RUN ["executable", "param1", "param2"]
示例:RUN apt-get update && install -y git
- CMD
在运行容器时提供默认参数。
格式:CMD ["param1","param2"]
或CMD command param1 param2
示例:CMD ["/usr/bin/wc","--help"]
- LABEL
用来定义键值对,这些键值对被记录为镜像或者容器的元数据。这和启动容器时的 –label 选项在功能上一致。
格式LABEL <key>=<value> <key>=<value> <key>=<value> ...
示例:LABEL multi.label1="value1" multi.label2="value2" other="value3"
- MAINTAINER(不推荐使用)
维护者信息。
格式:MAINTAINER <name>
示例:LABEL maintainer="SvenDowideit@home.org.au"
- EXPOSE
通知 Docker 容器启动时在指定的网络端口上进行监听。启动容器时可以使用 -p
选项来覆盖该设置。
格式:EXPOSE <port> [<port>/<protocol>...]
示例:EXPOSE 80/udp
- ENV
设置镜像的环境变量,类似启动容器时的 -e(–env)选项。
格式:ENV <key> <value>
或ENV <key>=<value> ...
示例:ENV myName="John Doe" myDog=Rex\ The\ Dog myCat=fluffy
- ADD
复制文件到镜像中。
格式:ADD [--chown=<user>:<group>] <src>... <dest>
或ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
示例:ADD test.txt relativeDir/
- COPY
复制文件到镜像中。
格式:COPY [--chown=<user>:<group>] <src>... <dest>
或COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
示例:COPY test.txt relativeDir/
- ENTRYPOINT
设置入口点,指定容器启动时需要被运行的可执行程序。
格式:ENTRYPOINT ["executable", "param1", "param2"]
示例:ENTRYPOINT ["top", "-b"]
- VOLUME
创建 Docker 管理卷。
格式:VOLUME ["/data"]
示例:VOLUME ["/data"]
- USER
启动容器时指定用户。
格式:USER <user>[:<group>]
示例:USER patrick
- WORKDIR
指定默认的工作目录,若指定的目录不存在则创建。
格式:WORKDIR /app
示例:WORKDIR /app
6.2.3 .dockerignore 介绍
在 Dockerk 客户端将上下文发送到 Docker 守护进程之前,它将在上下文的根目录中查找名为.dockerignore 的文件。如果此文件存在,则客户端会修改上下文以排除与其中的模式匹配的文件和目录。这有助于避免将不必要的大型文件或敏感文件和目录发送到守护进程,并避免使用 ADD 或 COPY 将它们添加到映像中。.dockerignore 的模式匹配行为如下:
规则 | 行为 |
---|---|
#comment | 注释,被忽略 |
* | 匹配任意数量的非分隔符,如 /temp 表示排除一级子目录中以 temp 开头的目录或文件,//temp* 表示排除二级子目录中以 temp 开头的目录或文件 |
? | 匹配任意单个非分隔符 |
! | 以感叹号! 开头的行表示该行不受.dockerignore 模式匹配的控制,可以被 ADD . 加入到镜像 |
例如我们只把必要的文件加入镜像,下面的示例表示只会把 app(不管是文件还是目录)、app.py、requirements.txt 加入到镜像
# 先排除所有
*
# 再加入需要的文件或目录
!app
!app.py
!requirements.txt
7. Docker 其他
Docker 运行容器时可以限制容器所使用的 CPU、内存、设备的访问权,还可以运行特权容器(docker container run --privileged
)。使用 Docker 过程中,可以通过 docker help < 命令 >
或者 docker < 命令 > --help
来查看帮助
docker run --help
Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
Run a command in a new container
Options:
--add-host list Add a custom host-to-IP mapping
(host:ip)
-a, --attach list Attach to STDIN, STDOUT or STDERR
...
8. 参考文献
- 《深入浅出 Docker》
- 《Docker 实战》
- https://docs.docker.com/engin…