若是您以爲咱們寫得還不錯,記得 點贊 + 關注 + 評論 三連🥰🥰🥰,鼓勵咱們寫出更好的教程💪前端
數據是一切應用和服務的核心,特別是目擊了一次次「刪庫跑路」引起的慘劇以後,咱們更能深刻體會到數據存儲與備份的重要性。Docker 也爲咱們提供了方便且強大的方式去處理容器的數據。在這一篇文章中,咱們將帶你經過理論和實戰的方式掌握 Docker 的兩種經常使用的數據管理方式:數據卷(Volume)和綁定掛載(Bind Mount),從而可以遊刃有餘地處理好數據,爲你的應用提供強有力的支撐和保障。git
很久不見,歡迎繼續閱讀「築夢師系列」 Docker 教程,前情回顧:github
而在這一篇教程中,咱們將帶你上手 Docker 數據管理,搭建起」夢境「(容器環境)與」現實「(主機環境)的橋樑。Docker 數據的管理方式主要分爲三種:mongodb
注意docker
tmpfs 掛載只適用於 Linux 操做系統。數據庫
咱們立刻經過幾個小實驗來體驗一下(已經比較熟悉的同窗能夠直接移步下面的」實戰演練「環節)。編程
正如在上一篇中最後「記住幾十個 Docker 命令小訣竅」所提到的,數據卷(Volume)也是常見的 Docker 對象類型的一種,所以也支持 create
(建立)、inspect
(查看詳情)、ls
(列出全部數據卷)、prune
(刪除無用數據卷)和 rm
(刪除)等操做。json
咱們來走一個流程體驗一下。首先建立一個數據卷:後端
docker volume create my-volume
複製代碼
查看當前全部的數據卷:
docker volume ls
複製代碼
輸出了剛剛建立的 my-volume
數據卷:
local my-volume
複製代碼
查看 my-volume
數據卷的詳細狀況:
docker volume inspect my-volume
複製代碼
能夠看到輸出了 JSON 格式的 my-volume
信息:
[
{
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/my-vol/_data",
"Name": "my-volume",
"Options": {},
"Scope": "local"
}
]
複製代碼
提示
好奇的同窗可能會去查看
/var/lib/docker/volumes
目錄下面是否是真的有數據卷,答案是:對於非 Linux 系統而言(Windows 和 Mac 系統),該目錄不存在於你的文件系統中,而是存在於 Docker 虛擬機中。
最後刪除 my-volume
數據卷:
docker volume rm my-volume
複製代碼
單首創建一個數據卷意義不大,畢竟它原本的做用就是爲容器的數據管理服務。請看下圖(來源 Safari Books Online):
能夠看到,數據卷在「主機環境」和「容器環境」之間架起了「一道橋樑」。一般,咱們在容器中將須要存儲的數據寫入數據卷所掛載的路徑(位置),而後就會當即、自動地將這些數據存儲到主機對應的區域。
在建立帶有數據卷的容器時,一般有兩種選擇:1)命名卷(Named Volume);2)匿名卷(Anonymous Volume)。接下來咱們就分別詳細講解。
首先咱們來演示一下如何建立帶有命名卷的容器,運行如下命令:
docker run -it -v my-vol:/data --name container1 alpine
複製代碼
能夠看到,咱們經過 -v
(或者 --volume
)參數指定了數據卷的配置爲 my-vol:/data
,其中(你應該猜到了)my-vol
就是數據卷的名稱,/data
就是容器中數據卷的路徑。
在進入容器中後,咱們向 /data
目錄中添加一個文件後退出:
/ # touch /data/file.txt
/ # exit
複製代碼
注意
/ #
是 alpine 鏡像默認的命令提示符,後面的touch /data/file.txt
纔是真正要執行的命令哦。
爲了驗證 /data
中的數據是否真的保存下來,咱們刪除 container1
容器,而後再建立一個新的容器 container2
,查看其中的 /data
目錄內容:
docker rm container1
docker run -it -v my-vol:/data --name container2 alpine
/ # ls /data
file.txt
/ # exit
複製代碼
能夠看到剛剛在 container1
中建立的 file.txt
文件!事實上,這種在容器之間共享數據卷的模式很是常見,Docker 提供了一個方便的參數 --volumes-from
來輕鬆實現數據卷共享:
docker run -it --volumes-from container2 --name container3 alpine
/ # ls /data
file.txt
複製代碼
一樣,container3
中也能訪問到數據卷中的內容。
建立匿名卷的方式就很簡單了,以前咱們經過 my-vol:/data
做爲 -v
的參數,而建立匿名卷只需省略數據卷名稱(my-vol
便可):
docker run -v /data --name container4 alpine
複製代碼
咱們經過 inspect
命令來查看一下 container4
的狀況:
docker inspect container4
複製代碼
咱們能夠在其中的 Mounts
字段中看到以下數據:
"Mounts": [
{
"Type": "volume",
"Name": "dfee1d707956e427cc1818a6ee6060699514102e145cde314d4d938ceb12dfd3",
"Source": "/var/lib/docker/volumes/dfee1d707956e427cc1818a6ee6060699514102e145cde314d4d938ceb12dfd3/_data",
"Destination": "/data",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
]
複製代碼
咱們來分析一下重要的字段:
Name
即數據卷的名稱,因爲是匿名卷,因此 Name
字段就是一串長長的隨機數,命名卷則爲指定的名稱Source
爲數據卷在主機文件系統中的存儲路徑(以前說了,Windows 和 Mac 在 Docker 虛擬機中)Destination
爲數據卷在容器中的掛載點RW
指可讀寫(Read-Write),若是爲 false
,則爲只讀數據卷在 Dockerfile 中使用數據卷很是簡單,只需經過 VOLUME
關鍵詞指定數據卷就能夠了:
VOLUME /data
# 或者經過 JSON 數組的方式指定多個數據卷
VOLUME ["/data1", "/data2", "/data3"] 複製代碼
有兩點須要注意:
docker run -v
指定數據卷時,Dockerfile 中的配置會被覆蓋綁定掛載(Bind Mount)是出現最先的 Docker 數據管理和存儲解決方案,它的大體思路和數據卷是一致的,只不過是直接創建本機文件系統和容器文件系統之間的映射關係,很是適合簡單、靈活地在本機和容器之間傳遞數據。
咱們能夠試着把本身機器的桌面(或者其餘路徑)掛載到容器中:
docker run -it --rm -v ~/Desktop:/desktop alpine
複製代碼
咱們仍是經過 -v
參數來進行配置,~/Desktop
是本機文件系統路徑,/desktop
則是容器中的路徑,~/Desktop:/desktop
則是將本機路徑和容器路徑進行綁定,彷彿架起了一道橋樑。這裏的 --rm
選項是指在容器中止以後自動刪除(關於容器生命週期的更多細節,請參考第一篇文章)。
進入到容器以後,能夠試試看 /desktop
下面有沒有本身桌面上的東西,而後再在容器中建立一個文件,看看桌面上有沒有收到這個文件:
/# ls /desktop
# 我本身桌面上的不少東西 :D
/# touch /desktop/from-container.txt
複製代碼
你應該能看到本身的桌面上多了容器中建立的 from-container.txt
文件!
咱們貼出官方文檔這張示意圖:
能夠看到:
在指定數據卷或綁定掛載時,-v
參數的格式爲 <first_field>:<second_field>:<rw_options>
(注意經過冒號分隔),包括三個字段,分別是:
ro
(Read-only),則爲只讀提示
Docker 在 17.06 版本以後引入了
--mount
參數,功能與-v
/--volume
參數幾乎一致,經過鍵值對的方式指定數據卷的配置,更爲冗長但也更清晰。這篇文章將詳細講解更爲常見和廣泛的-v
參數,--mount
參數的更多使用可參考文檔。
好的,終於到了實戰演練環節——繼續部署咱們以前一直在作的全棧待辦事項項目(React 前端 + Express 後端 + MongoDB 數據庫)。若是你沒有閱讀以前的教程,想直接從這一步開始作起,請運行如下命令:
git clone -b volume-start https://github.com/tuture-dev/docker-dream.git
cd docker-dream
複製代碼
在以前項目的基礎上,咱們打算
OK,咱們在 server/Dockerfile
中添加 VOLUME
配置,而且指定 LOG_PATH
(日誌輸出路徑環境變量,可參考 server/index.js
的源碼)爲 /var/log/server/access.log
,代碼以下:
# ...
# 指定工做目錄爲 /usr/src/app,接下來的命令所有在這個目錄下操做
WORKDIR /usr/src/app
VOLUME /var/log/server
# ...
# 設置環境變量(服務器的主機 IP 和端口)
ENV MONGO_URI=mongodb://dream-db:27017/todos
ENV HOST=0.0.0.0
ENV PORT=4000
ENV LOG_PATH=/var/log/server/access.log
# ...
複製代碼
而後 build 服務器鏡像:
docker build -t dream-server server/
複製代碼
稍等片刻後,咱們把整個項目開起來:
# 建立網絡,便於容器互聯
docker network create dream-net
# 啓動 MongoDB 容器(dream-db)
docker run --name dream-db --network dream-net -d mongo
# 啓動 Express API 容器(dream-api)
docker run -p 4000:4000 --name dream-api --network dream-net -d dream-server
# 構建提供 React 前端頁面的 Nginx 服務器
docker build -t dream-client client
# 啓動 Nginx 服務器容器(client)
docker run -p 8080:80 --name client -d dream-client
複製代碼
經過 docker ps
確保三個容器都已經開啓:
訪問 localhost:8080
,進入到待辦事項頁面,建立幾個事項:
以前咱們把日誌數據存儲到了匿名卷中,因爲直接獲取數據卷中的數據是比較麻煩的,推薦的作法是經過建立一個新的臨時容器,經過共享數據卷的方式來備份數據。聽着有點暈?請看下圖:
按照如下步驟進行:
第一步,實現 dream-api
容器和數據卷之間的數據共享(已實現)。
第二步,建立臨時容器,獲取 dream-api
的數據卷。運行如下命令:
docker run -it --rm --volumes-from dream-api -v $(pwd):/backup alpine
複製代碼
上面這句命令同時用到了上面講解的數據卷和綁定掛載:
--volumes-from dream-api
用於容器之間共享數據卷,這裏咱們獲取 dream-api
的數據卷-v $(pwd):/backup
用於創建當前本機文件路徑(pwd
命令獲取)和臨時容器內 /backup
路徑的綁定掛載第三步,進入臨時容器以後,咱們把日誌數據壓縮成 tar 包放到 /backup
目錄下,而後退出:
/ # tar cvf /backup/backup.tar /var/log/server/
tar: removing leading '/' from member names
var/log/server/
var/log/server/access.log
/ # exit
複製代碼
退出以後,是否是在當前目錄看到了日誌的備份 backup.tar
?事實上,咱們能夠經過一條命令搞定:
docker run -it --rm --volumes-from dream-api -v $(pwd):/backup alpine tar cvf /backup/backup.tar /var/log/server
複製代碼
若是你以爲上面這條命令難以理解的話,答應我,必定要去仔細看看上一篇文章中的」回憶與昇華「-」理解命令:夢境的主旋律「這一部分!
接下里就是這篇文章的重頭戲,各位打起十二分的精神!咱們的應用會不會遭遇刪庫跑路的危機全看你有沒有學會這一節的操做技巧了!
提示
咱們這裏使用 MongoDB 自帶的備份與恢復命令(
mongodump
與mongorestore
),其餘數據庫(例如 MySQL)也有相似的命令,均可以借鑑本文的方式。
按照以前共享數據卷的思路,咱們也嘗試經過一個臨時 Mongo 容器來備份數據。示意圖以下:
首先,咱們的臨時容器得鏈接上 dream-db
容器,並配置好綁定掛載,命令以下:
docker run -it --rm -v $(pwd):/backup --network dream-net mongo sh
複製代碼
和以前備份日誌數據相比,咱們要把這個臨時容器鏈接到 dream-net
網絡中,它才能訪問到 dream-db
的數據進行備份(不熟悉 Docker 網絡的同窗可複習前一篇文章)。
第二步,進入到這個臨時容器後,運行 mongodump
命令:
/ # mongodump -v --host dream-db:27017 --archive --gzip > /backup/mongo-backup.gz
複製代碼
此時,因爲綁定掛載,輸出到 /backup
的文件將保存到當前目錄(pwd
)中。退出後,就能夠在當前目錄下看到 mongo-backup.gz
文件了。
在前一篇教程的」回憶與昇華「部分,咱們輕描淡寫地講解了經過 docker exec
執行 mongodump
命令來作備份,可是當時輸出的備份文件仍是停留在容器中,只要容器被刪除,備份文件也就消失了。因而一個很天然的想法就出現了:咱們能不能在建立數據庫容器的時候就作好綁定掛載,而後經過 mongodump
把數據備份到掛載區域?
事實上,以前在建立數據庫容器的時候,運行如下命令:
docker run --name dream-db --network dream-net -v $(pwd):/backup -d mongo
複製代碼
而後再經過 docker exec
執行 mongodump
命令:
docker exec dream-db sh -c 'mongodump -v --archive --gzip > /backup/mongo-backup.gz'
複製代碼
就能夠輕鬆實現。這裏咱們用 sh -c
來執行一整條 Shell 命令(字符串形式),這樣避免了重定向符 >
引起的歧義(不理解的話能夠把 sh -c 'xxx'
替換成 xxx
)。能夠看到,mongodump
的命令簡單了許多,咱們不再須要指定 --host
參數,由於數據庫就在本容器內。
可是有個問題:若是已經建立了數據庫,而且沒有提早作綁定掛載,這種方法就行不通了!
有了數據庫備份文件,咱們就能夠肆無忌憚地來作一波」演習「了。經過如下命令,直接端了目前的數據庫和 API 服務器:
docker rm -f --volumes dream-db
docker rm -f dream-api
複製代碼
沒錯,經過 --volumes
開關,咱們不只把 dream-db
容器刪了,還順帶把掛載的數據卷所有刪除!演習就是要足夠逼真才行。這時候再訪問 localhost:8080
,以前的待辦數據所有丟失!
開始災後重建,讓咱們再次建立新的 dream-db
容器:
docker run --name dream-db --network dream-net -v $(pwd):/backup -d mongo
複製代碼
注意到,咱們經過綁定掛載的方式把當前目錄映射到容器的 /backup
目錄,這意味着能夠在這個新的容器中經過 /backup/mongo-backup.gz
來恢復數據,運行如下命令:
docker exec dream-db sh -c 'mongorestore --archive --gzip < /backup/mongo-backup.gz'
複製代碼
咱們應該會看到輸出了一些日誌,提示咱們數據恢復成功。最後從新開啓 API 服務器:
docker run -p 4000:4000 --name dream-api --network dream-net -d dream-server
複製代碼
回頭訪問咱們的待辦應用,數據是否是都回來了!?
以前,咱們經過共享數據卷或者綁定掛載的方式來把容器的數據傳送到容器以外。事實上,在容器和本機之間還能夠經過另外一種方式傳遞和共享數據:docker cp
命令。沒錯,若是你用過 cp
命令拷貝文件,它的用法必定不會陌生。例如,咱們將 dream-api
容器內的日誌文件拷貝到當前目錄下:
docker cp dream-api:/var/log/server/access.log .
複製代碼
看!access.log
就有了!固然,咱們還能夠」反向操做「一波,把本地的文件拷貝到容器裏面去:
docker cp /path/to/some/file dream-api:/dest/path
複製代碼
能夠看到,docker cp
用起來很是方便,很適合一次性的操做。缺陷也很明顯:
在備份和恢復數據庫時,有一個更加簡單粗暴的思路:爲何咱們不能直接備份整個容器呢?事實上,Docker 確實爲咱們提供了兩個命令來搞定整個容器的打包和裝載:export
和 import
。
例如,經過如下命令將整個容器的文件系統導出爲 tar
包:
docker export my-container > my-container.tar
複製代碼
注意
export
命令不會導出容器相關數據卷的內容。
而後能夠經過 import
命令建立擁有徹底相同內容的鏡像:
docker import my-container.tar
複製代碼
import
命令會輸出一個 SHA256 字符串,就是鏡像的 UUID。接着能夠用 docker run
命令啓動這個鏡像(能夠指定 SHA256 串,也能夠先經過 docker tag
打個標籤)。
若是你剛剛嘗試了 export
和 import
命令,必定會發現一個至關嚴重的問題:容器打包以後的 tar 包有好幾百兆。很顯然,簡單粗暴地打包容器也包括了不少根本無用的數據(例如操做系統中的其餘文件),對硬盤的壓力陡然增長。
在學習和實踐了數據卷的知識後,咱們還接觸了一下 docker cp
和 docker export/import
命令。至此,咱們不由追問,鏡像和容器的本質究竟是什麼,其中的數據是怎樣存儲的?
或者咱們提一個更具體的問題:爲何鏡像中的數據(例如操做系統中的各類文件)每次建立容器時都會存在,而在建立容器後寫入的數據會在容器刪除後卻丟失?
這背後的一切就是 Docker 賴以生存的 Union File System(UFS)機制。咱們經過一張圖(來源:The Docker Ecosystem)來大體感覺一下:
咱們來一點點分析上面這張 UFS 示意圖的要點:
而咱們這一篇文章所講解的數據管理技巧(數據卷、綁定掛載),則是徹底繞開了 UFS,讓重要的業務數據獨立存儲,而且可備份、可恢復,而不是陷入在容器的可寫層中讓整個容器變得臃腫不堪。
再回過頭看上面的問題,是否是有思路了?
想要學習更多精彩的實戰技術教程?來圖雀社區逛逛吧。
本文所涉及的源代碼都放在了 Github 上,若是您以爲咱們寫得還不錯,但願您能給❤️這篇文章點贊+Github倉庫加星❤️哦