Docker Volume 是個讓 Docker Container 保存與共用資料的機制。原本 Docker Container 的資料只會存在該 container 內,並不會與外部或是其他 container 共享,如果在 container 運作時新增了一些資料,而在 container 移除時又沒有將該 container commit 成 image,則當 container 被刪除時,資料就會遺失。可以參考官方文件 Understand images, containers, and storage drivers 了解 image 與 container 的運作方式。
要避免資料遺失,就可以使用 Docker Volume。你可以建立一個 volume,將它指定到 container 內的某個目錄,這樣該目錄的資料就可以永續保存著,不會因為 container 移除而被移除。你也可以為 volume 命名,這樣其他 container 就能透過這個名稱來使用相同的 volume,另外 volume 也可以與本機的目錄上連結,這樣你就可以讓你本機上的目錄和 container 上的目錄共享資料。
建立 volume
你可以使用許多方式建立 volume,比如說在建立 container 時使用 -v flag 一併建立:
docker run -d -v /var/lib/mysql mysql:5.7.17
你也可以在使用 -v 時一併指定 volume 名稱,讓資料保存下來,當下次要用別的 container 時,指定相同 volume 名稱就能使用相同資料:
docker run -d -v db_data:/var/lib/mysql mysql:5.7.17
或是先使用 docker volume 指令建立好 volume,在 run 時指定你建立的 volume 名稱即可使用:
docker volume create db_data2
docker run -d -v db_data2:/var/lib/mysql mysql:5.7.17
你也可以使用 volume 來實現本機目錄與 container 目錄資料共享:
docker run -d -v /Users/mayer/Documents/dockerspace/mysqldata:/var/lib/mysql mysql:5.7.17
volume 掛載測試
官方有說明,如果你是建立一個 volume 然後掛載到 container 的目錄,則原本 container 的目錄內所有的資料會複製到 volume 內。但是看文件沒有說明如果掛載的目錄有 a 檔案,而 volume 內也一樣有 a 檔案,會有什麼狀況發生,因此就自己想了個例子測試。
首先建立一個全新的 tomcat container,命名為 foo1,然後使用 -v 指令掛載一個名為 foovolume 的 VOLUME 到 tomcat container 的 /usr/local/tomcat 目錄,接著進入到該 tomcat 的 /bin/bash:
$ docker run -d --name foo1 -v foovolume:/usr/local/tomcat/conf tomcat:8.5.11-jre8
$ docker exec -it foo1 /bin/bash
使用 ls 觀察 server.xml 檔案修改時間,接著在使用 echo 指令在 server.xml 底下加上一些註解,用 tail 指令觀察該檔案是否已經加入資料。然後退出 bash 並停止 foo1 container,讓他不影響接下來的測試。
root@a6e3c1a143f5:/usr/local/tomcat# ls -al conf/server.xml
-rw------- 1 root root 7511 Jan 10 21:05 server.xml
root@a6e3c1a143f5:/usr/local/tomcat# echo '<!-- foobar -->' >> conf/server.xml
root@a6e3c1a143f5:/usr/local/tomcat# tail -n 3 conf/server.xml
</Service>
</Server>
<!-- foobar -->
root@a6e3c1a143f5:/usr/local/tomcat# ls -al conf/server.xml
-rw------- 1 root root 7527 Mar 16 09:10 conf/server.xml
root@a6e3c1a143f5:/usr/local/tomcat# exit
$ docker stop foo1
接著建立另一個全新的 tomcat container,命名為 foo2,一樣使用 -v 指令掛載剛剛建立好的 foovolume VOLUME 到 /usr/local/tomcat 目錄,接著進入到該 tomcat 的 /bin/bash:
$ docker run -d --name foo2 -v foovolume:/usr/local/tomcat/conf tomcat:8.5.11-jre8
$ docker exec -it foo2 /bin/bash
然後使用 ls 與 tail 指令觀察 server.xml 檔案修改日期與檔案的內容,可以發現到與剛剛 foo1 修改後的檔案日期與內容是一致的。
root@a564777a0059:/usr/local/tomcat# ls -al conf/server.xml
-rw------- 1 root root 7527 Mar 16 09:10 conf/server.xml
root@a564777a0059:/usr/local/tomcat# tail -n 3 conf/server.xml
</Service>
</Server>
<!-- foobar -->
root@a564777a0059:/usr/local/tomcat# exit
$ docker stop foo2
從這個測試可以觀察出,如果你將一個剛建立的 volume 掛載到某個 container 的目錄,若該目錄裡面已經有資料了,則 Docker 會將資料複製到這個剛建立的 volume,然後把這個 volume 覆蓋在你指定的目錄上,原本目錄的東西將會變成不可視。若你掛載的是一個經過初始化的 volume,則他會直接把 volume 內的資料覆蓋在你指定的目錄。
可以用另一個簡單的測試來驗證上面的說法是否正確。新建立一個 volume,掛載到 tomcat 的 /usr/local/tomcat/conf,然後在將這個 volume 掛載到另一個 tomcat 的 /usr/local/tomcat/webapps,就可以看到,原本 webapps 底下的 ROOT 等等的 Web App 都變成了設定檔了,也就是說,初次建立 volume 時,Docker 把 設定檔複製到 volume 上,volume 再次被掛載時,Docker 把 volume 蓋到 webapps 目錄之上。
$ docker run -d --name foo3 -v foovolume2:/usr/local/tomcat/conf tomcat:8.5.11-jre8
$ docker stop foo3
$ docker run -d --name foo4 -v foovolume2:/usr/local/tomcat/webapps tomcat:8.5.11-jre8
$ docker exec -it foo4 /bin/bash
root@028b5095b04a:/usr/local/tomcat# ls -al webapps/
total 236
drwxr-x--- 3 root root 4096 Mar 16 09:50 .
drwxr-sr-x 14 root staff 4096 Mar 16 09:51 ..
drwxr-x--- 3 root root 4096 Mar 16 09:50 Catalina
-rw------- 1 root root 12895 Jan 10 21:05 catalina.policy
-rw------- 1 root root 7202 Jan 10 21:05 catalina.properties
-rw------- 1 root root 1338 Jan 10 21:05 context.xml
-rw------- 1 root root 1149 Jan 10 21:05 jaspic-providers.xml
-rw------- 1 root root 2358 Jan 10 21:05 jaspic-providers.xsd
-rw------- 1 root root 3622 Jan 10 21:05 logging.properties
-rw------- 1 root root 7511 Jan 10 21:05 server.xml
-rw------- 1 root root 2164 Jan 10 21:05 tomcat-users.xml
-rw------- 1 root root 2633 Jan 10 21:05 tomcat-users.xsd
-rw------- 1 root root 168133 Jan 10 21:05 web.xml
$ docker stop foo4
volume 與本機目錄掛載測試:
接下來是一些 volume 與本機目錄的掛載測試,主要是要看確認資料是否真的共享,與掛載本機目錄之後的 container 其目錄變化,測試將會使用到 tomcat container,還會用到自己打包的 war 檔。
首先在測試前先準備好 war 檔,放在本機目錄,
$ ls /Users/mayer/Documents/dockerspace/volumetest/
miniweb.war
接著啟動一個沒有掛載 volume 的 tomcat container,然後觀察其 webapps 目錄,可以看到 webapps 底下會有一些預設安裝 tomcat 時的應用程式:
$ docker run -d --name foo tomcat:8.5.11-jre8
$ docker exec -it foo /bin/bash
$ root@635985e5b693:/usr/local/tomcat# ls webapps
ROOT docs examples host-manager manager
接著使用 -v flag 啟動一個有掛載的 container,將本機目錄掛載到 webapps 目錄:
$ docker run -d -v
/Users/mayer/Documents/dockerspace/volumetest:/usr/local/tomcat/webapps --name bar tomcat:8.5.11-jre8
然後觀察 tomcat webapps 目錄,可以跟上面沒掛載的對照看,發現原本的 ROOT 等等目錄都不見了,只有你本機目錄上的 war 檔與 tomcat 自動幫你解開的目錄。也就是說原本 webapps 的目錄被隱藏了,現在只會顯示 volume 掛載目錄的內容。
$ docker exec -it bar /bin/bash
root@3eca0381a364:/usr/local/tomcat# ls webapps
miniweb miniweb.war
然後接著測試是否真的目錄共享,先在 container 內新增一個 bar.txt,然後退出 container bash,觀察本機目錄,可以看到本機目錄上也新增了一個 bar.txt 檔案:
root@3eca0381a364:/usr/local/tomcat# touch webapps/bar.txt
root@3eca0381a364:/usr/local/tomcat# ls webapps
bar.txt miniweb miniweb.war
root@3eca0381a364:/usr/local/tomcat# exit
$ ls /Users/mayer/Documents/dockerspace/volumetest
bar.txt miniweb miniweb.war
接著反向測試,看看在本機目錄新增 foo.txt,再回到 container 內觀察 webapps 目錄是否也會新增一個 foo.txt。從結果可以看出,container 內也新增 foo.txt 檔案了:
$ touch /Users/mayer/Documents/dockerspace/volumetest/foo.txt
$ docker exec -it bar /bin/bash
root@3eca0381a364:/usr/local/tomcat# ls webapps
bar.txt foo.txt miniweb miniweb.war
透過上面的幾項簡單測試,可以看出當 volume 掛載本機目錄時,container 的目錄與本機的目錄資料會完全共用。
需要注意這個測試和上一個測試的不同之處,若是透過 volume 把本機目錄掛載到 container 的目錄,則 container 目錄裡面原本的內容會被完全隱藏,只是出現你本機目錄的檔案;如果是單純的建立 volume 然後掛載到 container 的目錄,則對於初次建立的 volume,Docker 會將 container 目錄裡面原本的內容全部都複製到 volume 上。
使用其他 container 當成 data volume
官方有提到另一個用法,就是在啟動 container 時使用 volumes-from 指令,來分享 container 之間的資料,對於此用法我個人感受不深,覺得很不直覺,因此這邊只會紀錄一下官方的範例。
首先先建立一個有名稱的 container。該 container 不執行任何的應用,他只重新利用 training/postgres image,使所有容器都使用共同的層,節省磁盤空間。
$ docker create -v /dbdata --name dbstore training/postgres /bin/true
接著你就可以在其他 container 上使用 --volumes-form 來掛載 /dbdata volumes。
$ docker run -d --volumes-from dbstore --name db1 training/postgres
$ docker run -d --volumes-from dbstore --name db2 training/postgres
這範例,如果 training/postgres images 原本 /dbdata 目錄內有資料,則會被隱藏,只有從 dbstore container 掛載過來的資料可以被看見。
另外也可以用在備份上,可以透過以下方式進行備份:
$ docker run --rm --volumes-from dbstore -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /dbdata
然後將備份還原到其他 container:
$ docker run -v /dbdata --name dbstore2 ubuntu /bin/bash
$ docker run --rm --volumes-from dbstore2 -v $(pwd):/backup ubuntu bash -c "cd /dbdata && tar xvf /backup/backup.tar --strip 1"
移除 volumes
使用 volumes 時要注意到,docker 並不會自動幫你移除你沒用到的 volumes。例如你建立 foo container 並掛載一個 foovolume,當你的 foo container 移除時,foovolume 會繼續存在。
在 docker run 時,可以加上 --rm 指令,讓 container 移除時一併把匿名 volume 移除,比如說以下指令:
$ docker run --rm -v /foo -v awesome:/bar busybox top
此命令會建立一個匿名的 /foo volume。當 container 被移除時,Docker Engine 會移除 /foo volume,而不會移除 awesome volume。
你可以用以下指令查看看哪些 volumes 目前沒被任何 container 關聯,然後在下指令移除他們:
docker volume ls -qf dangling=true
docker volume rm $(docker volume ls -qf dangling=true)
若你也像我一樣,目前正在測試階段,則可以直接使用以下指令,清除沒被用到的 volumes:
參考
Manage data in containers
Docker volume 簡單用法
深入理解Docker Volume(一)