Docker
摘自:https://yeasy.gitbook.io/docker_practice/
什么是 Docker
摘自:https://zhuanlan.zhihu.com/p/187505981
容器技术 vs 虚拟机
从空间和时间两个维度考虑
和一个单纯的应用程序相比,操作系统是一个很重而且很笨的程序
操作系统运行起来是需要占用很多资源的,刚装好的系统还什么都没有部署,单纯的操作系统其磁盘占用至少几十 G 起步,内存要几个 G 起步。
假设我有一台机器,16G 内存,需要部署三个应用,那么使用虚拟机技术可以这样划分:

在这台机器上开启三个虚拟机,每个虚拟机上部署一个应用,其中 VM1 占用 2G 内存,VM2 占用 1G 内存,VM3 占用了 4G 内存。
我们可以看到虚拟本身就占据了总共 7G 内存,因此我们没有办法划分出更多虚拟机从而部署更多的应用程序,可是我们部署的是应用程序,要用的也是应用程序而不是操作系统。
如果有一种技术可以让我们避免把内存浪费在 “无用” 的操作系统上岂不是太香?这是问题一,主要原因在于操作系统太重了。
还有另一个问题,那就是启动时间问题,我们知道操作系统重启是非常慢的,因为操作系统要从头到尾把该检测的都检测了该加载的都加载上,这个过程非常缓慢,动辄数分钟,因此操作系统还是太笨了。
那么有没有一种技术可以让我们获得虚拟机的好处又能克服这些缺点从而一举实现鱼和熊掌的兼得呢?
答案是肯定的,这就是容器技术。
什么是容器
容器一词的英文是 container,其实 container 还有集装箱的意思,而容器和集装箱在概念上是很相似的。
现代软件开发的一大目的就是隔离,应用程序在运行时相互独立互不干扰,这种隔离实现起来是很不容易的,其中一种解决方案就是上面提到的虚拟机技术,通过将应用程序部署在不同的虚拟机中从而实现隔离:
但是虚拟机技术有上述提到的各种缺点,那么容器技术又怎么样呢?
与虚拟机通过操作系统实现隔离不同,容器技术
- 只隔离应用程序的运行时环境
- 容器之间可以共享同一个操作系统
这里的运行时环境指的是程序运行依赖的各种库以及配置。
从图中我们可以看到容器更加的轻量级且占用的资源更少,与操作系统动辄几 G 的内存占用相比,容器技术只需数 M 空间,因此我们可以在同样规格的硬件上大量部署容器,这是虚拟机所不能比拟的,而且不同于操作系统数分钟的启动时间容器几乎瞬时启动,容器技术为打包服务栈提供了一种更加高效的方式,So cool。
那么我们该怎么使用容器呢?这就要讲到 docker 了。
注意,容器是一种通用技术,docker 只是其中的一种实现。
什么是 docker
docker 是一个用 Go 语言实现的开源项目,可以让我们方便的创建和使用容器,docker 将程序以及程序所有的依赖都打包到 docker container,这样你的程序可以在任何环境都会有一致的表现,这里程序运行的依赖也就是容器。
就好比集装箱,容器所处的操作系统环境就好比货船或港口,程序的表现只和集装箱有关系(容器),和集装箱放在哪个货船或者哪个港口 (操作系统) 没有关系。
因此我们可以看到 docker 可以屏蔽环境差异,也就是说,只要你的程序打包到了 docker 中,那么无论运行在什么环境下程序的行为都是一致的,程序员再也无法施展表演才华了,不会再有 “在我的环境上可以运行”,真正实现 “build once, run everywhere”。
此外 docker 的另一个好处就是快速部署,这是当前互联网公司最常见的一个应用场景,一个原因在于容器启动速度非常快,另一个原因在于只要确保一个容器中的程序正确运行,那么你就能确信无论在生产环境部署多少都能正确运行。
如何使用 docker
docker 中有这样几个概念:
- dockerfile
- image
- container
实际上你可以简单的把
- dockerfile 理解为源代码
- image 理解为可执行程序
- container 就是运行起来的进程
- docker 就是 " 编译器 "。
官方文档的术语解释:
A Dockerfile is simply a text-based script of instructions that is used to create a container image.
A container is a sandboxed process on your machine that is isolated from all other processes on the host machine.
因此我们只需要在 dockerfile 中指定需要哪些程序、依赖什么样的配置,之后把 dockerfile 交给“编译器” docker 进行“编译”,也就是 docker build 命令,生成的可执行程序就是 image,之后就可以运行这个 image 了,这就是 docker run 命令,image 运 行起来后就是 docker container。
docker 是如何工作的
实际上 docker 使用了常见的 CS 架构,也就是 client-server 模式,docker client 负责处理用户输入的各种命令,比如 docker build、docker run,真正工作的其实是 server,也就是 docker daemon
值得注意的是,docker client 和 docker demon 可以运行在同一台机器上。
接下来我们用几个命令来讲解一下 docker 的工作流程:
docker build
当我们写完 dockerfile 交给 docker “编译” 时使用这个命令,那么 client 在接收到请求后转发给 docker daemon,接着 docker daemon 根据 dockerfile 创建出 “可执行程序” image。

docker run
有了 “可执行程序” image 后就可以运行程序了,接下来使用命令 docker run,docker daemon 接收到该命令后找到具体的 image,然后加载到内存开始执行,image 执行起来就是所谓的 container。

docker pull
其实 docker build 和 docker run 是两个最核心的命令,会用这两个命令基本上 docker 就可以用起来了,剩下的就是一些补充。
那么 docker pull 是什么意思呢?
我们之前说过,docker 中 image 的概念就类似于“可执行程序”,我们可以从哪里下载到别人写好的应用程序呢?很简单,就是 Docker Hub,docker 官方的“应用商店”,你可以在这里下载到别人编写好的 image,这样你就不用自己编写 dockerfile 了。
docker registry 可以用来存放各种 image,公共的可以供任何人下载 image 的仓库就是 docker Hub。那么该怎么从 Docker Hub 中下载 image 呢,就是这里的 docker pull 命令了。
因此,这个命令的实现也很简单,那就是用户通过 docker client 发送命令,docker daemon 接收到命令后向 docker registry 发送 image 下载请求,下载后存放在本地,这样我们就可以使用 image 了。

最后,让我们来看一下 docker 的底层实现。
docker 的底层实现
docker 基于 Linux 内核提供这样几项功能实现的:
- NameSpace 我们知道 Linux 中的 PID、IPC、网络等资源是全局的,而 NameSpace 机制是一种资源隔离方案,在该机制下这些资源就不再是全局的了,而是属于某个特定的 NameSpace,各个 NameSpace 下的资源互不干扰,这就使得每个 NameSpace 看上去就像一个独立的操作系统一样,但是只有 NameSpace 是不够。
- Control groups 虽然有了 NameSpace 技术可以实现资源隔离,但进程还是可以不受控的访问系统资源,比如 CPU、内存、磁盘、网络等,为了控制容器中进程对资源的访问,Docker 采用 control groups 技术(也就是 cgroup),有了 cgroup 就可以控制容器中进程对系统资源的消耗了,比如你可以限制某个容器使用内存的上限、可以在哪些 CPU 上运行等等。
有了这两项技术,容器看起来就真的像是独立的操作系统了。
安装
Ubuntu
使用 APT 安装
由于 apt 源使用 HTTPS 以确保软件下载过程中不被篡改。因此,我们首先需要添加使用 HTTPS 传输的软件包以及 CA 证书。
$ sudo apt-get update
$ sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
gnupg \
lsb-release
为了确认所下载软件包的合法性,需要添加软件源的 GPG 密钥。
$ curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# 官方源
# $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
然后,我们需要向 sources.list 中添加 Docker 软件源
$ echo \
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://mirrors.aliyun.com/docker-ce/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 官方源
# $ echo \
# "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
# $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
以上命令会添加稳定版本的 Docker APT 镜像源,如果需要测试版本的 Docker 请将 stable 改为 test。
更新 apt 软件包缓存,并安装 docker-ce:
$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io
启动 Docker
$ sudo systemctl enable docker
$ sudo systemctl start docker
测试
$ docker run --rm hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:308866a43596e83578c7dfa15e27a73011bdd402185a84c5cd7f32a88b501a24
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/
若能正常输出以上信息,则说明安装成功。
建立 docker 用户组
默认情况下,docker 命令会使用 Unix socket 与 Docker 引擎通讯。而只有 root 用户和 docker 组的用户才可以访问 Docker 引擎的 Unix socket。出于安全考虑,一般 Linux 系统上不会直接使用 root 用户。因此,更好地做法是将需要使用 docker 的用 户加入 docker 用户组。
建立 docker 组:
$ sudo groupadd docker
将当前用户加入 docker 组:
$ sudo usermod -aG docker $USER
使用镜像
Docker 镜像 是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像 不包含 任何动态数据,其内容在构建之后也不会被改变。
获取镜像 docker pull
从 Docker 镜像仓库获取镜像的命令是 docker pull。其命令格式为:
$ docker pull [选项] 仓库名[:标签]
# 更具体地
$ docker pull [选项] [Docker Registry 地址[:端口号]]仓库名[:标签]
具体的选项可以通过 docker pull --help 命令看到,这里我们说一下镜像名称的格式。
- Docker 镜像仓库地址:地址的格式一般是
<域名/IP>[:端口号]。- 默认地址是 Docker Hub (
docker.io)
- 默认地址是 Docker Hub (
- 仓库名:两段式名称,即
<用户名>/<软件名>。- 对于 Docker Hub,如果不给出用户名,则默认为
library,也就是官方镜像。
- 对于 Docker Hub,如果不给出用户名,则默认为
例
$ docker pull ubuntu:18.04
# 更具体地
$ docker pull docker.io/library/ubuntu:18.04
上面的命令中没有给出 Docker 镜像仓库地址,因此将会从 Docker Hub (docker.io)获取镜像。镜像名称是 ubuntu:18.04,因此将会获取官方镜像 library/ubuntu 仓库中标签为 18.04 的镜像。
下载过程:
18.04: Pulling from library/ubuntu
92dc2a97ff99: Pull complete
be13a9d27eb8: Pull complete
c8299583700a: Pull complete
Digest: sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26
Status: Downloaded newer image for ubuntu:18.04
docker.io/library/ubuntu:18.04
从下载过程中可以看到我们之前提及的分层存储的概念,镜像是由多层存储所构成。下载也是一层层的去下载,并非单一文件。
下载过程中给出了每一层的 ID 的前 12 位。并且下载结束后,给出该镜像完整的 sha256 的摘要,以确保下载一致性。
试运行
以上面的 ubuntu:18.04 为例,如果我们打算启动里面的 bash 并且进行交互式操作的话,可以执行下面的命令。
$ docker run -it --rm ubuntu:18.04 bash
-it:这是两个参数-i:交互式操作-t终端。我们这里打算进入bash执行一些命令并查看返回结果,因此我们需要交互式终端。
--rm:这个参数是说容器退出后随之将其删除。- 默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动
docker rm。 - 我们这里只是随便执行个命令,看看结果,不需要排障和保留结果,因此使用
--rm可以避免浪费空间。
- 默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动
ubuntu:18.04:这是指用ubuntu:18.04镜像为基础来启动容器。bash:放在镜像名后的是 命令,这里我们希望有个交互式 Shell,因此用的是bash。
列出镜像 docker image ls
要想列出已经下载下来的镜像,可以使用 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 329ed837d508 3 days ago 63.3MB
ubuntu bionic 329ed837d508 3 days ago 63.3MB
列表包含了 仓库名、标签、镜像 ID、创建时间 以及 所占用的空间。
镜像 ID 则 是镜像的唯一标识,一个镜像可以对应多个 标签。因此,在上面的例子中,我们可以看到 ubuntu:18.04 和 ubuntu:bionic 拥有相同的 ID,因为它们对应的是同一个镜像。
镜像体积
如果仔细观察,会注意到,这里标识的所占用空间和在 Docker Hub 上看到的镜像大小不同。比如,ubuntu:18.04 镜像大小,在这里是 63.3MB,但是在 Docker Hub 显示的却是 25.47 MB。
这是因为 Docker Hub 中显示的体积是压缩后的体积。在镜像下载和上传过程中镜像是保持着压缩状态的,因此 Docker Hub 所显示的大小是网络传输中更关心的流量大小。
而 docker image ls 显示的是镜像下载到本地后,展开的大小,准确说,是展开后的各层所占空间的总和,因为镜像到本地后,查看空间的时候,更关心的是本地磁盘空间占用的大小。
另外一个需要注意的问题是,docker image ls 列表中的镜像体积总和并非是所有镜像实际硬盘消耗。由于 Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于 Docker 使用 Union FS,相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。
你可以通过 docker system df 命令 来便捷的查看镜像、容器、数据卷所占用的空间。
$ 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 329ed837d508 3 days ago 63.3MB
ubuntu bionic 329ed837d508 3 days ago 63.3MB
列出特定的某个镜像,也就是说指定仓库名和标签
$ docker image ls ubuntu:18.04
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 18.04 329ed837d508 3 days ago 63.3MB
除此以外,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 rm
如果要删除本地的镜像,可以使用 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
我们也可以用 镜像名,也就是 <仓库名>:<标签>,来删除镜像。
$ docker image rm centos
当然,更精确的是使用
镜像摘要删除镜像。
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)