Docker有一个被称为 "卷 "的功能,它允许开发者在使用容器时持久保存数据。它们完全由Docker引擎管理,对终端用户来说是无缝的。Docker卷是一个非常重要和有用的概念,在本教程中,我们将学习所有关于Docker卷的知识,如何创建卷,如何列出卷,以及如何删除卷。我们还将看到如何通过启动几个容器在容器之间共享一个卷,这些容器都使用同一个卷来共享数据。
关于Docker Volumes
要开始使用Docker卷,我们可以简单地看一下docker卷管理命令下的可用命令。像Docker的其他东西一样,我们可以创建、检查、列出、修剪和删除卷。
docker-volumes> docker volume
Usage: docker volume COMMAND
Manage volumes
Commands:
create Create a volume
inspect Display detailed information on one or more volumes
ls List volumes
prune Remove all unused local volumes
rm Remove one or more volumes
创建一个Docker卷
学习Docker卷的最好方法是直接进入并创建一个卷。在这里,我们创建了一个名为my-volume的卷。
docker-volumes> docker volume create my-volume
my-volume
列出Docker卷
使用 **docker volume ls**显示我们的新卷。
docker-volumes> docker volume ls
DRIVER VOLUME NAME
local my-volume
检查一个Docker卷
docker-volumes> docker volume inspect my-volume
[ { "CreatedAt": "2020-10-20T17:35:06Z", "Driver": "local", "Labels": {}, "Mountpoint": "/var/lib/docker/volumes/my-volume/_data", "Name": "my-volume", "Options": {}, "Scope": "local" }]
真实世界的Docker卷实例
在这个测试中,我们将使用Jenkins,一个开源的自动化服务器,使软件工程师能够使用持续集成和持续交付可靠地构建、测试和部署他们的软件。Jenkins是一个基于服务器的系统,在Apache Tomcat等Servlet容器中运行。这里的目标是展示我们可以存储Jenkins服务器在一个容器中工作时产生的数据,然后利用一个卷来在随后运行Jenkins实例的容器中使用这些数据。
从Docker Hub拉出Jenkins镜像
docker-volumes> docker pull jenkins
Using default tag: latest
latest: Pulling from library/jenkins
55cbf04beb70: Pull complete
...
1322ea3e7bfd: Pull complete
Digest: sha256:eeb4850eb65f2d92500e421b430ed1ec58a7ac909e91f518926e02473904f668
Status: Downloaded newer image for jenkins:latest
docker.io/library/jenkins:latest
在一个容器中运行Jenkins
现在我们有了本地仓库中的镜像,我们可以轻松地在容器中启动一个Jenkins服务器实例。你至少要对运行容器有一点了解,因为这里使用的命令使用了几个选项,包括-name、-v、和-p。如果你需要复习,可以看看之前的一些Docker教程。在本教程中,需要注意的主要概念是,我们在启动时将my-volume的卷附加到这个容器上(使用-v my-volume:/var/jenkins_home)。
docker-volumes> docker container run --name my-jenkins -v my-volume:/var/jenkins_home -p 8080:8080 -p 50000:50000 jenkins
Running from: /usr/share/jenkins/jenkins.war
webroot: EnvVars.masterEnvVars.get("JENKINS_HOME")
...
...
INFO:
*************************************************************
*************************************************************
*************************************************************
Jenkins initial setup is required. An admin user has been created and a password generated.
Please use the following password to proceed to installation:
039b2e1d3609465ead266da3a660eaf1
This may also be found at: /var/jenkins_home/secrets/initialAdminPassword
*************************************************************
*************************************************************
*************************************************************
Oct 20, 2020 5:53:51 PM hudson.model.UpdateSite updateData
INFO: Obtained the latest update center data file for UpdateSource default
...
...
INFO: Jenkins is fully up and running
--> setting agent port for jnlp
--> setting agent port for jnlp... done
Jenkins服务器现在正在运行。[我们可以访问http]://localhost:8080,看看它的工作情况。此外,我们可以从上面的输出中复制并粘贴生成的密码,以便立即登录。

在初次安装Jenkins时,会涉及一些设置。我们选择 "选择要安装的插件"

然后在 "入门 "页面中,在左上方选择 "无",然后在右下方选择 "安装"。

在这个页面上以管理员身份继续。

当Jenkins设置完成后,点击 "开始使用Jenkins "按钮。

使用卷的Docker持久化数据
这里的目标是演示这个容器中的Jenkins实例的数据如何被未来运行Jenkins的容器使用,这些容器使用了我们创建的卷。在下面的几个步骤中,我们在Jenkins中设置了一个新的作业。

工作名称可以是你喜欢的任何东西,我们简单地选择ExampleJob,选择Freestyle项目,然后点击ok。

只是作为一个例子,我们可以通过添加一个shell命令来添加一个构建步骤。

现在我们在Jenkins服务器上有了一个完全可操作的实例作业。

创建一个新的Jenkins容器
在接下来的步骤中,我们将创建第二个容器,并再次在其中运行Jenkins的实例。由于我们已经有一个实例在运行,它使用了8080:8080和50000:50000端口,我们需要指定新的端口,这样它们就不会发生冲突。另外,注意这个容器有一个新的不同的名字my-OTHER-jenkins,但是,卷是一样的(-v my-volume:/var/jenkins_home)。
docker-volumes> docker container run --name my-OTHER-jenkins -v my-volume:/var/jenkins_home -p 9090:8080 -p 60000:50000 jenkins
Running from: /usr/share/jenkins/jenkins.war
webroot: EnvVars.masterEnvVars.get("JENKINS_HOME")
...
...
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@54b6b3cf: defining beans [filter,legacy]; root of factory hierarchy
Oct 20, 2020 6:18:31 PM hudson.WebAppMain$3 run
INFO: Jenkins is fully up and running
--> setting agent port for jnlp
--> setting agent port for jnlp... done

现在打开一个新的浏览器标签,[访问http://localhost:9090,瞧!我们看到了另一个Jenkins实例。我们看到了另一个Jenkins的实例。请注意,我们马上就会看到一个登录屏幕。不需要任何设置步骤。这表明我们在Docker中的卷正在正常工作。由于我们创建了名为my-volume的卷,我们为设置第一个Jenkins实例所做的所有工作都与这个新的Jenkins实例共享。

让我们确认一下,我们有两个Jenkins容器在运行,当然,它们就在那里。
docker volumes> docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ca63090c2877 jenkins "/bin/tini -- /usr/l…" 7 minutes ago Up 7 minutes 0.0.0.0:9090->8080/tcp, 0.0.0.0:60000->50000/tcp my-OTHER-jenkins
e87acec5997f jenkins "/bin/tini -- /usr/l…" 32 minutes ago Up 32 minutes 0.0.0.0:8080->8080/tcp, 0.0.0.0:50000->50000/tcp my-jenkins
让我们也列出我们拥有的卷。my-volume卷对我们来说工作得很完美。
docker volumes> docker volume ls
DRIVER VOLUME NAME
local my-volume
如果我们不早点创建这个卷,当我们启动第二个Jenkins实例时,它就会要求我们重新提供所有的设置信息。这个卷将在我们建立和拆除容器时继续工作。
让我们删除所有正在运行的容器。
docker volumes> docker rm $(docker ps -a -q)
Error response from daemon: You cannot remove a running container ca63090c2877cef6cdc6c5e2338471fc347f5f5be48766a997feee7cf6a2ec39. Stop the container before attempting removal or force remove
Error response from daemon: You cannot remove a running container e87acec5997fe4eeb73c4bf30ba23d416dd6d2723e97f7270f84d7afdffdd439. Stop the container before attempting removal or force remove
糟糕。我们需要在删除容器之前停止它们。
docker volumes> docker stop $(docker ps -a -q)
ca63090c2877
e87acec5997f
现在我们可以删除所有的容器,因为它们已经停止了。
docker volumes> docker rm $(docker ps -a -q)
ca63090c2877
e87acec5997f
列出容器显示它们都已经消失了。
docker volumes> docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
第三个容器
想象一下,我们刚刚结束了一天的工作,现在我们明天回来,想再一次运行Jenkins。我们可以启动另一个容器,它应该可以继续访问最初生成并存储在my-volume中的数据。
docker volumes> docker container run --name jenkins-THE-THIRD -v my-volume:/var/jenkins_home -p 8080:8080 -p 50000:50000 jenkins
Running from: /usr/share/jenkins/jenkins.war
webroot: EnvVars.masterEnvVars.get("JENKINS_HOME")
Oct 20, 2020 6:29:44 PM Main deleteWinstoneTempContents
...
...
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@5517b224: defining beans [filter,legacy]; root of factory hierarchy
Oct 20, 2020 6:29:48 PM hudson.WebAppMain$3 run
INFO: Jenkins is fully up and running
--> setting agent port for jnlp
--> setting agent port for jnlp... done
我们可以打开Jenkins服务器,再一次回到我们原来的工作流程中,同样的ExampleJob仍然完整。

确认这其实是一个全新的容器。

所有这些都是由我们卑微的卷,my-volume实现的。
docker volumes> docker volume ls
DRIVER VOLUME NAME
local my-volume
在Docker文件中指定VOLUME
在上面的例子中,我们完全从命令行中创建并指定了要使用的卷。你也可以在[Docker文件]中使用VOLUME命令指定卷。例如,在官方的MySql Docker文件中,有以下一行。
VOLUME /var/lib/mysql
这是MySql数据库的默认位置,Docker文件中的这条命令告诉Docker,当一个新的容器被启动时,它将创建一个新的卷的位置,并将其分配给容器中的这个目录。这意味着容器中的任何文件都会超过容器的寿命,直到卷本身被手动删除。如果你不想再使用卷,就需要手动删除它们。毕竟,这就是它们的目的,存储数据供将来使用。因此,需要手动删除卷是一个很好的保险政策。让我们看看这个动作。
目前我们的Docker环境已经清理完毕,我们可以列出卷,看到没有卷。
docker-volumes> docker volume ls
DRIVER VOLUME NAME
现在让我们运行一个docker容器,像这样。
docker-volumes> docker container run -d --name my-database -e MYSQL_ALLOW_EMPTY_PASSWORD=True mysql
9308a2d6d73195a517f039107b13d858334e9304a5a169d8b08abd8bdb99dbb1
当然,我们有一个正在运行的MySql容器。

现在让我们注意一些有趣的事情。如果我们再次列出卷,有一个卷存在。这是由于MySql的Docker文件中的VOLUME命令造成的。
docker-volumes> docker volume ls
DRIVER VOLUME NAME
local 4ee2bfdd76937afa4e431a1a931cb30e8aaff292eb5f5fda8560b915f4846cd6
如果你使用docker image inspect mysql进行检查,你也可以在图像中看到卷。在你看到的大量输出中,包含了以下显示卷的位置的片段。
"Volumes": {
"/var/lib/mysql": {}
},
所以我们可以知道,在构建镜像时,来自Dockerfile的配置为该路径分配了一个卷。
仔细看看这个,我们也可以用docker container inspect my-database来检查运行中的容器。输出中再次包含了一个显示挂载的部分。
"Mounts": [
{
"Type": "volume",
"Name": "4ee2bfdd76937afa4e431a1a931cb30e8aaff292eb5f5fda8560b915f4846cd6",
"Source": "/var/lib/docker/volumes/4ee2bfdd76937afa4e431a1a931cb30e8aaff292eb5f5fda8560b915f4846cd6/_data",
"Destination": "/var/lib/mysql",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
],
上面的内容代表了正在运行的容器在主机上获得了自己独特的位置来存储源数据。在后台,这个位置被映射或挂载到/var/lib/mysql容器中的目标位置。所以MySql引擎只是认为它在向/var/lib/mysql写数据,然而,数据实际上是被存储在主机上Source的那个长路径中。因此,如果你用大量的数据填充该数据库,容器的大小就会保持不变。
你也可以用docker volume inspect检查卷本身**,**它将显示主机上相同的Mountpoint。
docker-volumes> docker volume inspect 4ee2bfdd76937afa4e431a1a931cb30e8aaff292eb5f5fda8560b915f4846cd6
[
{
"CreatedAt": "2020-10-21T22:19:22Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/4ee2bfdd76937afa4e431a1a931cb30e8aaff292eb5f5fda8560b915f4846cd6/_data",
"Name": "4ee2bfdd76937afa4e431a1a931cb30e8aaff292eb5f5fda8560b915f4846cd6",
"Options": null,
"Scope": "local"
}
]
命名的卷是更好的
上面的部分展示了MySql容器如何使用卷来存储数据。你可能已经注意到了,那些超长的挂载点ID对用户来说不是那么友好。一个更友好的方法是,当你使用语法**-v volume-name:/path/in/container**运行一个容器时,使用命名的卷。让我们用这种方法重做上面的内容。
docker-volumes> docker container run -d --name my-database -e MYSQL_ALLOW_EMPTY_PASSWORD=True -v my-volume:/var/lib/mysql mysql
44f34578e6e8eb359725320ded595171e299509b3c6c1184e1e0f4c7ea615c52
docker-volumes> docker volume ls
DRIVER VOLUME NAME
local my-volume
注意这个卷的名字更方便用户使用。在检查容器和卷本身时,我们可以看到同样的情况。
docker-volumes> docker container inspect my-database
"Mounts": [
{
"Type": "volume",
"Name": "my-volume",
"Source": "/var/lib/docker/volumes/my-volume/_data",
"Destination": "/var/lib/mysql",
"Driver": "local",
"Mode": "z",
"RW": true,
"Propagation": ""
}
],
docker-volumes> docker volume inspect my-volume
[
{
"CreatedAt": "2020-10-21T22:57:25Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/my-volume/_data",
"Name": "my-volume",
"Options": null,
"Scope": "local"
}
]
现在,这里还有一些事情需要考虑。在第一种方法中,只要你启动一个新的容器,就会用一个随机的ID创建一个新的卷。所以,如果你在几天内启动了10个容器,你也会有10个不同的卷,而且是随机的ID,你将不知道什么会在哪里。使用命名卷的方法,你可以随意启动多个容器,并使用同一个卷,保持你的数据一致,并使跟踪你的卷更加容易。
让我们在第一个MySql容器中添加一个新的表。
docker-volumes> docker exec -it my-database bash
root@44f34578e6e8:/# mysql
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.22 MySQL Community Server - GPL
Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> create database NEWDATABASE;
Query OK, 1 row affected (0.02 sec)
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| NEWDATABASE |
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.01 sec)
现在我们可以创建一个新的MySql容器,并使用同一个卷。
docker-volumes> docker container run -d --name my-OTHER-database -e MYSQL_ALLOW_EMPTY_PASSWORD=True -v my-volume:/var/lib/mysql mysql
ad1094d0d7b696059f652b1d6d949c5f83b1d554d43dd38f14750d67f6cfc5fa
docker-volumes> docker volume ls
DRIVER VOLUME NAME
local my-volume
仍然只有上面列出的一个卷,并有一个用户友好的名字。
让我们看看新的容器是否有相同的数据库。
docker-volumes> docker exec -it my-OTHER-database bash
root@ad1094d0d7b6:/# mysql
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.22 MySQL Community Server - GPL
Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| NEWDATABASE |
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.00 sec)
果然,那个NEWDATABASE还在那里!如果我们没有指定一个命名的卷,就不会出现这种情况。
Docker卷总结
容器在设计上是不可改变的和临时的。我们把它们旋转起来,使用它们,然后再把它们拆掉。这就是所谓的不可改变的基础设施,意味着容器可以被重新部署,但永远不会改变。这很好,但我们需要一种方法来处理状态、独特的数据和数据库。Docker提供了两种处理持久化数据的方法,其中卷轴是我们在本教程中涉及的主题。卷轴在容器的UFS之外做了一个特殊的位置来处理这个问题。