5.5 另一種方式:Bind Mount
除了前面談到的 volume 之外,bind mount 也是另外一種把不屬於映像檔本身的檔案放入容器中的方法。
這個功能本身的設計非常的酷,第一次使用的時候真的有一種『 啊! 』的聲音出現,完全讓人理解要如何在本地端使用 Docker 進行開發。
而實際上 Bind Mount 就是單純的把本機的檔案如同字面上的意思,掛載到容 器內,而這個指令沒有辦法寫在 Dockerfile 裡面,因為必須把一個真實存在的資料夾或是檔案掛載到容器內,所以只能在 docker container run 的時候加入。
而在背後執行的原理,就是本機的路徑和容器內的路徑指向本機的同一個檔案,而使用起來的方式也很像 volume,容器的刪除並不會連帶地刪除掉本機的檔案,兩個之間的優先度,當然是本機獲勝!
接下來一樣使用 nginx 來示範一下,bind mount 的使用情境是如何,不要覺得為什麼又是 nginx,因為它最輕鬆就可以把畫面體現在瀏覽器上,可以很快地看到差異。
--mount 或是 --volume 都可以
這個小節的標題可能會有點疑惑,--volume 剛剛不是介紹過了嗎?
這是因為要使用 bind mount 這個功能,不論是用 --volume 或是 --mount 的指令都可以,但就是有一些細節需要注意。
本次的範例放在本書 GitHub 儲存庫中 ch-05 的 nginx-example 中。
進入到這個資料夾後,可以透過標題提到的兩種方式,達到一模一樣的目的。
$ docker container run --detach --name nginx-volume --publish 80:80 --volume $(pwd):/usr/share/nginx/html nginx # 不換行
6cb48a848c2373ff65b...
接著打開瀏覽器輸入 http://localhost,會看到『 Hi, 你成功的使用了 Bind Mount 功能了!』的字樣,我們利用了資料夾內本地的 index.html 去替換掉了 nginx 原先設定好的 index.html。
這是第一種方式,一樣使用 --volume 的指令,但是搭配的是本地端的絕對路徑而非上一小節的 volume 名稱,而 $( ) 的寫法,則是在 Docker 的指令中穿插終端機指令的作法 ,這邊的 pwd 在 Linux 以及 macOS 代表的是此處的意思。
而在 Windows 系統中的使用者,則可能需要將指令更改為下方所示 ( %cd% ),這單純就只是作業系統不同導致指令不相同,但實際上要傳達給 Docker 的意思是一樣的。
$ docker container run --detach --name nginx-volume --publish 80:80 --volume %cd%:/usr/share/nginx/html nginx
# 不換行
換句話說,--volume 可以使用 Volume 的方式 ( 可能有點饒口 ),也就是上一個小節中替一個 volume 命名並且連接到容器上;也可以使用 Bind Mount 的方式將本機的檔案掛載至容器內。
接著來試試第二種方式實現 bind mount 的 功能。
$ docker container run --detach --name nginx-bind-mount --mount type=bind,source=$(pwd),target=/usr/share/nginx/html --publish 8080:80 nginx # 不換行
55a3af9c6969d425e64c304015b1...
打開瀏覽器輸入 http://localhost:8080,一樣可以看到相同的字樣 『 Hi, 你成功的使用了 Bind Mount 功能了!』來證明確實取代了 nginx 預設的畫面。
這個寫法確實會囉唆一些,但我覺得對於新手使用 Bind Mount 功能來說,是非常清晰易懂的,--mount 後方要傳遞三個參數給 Docker,分別是 type、source、target,而 type 就是使用 bind 的方式,source 則是一樣的 $(pwd) / %cd% ( Windows 使用者 ) 代表此處的絕對路徑,target 則是容器內的絕對路徑。
這邊實現了兩種指令都將本地的資料掛載至容器內,進去容器看看長什麼樣子吧,現在有兩個容器都在背景執行,想去哪一個都可以。
$ docker container exec --interactive --tty nginx-bind-mount bash
root@55a3af9c6969:/# cd /usr/share/nginx/html
root@55a3af9c6969:/# ls
Dockerfile LICENSE README.md index.html
可以看到連這個資料夾內的 Dockerfile 以及 README.md 甚至是 LICENSE 都掛載進來了,是因為這邊用一整個檔案目錄去取代掉另一個檔案目錄;要注意的地方是我們不能夠使用一個檔案去取代掉檔案目錄,後面會做一個錯誤示範來驗證這件事。
而現在要做的是 Bind Mount 最讓人驚艷的功能,現在我們還停留在 nginx-bind-mount 這個容器內,接著打開一個終端機的視窗,並且進入 docker-volume-nginx-example 這個資料夾,並且隨意新增一個檔案。
# 另一個終端機視窗
$ cd docker-volume-nginx-example
$ touch test.txt
# 原本的終端機視窗,nginx-bind-mount 的容器內
root@55a3af9c6969:/# ls
Dockerfile LICENSE README.md index.html test.txt
非常有趣,剛剛新增的 test.txt 這個檔案就這樣即時的更新到了容器內,而且完全不需要重新啟動容器,也不必做任何的指令,這也讓使用 Docker 在本機開發變成一件輕鬆寫意的事情。
只需要啟動一個符合應用程式環境的容器,並且把開發的檔案掛載到容器內,一切就搞定了,也不必在容器內去做複雜的設定,因為需要的檔案還是存放在本機中。
而前面有提過背後的原理就是 本機的路徑和容器內的路徑指向本機的同一個檔案,所以不論是在容器內做編輯,刪除,都會相同的影響到本機的檔案。
接著是剛剛提到的錯誤示範,來驗證 不能夠使用一個檔案去取代掉檔案目錄 這句話,這邊的 source 只有 index.html 一個檔案,但是 target 卻是一整個檔案目錄。
$ docker container run --detach --name nginx-bind-mount-wrong --mount type=bind,source=$(pwd)/index.html,target=/usr/share/nginx/html --publish 8081:80 nginx # 不換行
566580d89326987432d87d7c434ed23875728f29aa54673e2e831d208e76733a
docker: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: error mounting "/host_mnt/Users/RobertChang/docker-volume-nginx-example/index.html" to rootfs at "/usr/share/nginx/html": mount /host_mnt/Users/RobertChang/docker-volume-nginx-example/index.html:/usr/share/nginx/html (via /proc/self/fd/14), flags: 0x5000: not a directory: unknown: Are you trying to mount a directory onto a file (or vice-versa)? Check if the specified host path exists and is the expected type.
Docker 很聰明地會提示你,source 本身並不是一個 directory,這是在掛載時需要注意的小地方。
小小測驗
這並不是實際的練習題,而是因為 Volume 以及 Bind Mount 是兩個不同的概念,但做到的事情很像,所以在這邊有幾個小小的問題希望大家可以作答,問問自己,不知道可以直接看答案並思考一下。
第一題:哪一種方式可以將存在於本機的檔案連接到容器內?
第二題:當我們在使用 bind mount 的方式時,$(pwd) 代表的是什麼意思呢?
第三題:今天運行了一個需要儲存空間的服務 ( MySQL, PostgreSQL, Redis, Elasticsearch ... ),我要如何知道容器內存放資料的路徑呢?
解答
第一題: bind mount 的方式才能實踐這一個功能;volume 的方式並不能將存在本機的檔案放入容器內,volume 更像是替容器開了一個外部儲存空間的概念。
第二題: 代表這裡的意思,英文的原意是 print working directory,印出現在正在工作的資料夾 ( 絕對路徑 )
第三題: 仔細的閱讀 DockerHub 上面的說明,這類型需要儲存空間的服務一定都會寫在說明中告訴你容器內部存放資料的路徑;再者就是前面有示範過的方式,先拉下要使用的映像檔,並且使用 docker image inspect 的方式去查看存放資料的路徑。
不知道大家是不是都有答對呢?並真的理 解了 Volume 以及 Bind Mount 的差別,至於實際應用的場景則會在後面的章節一一出現。
升級資料庫版本練習
每一個章節都會提供一些小小的練習來熟悉本章的內容,通常會比較難一點;若是想不出來也不需要灰心,有些時候可能是指令不熟悉導致,可以往前翻閱來加強記憶喔,亦或是翻到後面的解答章節也可以幫助你解惑喔,記得 --help 以及 docs.docker.com 會是你最好的夥伴。
這個練習是模擬一個真實的情境,原先使用了 mariadb:10.3 版本的映像檔作為資料庫的服務,但因為某些原因,需要將服務升級到 mariadb:10.8 的映像檔作為服務,而這次的練習目的就是透過已命名的 volume 來保存資料,並且升級服務的同時資料不會遺失。
在非 Docker 環境的作法下,我們會直接用套件管理工作 ( brew, apt ... ) 去升級 mysql 的版本,而關於資料儲存的部分,這些升級都會在背後幫助你完成一切的手續,只需要 brew upgrade mariadb 就完工了。
- 以 mariadb:10.3 的映像檔建立容器,並且把 volume 命名為 mariadb-data 且連接到容器上,並確認容器的 logs 一切正常。
- 透過閱讀 DockerHub 上的使用說明來得到啟動容器所需要的參數以及容器內儲存資料的路徑。
- 確認一切都沒問題後,暫停掉以 mariadb:10.3 的映像檔所建立容器
- 改以 mariadb:10.8 的映像檔來建立新的容器,並且將 mariadb-data 這個 volume 連接到容器上,並確認容器的 logs 一切正常。
升級資料庫版本練習解答
- 以 mariadb:10.3 的映像檔建立容器,並且把 volume 命名為 mariadb-data 且連接到容器上,並確認容器的 logs 一切正常,並透過閱讀 DockerHub 上的使用說明來得到啟動容器所需要的參數以及容器內儲存資料的路徑。
來到 mariadb 的 DockerHub 頁面,可以看到使用說明上關於啟動服務需要的環境變數。

接著需要找到容器內資料庫的儲存路徑,往下滑,會看到官方說明也有明確地標示出要如何儲存資料。

接著可以正式的啟動 maridb:10.3 的服務了。
$ docker run --detach --name maria-db --env MARIADB_USER=user --env MARIADB_PASSWORD=password --env MARIADB_ROOT_PASSWORD=password --volume mariadb-data:/var/lib/mysql mariadb:10.3 # 不換行
Unable to find image 'mariadb:10.3' locally
10.3: Pulling from library/mariadb
675920708c8b: Pull complete
8cd439041170: Pull complete
566a6a9b01f3: Pull complete
033be70909e9: Pull complete
e62d5ac9f0cc: Pull complete
482b55ad0b73: Pull complete
73f5e77b89d0: Pull complete
5bc9da099573: Pull complete
59fd6ada2b2c: Pull complete
eaaa8398274c: Pull complete
0fdd4bd4ac7c: Pull complete
938a2e87683b: Pull complete
Digest: sha256:c769235db77fff4b6dc1eccb32cfc51b40b686a450bf12e6eecf...
Status: Downloaded newer image for mariadb:10.3
8d1f095437b3fcfc1f535d97723924780ae494a1f4a0f03caee47132d17fded7