Docker Study Note
Docker Container 基礎入門篇 1
猴子也會的 Docker,但猴子並不想會。
- 2023/08/18 更新:關於 Docker 更深入的原理,歡迎參考敝作 Docker實戰6堂課:56個實驗動手做,掌握Linux容器核心技術
- 2023/08/21 更新: 基礎入門 0,為了 Web Conf 設計的,給前端的 Docker 入門課,歡迎參考 [WebConf2023] Docker 入門 101。
- 2023/8/28 更新:調整成新版指令。
Docker 到今天,應該是很多人都會的基本技術了,原理可能不一定非常了解,但基本的操作應該或多或少都知道點,相關的文章也非常多,很猶豫要不要寫這一篇,其實我一開始是想要筆記 Amazon ECS 的,不過為了更好地學習 ECS,還是決定從 Docker container 開始複習起。
由於我自己的學習方式,通常是從實作開始,只有透過自己動手做,才能對一項技術更有感覺,所以會先從基本操作開始慢慢延伸到深入一點的討論,自己複習一下,希望也能給別人帶來幫助。
Docker 安裝
- Mac: https://docs.docker.com/docker-for-mac/install/
- Ubuntu: https://docs.docker.com/engine/install/ubuntu/
其他 OS 的安裝在 Docker 官網都能找到,就不佔篇幅了,這個自行處理,如果有問題可以到社群或留言討論。
這邊特別提醒一下,本系列文的範例都是在 Ubuntu 上完成的,本文已更新至:
# 20230828 更新本文實驗
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 22.04.2 LTS
Release: 22.04
Codename: jammy
安裝完成之後,如果想要讓非 root user 也能用 docker 命令,也就是不用在每次下 docker 指令前都加 sudo 的話,記得執行下列指令,可以將你目前的使用者加入 docker 群組中,已獲得執行的權限:
# your-user 記得換成自己的使用者名稱
# 執行完成後,登出該使用者,再重新登入一次讓命令生效
$ sudo usermod -aG docker your-user
安裝完成後,如果要檢查安裝有沒有成功,可以用以下這個指令檢查看看版本:
# 20230828 更新本文實驗
$ docker --version
Docker version 24.0.5, build ced0996
或是執行以下兩個可以取得 docker 環境資訊的命令來測試:
# Show the Docker version information
$ docker version
# Display system-wide information
$ docker info
Docker 基本組成
上面這張圖算是統整了 Docker 的重要組成與指令了,我們先來看看 Docker 中重要的三個最基本的東西:
- image: 這是一個*唯讀*的模板,可用來建立 container,如果有學過物件導向語言,可以想成 image 就是 class、container 是 object 或是 instance。
- container: 這就是我們的主角,是一個獨立、隔離的空間,裡面包括了我們執行一個應用程式所需要的元件。
- Docker registry: 這其實就很像 github,裡面存放了很多 images,公開的像是 Docker Hub,你也可以建立自己私人的 registry。
我相信這樣說完之後,沒概念的還是沒概念,就讓我們直接啟動一個容器來看看吧!
Docker 指令: 啟動容器
在正確安裝 docker 後,執行此指令:
$ docker container run -it node:20 /bin/bash
應該會看到以下的畫面:
docker container run
:啟動一個新的 container,相關的選項還不少,這次會就幾個常用或重要的來練習。-i
:--interactive
啟動互動模式,保持標準輸入的開放。-t
:--tty
讓 Docker 分配一個虛擬終端機(pseudo-TTY),並且綁定到容器的標準輸出上。node:20
: 啟動這個 container 所依據的 image。/bin/bash
:容器啟動後要執行的命令。
簡單地說,以上指令就是我們「從 image node:20 啟動了一個 container,並且開啟了它的輸入/輸出,然後請它執行 /bin/bash
這個命令」。
在執行完這行指令後,你可以看到畫面停在 root@4c9bee8fef4b:/#
,要注意的是,這時候我們已經不在我們原本的環境中了,而是「進入」了 container 中,可以在這裡執行 node -v
指令,會發現這個環境中已經安裝了 node,且版本是 20.5.1。
這時候我們在 container 中執行 ps aux
會看到它 PID 為 1 的 process 就是我們剛剛指定的 /bin/bash
,這個非常的重要,但我們要放到後面再討論了。
這裡讓我們先做個小測試:
- 在剛剛建立的容器中,建立一個檔案,例如
touch AAA.txt
,建議完成後用ls
確認一下。 - 開啟另外一個命令視窗,同樣再執行一次
docker container run -it node:20 /bin/bash
,然後一樣進入了一個容器,執行ls
看看,應該會看不到AAA.txt
這個檔案才是。
這裡很重要的就是我們提到的 container 是一個獨立的、隔離的環境,當你啟動了兩個容器時,即便是從同一個 image 啟動起來,Docker 是會幫你做出兩個不同的容器的:
這時候再開啟一個新的命令視窗,然後執行以下指令:
$ docker container ls
這個指令是用來列出目前有哪些 container 在運行中,執行後應該可以看到:
這裡看到目前在環境中已經啟動了兩個容器,而你的 CONTAINER ID 與 NAMES 應該會跟上面圖片的不同,值得注意的是,這個容器 ID 其實會出現在容器的虛擬終端機中:
# 第一個 container 的提示命令
root@4c9bee8fef4b:/#
# 第二個 container 的提示命令
root@1d41325da509:/#
Docker Image
在更近一步討論容器的其他操作之前,我們先來想一下,node:20 這個 image 是哪裡來得呢?
不知道你有沒有注意到,我們剛剛啟動了兩個容器的執行過程其實不太一樣:
啟動第一個容器的畫面:
啟動第二個容器的畫面:
第一個容器啟動時會多了很多東西,第一行是Unable to find image 'node:20' locally
,這句話的意思是在「本地找不到這個 image」,所以接下來就會開始 pull from library/node
,也就是去 registry 拉取我們所指定的 image,沒有指定的話,通常就是去 Docker Hub 拉,例如 https://hub.docker.com/_/node,從這個頁面我們可以發現 Docker Hub 提供了不同版本的 node 環境,而我們剛剛用的是 20 這個版本。
而當我們啟動第二個容器的時候,因為本地已經有了 node:20 這個 image,所以就可以直接啟動,而不用再去拉取 image 了。
Docker Hub 上有各式各用的 image,非常地好用,例如你想要跑一個 MySQL,但不想裝在自己的電腦上,這時候你就可以從 Docker Hub 上拉取一個 MySQL,還可以隨時換不同的版本。不過這邊會建議盡量用官方提供的 image 比較好,畢竟你不知道別人提供的 image 裡放了什麼東西…
在啟動容器之前,也可以先用指令把 image 拉取好,例如 $ docker image pull node:18
,這就是要從 Docker Hub 上拉下一個 tag 為 18 的 node image 到本機來。
這邊補充一下 tag,docker image 的名稱中 :
前面的是名稱,後面的是 tag(image_name:tag_name
),tag 通常用來表現一些特殊的資訊,例如版本,像 node:20
,就是版本為 20 的 node image。當然,既然叫 tag,不是叫 version 什麼的,就表示它不一定是要用來標註版本,基本上就是留一個欄位讓你來標注一些資訊,而且這是非必要的、可以不用放,例如 docker image pull node
,當沒有放 tag 時,它會自動幫你拉 node:latest
這個 image,也就是說, latest
是預設的 tag。
如果想要看看自己的環境中目前有哪些 image,可以用以下指令:
$ docker image ls
這邊可以看到,兩個 image 的大小分別都有 1 G 多,雖然這不是這兩個 images 真正佔用的硬碟空間,但如果你使用的 images 都比較沒有關聯,那日積月累下來,還是很佔硬碟空間的。所以沒事不要亂拉,或是要定期清理,才不會佔用太多硬碟空間喔,之後再來分享怎麼清理。(雖然我們後面會討論到 image layer 共用的部分,但積少成多,在主機中放了一堆 images,硬碟還是會被撐爆的…)
Image Layer
剛剛在說到 image 時,我們說 image 是一個「唯讀」的模板,這是什麼意思呢?還記得我們剛剛做的測試嗎?在第一個 container 中新增了一個 AAA.txt,但後來啟動的第二個 container 中並沒有看到這個檔案。
這是因當我們從 image 啟動一個 container 時候,Docker 會載入這個 image 作為唯讀層,並且在上面加上一個可寫層,而我們在這個 container 中的操作實際上就是發生在這個可寫層中:
不管你在這個可寫層中做了多少事情,例如在這個 container 中加入一個新的檔案,這個檔案是存在這個獨立的可寫層中的,當你下次又從 node:20 這個 image 來建立另外一個新的 container 時,仍會用原本藍色這個區塊(原本的 node:20)來建構這個 container,並且在上面加上另外一個可寫層,做出另外一個 container。
那總不能每次都是一個新的環境,而無法保留我們之前做過的事對吧,因此 Docker 提供的一個指令,讓我們可以把我們的容器做成一個新的 image:
docker container commit CONTAINER_ID [Repository:[Tag]]
# 在本文的範例中,執行指令如下,記得把 container id 換成你自己的:
$ docker container commit 4c9bee8fef4b node:20-updated
當執行完這個指令後,來檢視一下目前本機中有的 images:
多了一個 node:20-updated
,用這個 image 來啟動一個 container 看看:
用這個新的 image 建構出來的 container 會有 AAA.txt
這個檔案,這是因為我們用 node:20-updated
這個我們 commit 出來的新 image,會在原有的 node:20
上面再加上一層(layer),並且把我們剛剛對那個 container 做的事給封裝起來,變成一個新的 image,當我們用這個新的 image 來建構新的 container 時,就會在其上再加上一層可寫層來讓我們操作:
Docker 很聰明的是,雖然我們在電腦中目前有 node:20 與 node:20-updated 兩個不同的 image,不過 Docker 並不會重複存兩個 node:20 ,也就是重複的部分 Docker 只會存一個,這樣就大大的節省了空間,此外也可以理解為什麼要作成唯讀了,唯讀能做到共用而不互相影響。(關於這部分,在後面更進階的討論中,再來聊實際上的實作,這邊就先這樣簡單地認識一下。)
當然,原本我們從 Docker Hub 上拉下來的 node:20 也不會只有單獨一層,所以當我們在 pull 這個 image 時,會看到的畫面是像下面這樣:
按照這個過程,node:20 至少是由 8 個 layer 構成的。還記得我們 pull node:18
的過程嗎:
這裡像是 de4cac68b616 顯示的是 Already exists
,這是因為我們在拉 node:20 時,已經拉過了,因此在拉 node:18 時,已經存在的就不用再重拉一次,這也再次驗證了在 Docker 中,layer 是共用的、不會多佔空間的。
這邊有個不太常用的指令,但在熟悉 Docker 時可以玩玩看:
$ docker image history node:20
我們也來查看一下 node:20-updated 這個 image 並且比較看看:
這邊可以看到 node:20-updated 比起 node:20 會多了一個 78c69cf95de8 ,這個就是我們剛剛啟動第一個 container 時做出來的一層,而這之下,就跟 node:20 一模一樣了。
最後,這個 commit 出來的新 image,你也可以把它推送(push)到 Docker Hub 或是其他的 image repository 上去分享給別人。不過,在實務上,我們很少直接用 commit
這個指令,通常都會透過 Dockerfile
來做新的 image,這個就留到下次討論了。
其他 Docker 生命週期指令
延續剛剛的測試,我們在 node:20-updated
做出來的 container 中執行 exit
離開這個 container,這時候如果執行 docker container ls
會發現,只剩下兩個 container,而剛剛離開的那個 container 已經看不到了:
但如果加上 -a
,就可以檢視所有的 container:
這是因為我們剛剛在啟動 container 時,要它執行的命令是 /bin/bash
(且 PID 為 1),而當我們下了 exit
指令時,是在離開這個 bash,也就是離開了 PID 為 1 的這個 process,既然主要的 process 已經停止執行了,這個 container 自然也就關閉了,我們之後再來討論怎麼讓 container 持續執行。從這裡也可以從 Status 這個欄位看到 container 已經離開多久。
這時候如果想要回到這個 container,可以透過 docker container start CONTAINER_ID
來回到這個 container:
docker container start
只是重新啟動這個 container,執行後我們還是在本機中,如果要進入 container 中,可以透過 exec
來進入 container 中,基本上這個指令常用的參數,例如 -it
,跟 docker container run
的時候差不多,我們就先不討論了。
docker container exec -it CONTAINER_ID /bin/bash
這時候我們一樣再執行 exit,然後再下 docker container ls
來檢查,卻會發現這個 container 還是活著的、跟剛剛不一樣,我們透過 docker container exec
再進去一次,這次在裡面執行一下 ps aux
:
這時候我們可以看到有兩個 /bin/bash
的 process 在運行,我們透過 docker container exec
所執行的 /bin/bash
其實是 PID 為 26 的這一個,所以當我們執行 exit
時,離開的是這一個 bash,而 PID 為 1 的這個 /bin/bash
仍在執行中,所以這個 container 會持續存活。
跟 docker container exec
有一個用起來很像的指令 docker container attach CONTINER_ID
:
attach
這個指令一樣會進入 container 中,用起來跟剛剛的 docker container exec -it CONTAINER_ID
效果很像,但如果進去後執行 ps aux
會發現只有一個 /bin/bash
process。你應該可以猜到,如果這時候執行了 exit
,會離開的是 PID 為 1 的 /bin/bash
process,進而離開這個 container。此時再透過 docker container ls
或是 docker container ls -a
來確認,會發現這個 container 已經關閉了。所以,雖然 attach 也能進入這個 container 中,但我自己很少用就是了,以免一不小心把 container 給關掉…
移除 image 跟 container
前面有提到,如果電腦中存有太多的 image,會佔硬碟空間,因此建議定期清除用不到的 image,那我們來試試看清除剛剛拉下來的那個 node:18 ,其指令為 docker image rm IMAGE_ID
:
這邊可以看到,移除的時候也是逐層移除,不過,我們剛剛 pull
node:18 時,明明有 8 層,這邊卻只移除了 4 層,這樣真的有移乾淨嗎?
但透過 docker image ls
來檢視時,的確又已經看不到 node:18 了:
還記得前面有提到 layer 是共用的嗎?也有提到 node:20 與 node:18 有共用一些 layer,而因為共用的那些 layer 在 node:20 還需要用到,所以在移除 node:18 時,當然就不會移掉。
我們來試著移除剛剛我們做出來的那個 node:20-updated 看看:
移除失敗,根據錯誤訊息可以知道是因為這個 image 正在被 container 422f119c50f6 給參考著,透過 docker container ls -a
檢查看看:
果然,有一個 id 為 422f119c50f6 的 container 是透過這個 image 來建構的,如果這個 container 還在啟動狀態,那就需要先透過 docker container stop CONTAINER_ID
關閉這個 container,如果已經是關閉狀態,那就可以用 docker container rm CONTAINER_ID
來移除這個 container:
這時候就可以用 docker image rm IMAGE_ID
來移除 image:
結語
這次紀錄了 docker 的基本元件與操作,主要討論的是 docker container 的執行與一些基本的操作。也許在做過這先練習與測試之後,你心裡會有一大堆疑問,有的話是很棒的事情,但如一開始所說的,我想先筆記一些基本的操作,先會基本的運用,有點感覺後,然後再慢慢深入討論。
下次仍舊會是基礎篇,讓我們來討論怎麼讓 container 間可以彼此溝通,然後再來討論我最喜歡的 Dockerfile
,這可是我個人認為 docker 可以如此成功的重要因素之一。
系列文
指令整理
這邊把本文討論過的指令都整理起來,方便大家複習與查找,另外也會列出舊版的指令對照,推薦使用新版的指令,雖然較長,但具有一致的結構,非常好學!
# 查看 docker 版本
$ docker --version
$ docker version
# 查看 docker 系統資訊
$ docker info
# 從 node:20 image 啟動一個 docker container 並開啟輸出入
$ docker container run -it node:20 /bin/bash
# 舊版指令
# docker run -it node:20 /bin/bash
# 查看目前正在運行中的 container
$ docker container ls
# 舊版指令
# docker ps
# 查看全部的 container,包括已經停止的。 (a -> all)
$ docker container ls -a
# 舊版指令
# docker ps -a
# 從 DockerHub 上拉下版本為 18 的 node image
$ docker image pull node:18
# 舊版指令
# docker pull node:18
# 查看目前環境中的 docker images
$ docker image ls
# 舊版指令
# docker images
# 用 container id 為 4c9bee8fef4b 的 container
# 建立一個叫做 node:20-updated 的 image
$ docker container commit 4c9bee8fef4b node:20-updated
# 舊版指令
# docker commit 4c9bee8fef4b node:20-updated
# 查看 node:20 這個 image 的歷史
$ docker image history node:20
# 舊版指令
# docker history node:20
# 停止 4c9bee8fef4b 這個 contaienr
$ docker container stop 4c9bee8fef4b
# 舊版指令
# docker stop 4c9bee8fef4b
# 啟動 4c9bee8fef4b 這個 container
$ docker container start 4c9bee8fef4b
# 舊版指令
# docker start 4c9bee8fef4b
# 在 4c9bee8fef4b 這個 container 中執行 /bin/bash 這個命令,並且開啟輸出入
# 因為是執行 /bin/bash 又開啟了輸出入,所以就像是「進入」了這個 container 中
$ docker container exec -it 4c9bee8fef4b /bin/bash
# 舊版指令
# docker exec -it 4c9bee8fef4b /bin/bash
# 移除 4c9bee8fef4b 這個 container
$ docker container rm 4c9bee8fef4b
# docker rm 4c9bee8fef4b
# 移除所有停止的 containers
$ docker container prune -f
# 移除 node:18 這個 image
# 移除 image 前要先移除用這個 image 啟動的 containers
$ docker image rm node:18
# 舊版指令
# docker rmi node:18
推薦閱讀
- [WebConf2023] Docker 入門 101
- Docker Container 基礎入門篇 2
- Docker實戰6堂課:56個實驗動手做,掌握Linux容器核心技術(軟精裝)
- Docker實戰6堂課:56個實驗動手做,掌握Linux容器核心技術(平裝)
透過參加鐵人賽與完成《Docker 實戰 6 堂課》這本書,自己也學到了很多關於 Linux 的知識,設計了很多實驗去驗證自己的理解與觀察,或是從這些實驗的結果去找尋答案。
當初想要寫這樣的內容,是因為自己對 Docker 很有興趣,但市面上已經有很多很棒的 Docker 教學與實戰分享了,我問我自己,我還想讀些什麼呢?我的答案是,我會想要知道更底層的東西,想要知道這些好用的功能背後是怎麼做到的,我也想要更知道 Linux 一些,於是就給自己出了一個這樣的題目,過程中差點後悔,真的是有超過自己原本的能力,但還好沒有放棄。
所以,這的確不是一本初階的書籍,是我嘗試要讓自己更進階一點點的努力,想要跟大家一起變得再厲害一點點的心意,再請大家多多指教了。