本文发自 http://www.binss.me/blog/learn-docker-with-me-about-volume/,转载请注明出处。

在使用Docker的过程中,常常需要通过Volume来挂载目录。可以通过以下两种方式创建Volume:

  1. 在Dockerfile中指定VOLUME /some/dir

  2. 生成容器时执行docker run -v /some/dir命令来指定

有个问题我纠结了很久:Dockerfile中使用VOLUME指令挂载目录和docker run时通过-v参数指定挂载目录有什么区别?

区别

根据官方文档,Dockerfile生成目标镜像的过程就是不断docker run + docker commit的过程,当Dockerfile执行到VOLUME /some/dir(这里为/var/lib/mysql)这一行时,输出:

Step 6 : VOLUME /var/lib/mysql
 ---> Running in 0c842ec90849
 ---> 214e3dccd0f2

在这一步,docker生成了临时容器0c842ec90849,然后commit容器得到镜像214e3dccd0f2。因此VOLUME /var/lib/mysql是通过docker run -v /var/lib/mysql,即第二种方式来实现的,随后由于容器的提交,该配置被保存到了镜像214e3dccd0f2中,通过inspcet可以查看到:

"Volumes": {
    "/var/lib/mysql": {},
}

通过inspect该镜像生成的容器可以发现挂载配置:

"Mounts": [
    {
        "Name": "8827c361d103c1272907da0b82268310415f8b075b67854f27dbca0b59a31a1a",
        "Source": "/mnt/sda1/var/lib/docker/volumes/8827c361d103c1272907da0b82268310415f8b075b67854f27dbca0b59a31a1a/_data",
        "Destination": "/var/lib/mysql",
        "Driver": "local",
        "Mode": "",
        "RW": true,
        "Propagation": ""
    }
]

由于没有指定挂载到的宿主机目录,因此会默认挂载到宿主机的/var/lib/docker/volumes下的一个随机名称的目录下,在这里为/mnt/sda1/var/lib/docker/volumes/8827c361d103c1272907da0b82268310415f8b075b67854f27dbca0b59a31a1a/_data。因此Dockerfile中使用VOLUME指令挂载目录和docker run时通过-v参数指定挂载目录的区别在于,run的-v可以指定挂载到宿主机的哪个目录,而Dockerfile的VOLUME不能,其挂载目录由docker随机生成。

若指定了宿主机目录,比如:

docker run --name mysql -v ~/volume/mysql/data:/var/lib/mysql -d mysql:5.7

那么inspect如下:

"Mounts": [
    {
        "Source": "/Users/binss/volume/mysql/data",
        "Destination": "/var/lib/mysql",
        "Mode": "",
        "RW": true,
        "Propagation": "rprivate"
    }
]

这里将/var/lib/mysql挂载到宿主机的/Users/binss/volume/mysql/data目录下,而不再是默认的/var/lib/docker/volumes目录。这样做有什么好处呢?我们知道,/var/lib/mysql是mysql在linux下默认的数据存储目录。将该目录挂载到宿主机,可以使数据在容器被移除时得以保留,而不会随着容器go die。下次新建mysql容器,只需同样挂载到/Users/binss/volume/mysql/data,即可复用原有数据。

有童鞋可能会问,那VOLUME /some/dir和不指定宿主机目录的docker run -v /some/dir有什么用?我们不可能为了复用该目录的数据,先inspect找到目录在哪,然后将新容器挂载上去,多麻烦呀。

查阅官方文档,发现还有另外一种挂载方式:

docker run --name mysql2 --volumes-from mysql -d mysql:5.7

docker inspect mysql2发现,mysql2的/var/lib/mysql和mysql的/var/lib/mysql都挂载到了相同的目录下,因此在mysql2中可以复用mysql的数据:

"Mounts": [
    {
        "Name": "8827c361d103c1272907da0b82268310415f8b075b67854f27dbca0b59a31a1a",
        "Source": "/mnt/sda1/var/lib/docker/volumes/8827c361d103c1272907da0b82268310415f8b075b67854f27dbca0b59a31a1a/_data",
        "Destination": "/var/lib/mysql",
        "Driver": "local",
        "Mode": "",
        "RW": true,
        "Propagation": ""
    }
]

值得注意的是,虽然/var/lib/mysql没有在run的指令中出现,但其出现在了生成mysql:5.7镜像的Dockerfile中,所以即使在run时忘记使用volume,该目录依然能够从宿主机直接访问,官方真是用心良苦啊。另外,由于复用了mysql的所有数据,因此连接数据库用户名密码也和mysql一样,而不会是run时传入的参数,一切都和原来mysql中的一样。

备份与恢复

既然有了--volumes-from,那么备份也不成问题。

docker run --rm --volumes-from mysql -v $(pwd):/backup mysql:5.7 tar cvf /backup/backup.tar /var/lib/mysql

该指令将mysql容器的/var/lib/mysql目录打包成backup.tar,存放在宿主机的当前目录下。实现方法很简单,创建一个新容器,通过--volumes-from将新容器的/var/lib/mysql挂载到mysql的相同目录下,同时又将宿主机的当前目录挂载到容器的/backup目录下,然后通过tar将/var/lib/mysql打包存放到容器的/backup目录下,这样宿主机的当前目录就得到了打包文件backup.tar。以上流程完成后,容器由于设置了--rm而被删除,只留下backup.tar,毫无痕迹地备份了容器mysql的数据文件。

恢复时,只需执行以下命令:

docker run --rm --volumes-from mysql -v $(pwd):/backup mysql:5.7 bash -c "cd /var/lib/mysql && tar xvf /backup.tar --strip 1"

同样是创建了一个临时容器,将刚才打包backup.tar解压到mysql容器的/var/lib/mysql中。因为backup.tar解压出来是backup文件夹,所以通过--strip 1跳过这一级目录。

移除无用的挂载目录

由于poll下来的镜像常常都设置了VOLUME指令,所以如果我们创建容器时没有为其指定宿主机挂载目录,如前文所述,会在/var/lib/docker/volumes目录下生成挂载目录。而在删除容器时,如果忘记使用-v,则该挂载目录会残留在宿主机中,占用硬盘空间。例如/var/lib/mysql文件夹,一个就好几百M。

通过以下指令可以移除这些残余的无用挂载目录:

docker volume rm $(docker volume ls -qf dangling=true)

总结

通过VOLUME,我们得以绕过docker的Union File System,从而直接对宿主机的目录进行直接读写,实现了容器内数据的持久化和共享化。

2016.04.02更新

由于Mac是在VirtualBox里跑docker,因此在挂载一些容器目录时会出现各种问题,比如mongo:

WARNING (Windows & OS X): The default Docker setup on Windows and OS X uses a VirtualBox VM to host the Docker daemon. Unfortunately, the mechanism VirtualBox uses to share folders between the host system and the Docker container is not compatible with the memory mapped files used by MongoDB (see vbox bug, docs.mongodb.org and related jira.mongodb.org bug). This means that it is not possible to run a MongoDB container with the data directory mapped to the host.

因此只能使用Dockerfile给出的默认方式——让docker在/var/lib/docker/volumes下随机生成一个目录然后挂载到上面去。使用volumes-from,备份和恢复都没有问题,就是下次创建新容器时要挂载到该目录还要用inspect看下目录叫什么名字。这点让我们很不爽,有没有解决方案?

Docker 1.9新增了docker volume命令,可以创建命名的挂载目录:

docker volume create --name mongo_db
docker volume create --name mongo_configdb
docker run --name mongo -p 27017:27017 -v mongo_db:/data/db -v mongo_configdb:/data/configdb -d mongo

这样我们就创建了命名挂载:mongo_db和mongo_configdb。然后创建容器,将mongo的/data/db和/data/configdb分别挂载到上面去。inspect如下:

"Mounts": [
    {
        "Name": "mongo_db",
        "Source": "/mnt/sda1/var/lib/docker/volumes/mongo_db/_data",
        "Destination": "/data/db",
        "Driver": "local",
        "Mode": "z",
        "RW": true,
        "Propagation": "rprivate"
    },
    {
        "Name": "mongo_configdb",
        "Source": "/mnt/sda1/var/lib/docker/volumes/mongo_configdb/_data",
        "Destination": "/data/configdb",
        "Driver": "local",
        "Mode": "z",
        "RW": true,
        "Propagation": "rprivate"
    }
]

下次要复用数据时,指定相同的命名挂载即可:

docker run --name new_mongo -p 27017:27017 -v mongo_db:/data/db -v mongo_configdb:/data/configdb -d mongo