DOCKER STUDY NOTE
Docker Container 基礎入門篇 2
不當邊緣人的 container
在上一篇中,我們討論了 container 如何被啟動等基本的指令,以及簡單地介紹了一下 image 等,透過這些討論,我們知道了怎麼 pull image、怎麼啟動、停止一個 container 等。不過,到目前為止我們僅僅停留在單獨一個 container 的運作,今天就讓我們來討論看看 container 如何對外溝通。
既然要溝通,八九不離十就是要講網路了,而我想要分成以下角度來討論:
- Container 跟 Host 之間(Host 是指用來跑 docker 的環境,例如一台 linux 或是 mac 之類的。)
- Container 之間
- 不同 Host 的 container 之間
現在先讓我們來討論比較單純的前兩個:
Container 與 Host 之間
按照慣例(哪來的?),我們先動手做做看,所以讓我們先用以下指令來啟動一個 nginx 的 web server:
$ docker run --name websrv1 -d -p 9090:80 nginx
剛好複習一下,在我們執行了這個指令後,因為本地沒有 nginx
這個 image (Unable to find image ‘nginx:latest’ locally),所以 Docker 會去 Docker Hub 上面 pull 這個 image 下來,也可以看到 nginx
這個 image 有 5 個 layer。
但跟第一篇不太一樣的是,我們加上了 --name
、-d
與-p
這三個參數,少了 -it
與 /bin/bash
,而且指令執行完後並沒有進入 container 中。
--name
顧名思義就是幫我們把這個 container 取個名字,他會顯示在我們執行 docker ps
時顯示的最後一個欄位 NAMES,如果像第一篇那樣沒有指定,docker 會幫你隨機取個名字,做好命名,除了能更好的辨識之外,在操作 container 時,也可以用這個名字來取代 CONTAINER_ID,例如:
# 用 name 取代 CONTAINER_ID
$ docker stop websrv1
再來是 -d
(--detach
),這個設置讓我們在啟動 container 後可以停留在本機中,因為這個參數的意思是讓這個 container 在背景執行,所以執行之後,不會像第一篇那樣進入了一個執行 /bin/bash
的容器之中,如果不相信的話,你可以執行 docker ps
來檢查看看,這個 container 是有被執行起來的。而當然,因為已經背景執行了,我們沒有要跟這個 container 互動,所以不需要 -it
這兩個參數了。
在背景執行下,如果你想要進入這個 container 也還是可以的,就是透過我們第一篇分享的 docker exec
就可以了:
最後一個不一樣的參數是 -p 9090:80
(--publish
),這個參數意思是說我們將我們本機的 9090 port 映射到 container 開出來的 80 port,當然,必須要這個 container 有開放 80 port,這個映射才有意義。
在設置了這個映射之後,如果我們在瀏覽器開啟 http://localhost:9090
,實際上就是把這個 http request 導到了這個 container 中。讓我們試試看:打開瀏覽器,並且輸入 http://localhost:9090
,可以看到成功的開啟了 nginx 預設的首頁。
Container 之間
Docker 官方文件提供一個很不錯的實驗,讓我們也來做做看:
首先我們先用 alpine 這個輕量級的 image 來啟動兩個 container:
$ docker run -dit --name alpine1 alpine ash
$ docker run -dit --name alpine2 alpine ash
由於本機中沒有 alpine 這個 image,所以一樣 docker 會先去 pull 回來,在開始之前我們來看一下他有多輕量:
有沒有很驚人,竟然只有 5.57 MB,相較於 alpine,nginx, node 這些 image 真的是胖嘟嘟…
再往下之前,不知道大家有沒有發現到,這裡在啟動 alpine 時,用的參數是 -dit
,但之前討論過的 -d
是讓 container 在背景執行,那又為什麼需要加上 -it
呢?這不是有需要互動時才需要的嗎?這是因為 alpine 這樣的 image 不像 nginx 一樣,nginx image 在啟動的時候會執行一個能持續運行的程式,例如 nginx 是 nginx -g daemon off;
。而 alpine 則不然,它預設是執行 /bin/sh
(其實是 link 到 /bin/busybox
,也就是 ash),而我們上述的指令,是讓它啟動 ash
,如果沒有加上 -it
,那這個 shell 會被開啟,然後很快地就又結束了,container 也會隨之關閉,所以我們需要加上 -it
來 sh 或其他 shell 可以被分配一個虛擬終端機 (pseudo terminal),以保持 container 的運行。
來個小問題:那可以不可只加上 -t
或是 -i
就好呢?
好,現在讓我們來用 docker inspect
這個指令查看我們所啟動的 container:
# 因為啟動的時候有命名,所以這邊可以直接用 container 的名字,沒有命名的話,可以用 docker 隨機給的,或是用 container id。$ docker inspect alpine1
當 inspect 這個指令下下去,會看到一大堆資訊,有點太多了,我們來選擇一下我們想看的部分:
$ docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' alpine1
172.17.0.2$ docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' alpine2
172.17.0.3
由上述指令可以發現這兩個 container 的 ip 分別為 172.17.0.2 與 172.17.0.3,看起來很像在同一個網路,而且會跟你 Host 的 ip 不同。
我們進入第一個 container alpine1 去測試看看:
$ docker exec -it alpine1 sh/# ping -c 2 172.17.0.3
PING 172.17.0.3 (172.17.0.3): 56 data bytes
64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.083 ms
64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.069 ms
.../ # ping -c 2 www.google.com
PING www.google.com (172.217.25.100): 56 data bytes
64 bytes from 172.217.25.100: seq=0 ttl=103 time=1.651 ms
64 bytes from 172.217.25.100: seq=1 ttl=103 time=1.650 ms
....
透過這個實驗,我們發現在 alpine1 這個 container 中是可以直接 ping 到 alpine2 的,此外,也能 ping 到 google,也就是具有存取外部網路的能力這樣說來,我們在啟動 container 時,根本無須額外設定什麼就能讓 container 之間可以互相溝通跟具有對外溝通的能力。
不過,事情有這麼簡單嗎?
Docker 的網路模型
讓我們來看看 docker 的網路模型,因為目前還是給初學者學習 Docker 用的,所以我們不會講到太難的部分,但可以先有一個概念是 Docker 的網路系統是可插拔的,Docker 安裝完後通常會提供幾個預設的,但如果你想要用第三方開發的來替換掉,也是可以的。
Docker 預設提供的網路驅動:bridge, host, overlay, ipvlan, macvlan。以下就讓我們討論看看 bridge 與 host:
bridge
我們可以透過 docker network ls
這個指令來查看目前的網路有哪些:
這裡可以看到預設有三個網路 bridge, host 與 none,而這三個網路使用的驅動分別是 bridge, host 跟 null。(因為預設的網路 bridge 與 host 會跟他們的 driver 同名,所以討論起來真的是很容易混淆,後續的討論中,要注意在講的是網路還是驅動。)
bridge 網路(驅動是 bridge)是 docker 預設採用的網路,也就是當我們在啟動 container 時,如果沒有指定使用哪一個網路,預設就是用這一個,所以其實我們到目前為止啟動的 container 都是用 bridge 這種網路驅動。
在我們剛剛啟動的兩個 alpine container 的情況下,我們來查看一下預設的這個 network:
# 不管查看 network 還是查看 container,都一致性地用了 inspect,相當地好記
$ docker network inspect bridge
在顯示結果中找到 Containers 這一個區塊:
可以看到我們剛剛建立的兩個 alpine containers 被列在這裡了。此外,也可以看到其 subnet 為 “172.17.0.0/16”,而我們用 bridge 這個網路建立出來的 container 的 ip 也是在這個 subnet 範圍中。
讓我們來檢視一下 container 與 Host 的網路:
在 Host (本機)中的這個 docker0 是在安裝 docker 之後會被建立的一個 bridge,那bridge 與這些虛擬網卡之間怎麼溝通呢?簡單的來說,大概就是下圖:
所以其實就是藉由 docker0 這個 bridge,讓 container 之間能互相溝通,也讓 container 有了對外部網路進行存取的能力。
除了預設的這個 bridge 網路之外,我們還可以自定義網路,且官方建議我們在 production 環境用我們自定義的 bridge 網路,而不是預設的這一個。
現在讓我們來自定義一個 bridge network 試試看:
$ docker network create --driver bridge my-net
7cea7045192b8b6f4720125ecd8f8ff2b67fe98ba7105ff9020b52fcca4922ce$ docker network ls
NETWORK ID NAME DRIVER SCOPE
24ffc871d045 bridge bridge local
97422782804c host host local
7cea7045192b my-net bridge local
8bbb33d110e8 none null local
透過 docker network ls
我們看到有兩個 network 都是 bridge 這種 driver,其中一個是我們剛剛建立的 my-net。讓我們透過 docker network inspect
來查看一下:
$ docker network inspect my-net
[
{
"Name": "my-net",
"Id": "7cea7045192b8b6f4720125ecd8f8ff2b67fe98ba7105ff9020b52fcca4922ce",
"Created": "2020-11-13T11:46:32.438414421Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.19.0.0/16",
"Gateway": "172.19.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]
透過 inspect 可以看到這個我們自定義的 my-net 網路其 subnet 是 172.19.0.0/16(你跟我的可能會不同),所以等一下用我們自已的這個網路啟動的 container 的 ip 應該都會是在這個範圍中。
讓我們停止並移除剛剛建立的兩個 alpine container,重新建立,並且透過 --network
來指令使用我們剛剛建立出來的 my-net:
$ docker run -dit --network my-net --name alpine1 alpine ash
55ac8dd3fc869254a0921f137d5c851d17260660ca7bc4612b60642bfa68816b$ docker run -dit --network my-net --name alpine2 alpine ash
e6001aef8dee1872da2a72d82d0c35dcdb2ffe1687c409cf928ac950c842aa88$ docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' alpine1
172.19.0.2$ docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' alpine2
172.19.0.3
其 ip 果然是落在新的 subnet 中。讓我們進入 alpine1 中去測試看看:
$ docker exec -it alpine1 sh# 對外部
/ # ping -c 2 www.google.com
PING www.google.com (172.217.25.100): 56 data bytes
64 bytes from 172.217.25.100: seq=0 ttl=103 time=1.651 ms
64 bytes from 172.217.25.100: seq=1 ttl=103 time=1.650 ms
...# 對同一個網路底下的 container:
/ # ping -c 2 172.19.0.3
PING 172.19.0.3 (172.19.0.3): 56 data bytes
64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.114 ms
64 bytes from 172.19.0.3: seq=1 ttl=64 time=0.079 ms
...
一切都跟預設的那個 bridge 網路一樣,不過,有一個地方不太一樣,那就是我們自定義的網路可以透過「名字」來進行溝通,這個在預設的 bridge 網路是做不到的喔。
/ # ping -c 2 alpine2
PING alpine2 (172.19.0.3): 56 data bytes
64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.067 ms
64 bytes from 172.19.0.3: seq=1 ttl=64 time=0.079 ms
現在讓我們來啟動第三個 container,但讓它用預設的 bridge 網路:
$ docker run -dit --name alpine3 alpine ash
e4ae856dc2ac904e5b582b5da77fe1ac6eaf5055548e1d504d768fed7fcc30e1$ docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' alpine3
172.17.0.2
可以看到 alpine3 的 ip 是在預設的 bridge 網路的 subnet 中。回到 alpine1 中:
$ docker exec -it alpine1 sh
/ # ping -c 2 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes--- 172.17.0.2 ping statistics ---
2 packets transmitted, 0 packets received, 100% packet loss
果然 ping 不到了!
透過 ip addr
查看 Host 網卡,可以看到除了剛剛的 docker0 外,現在有一個新的 bridge br-7cea7045192b
被建立出來了,這個 bridge 就是用來負責 my-net 這個網路的溝通工作的。
最後,我們再來建立一個用 my-net 的 container,但這次我們在建立後,會把這個 container 連接到預設的那一個 bridge 網路去:
$ docker run -dit --network my-net --name alpine4 alpine ash
d7dc457a87b42676bf368f6e19f2f81c6724258471abb48f29b294456ff6cc02$ docker network connect bridge alpine4
讓我們看看 bridge 與 my-net 這兩個網路的情況:
$ docker network inspect bridge
bridge 這個網路底下果然有兩個 cotainers:
$ docker network inspect my-net
my-net 底下有三個,而且 alpine4 同時出現在 bridge 與 my-net 裡。
讓我們看一下 alpine4 的網路:
$ docker exec -it alpine4 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
20: eth0@if21: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:13:00:04 brd ff:ff:ff:ff:ff:ff
inet 172.19.0.4/16 brd 172.19.255.255 scope global eth0
valid_lft forever preferred_lft forever
22: eth1@if23: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.3/16 brd 172.17.255.255 scope global eth1
valid_lft forever preferred_lft forever
alpine4 有兩張網卡: eth0 其 IP 為 172.19.0.4,是屬於 my-net 的,另外一個 eth1 其 IP 為 172.17.0.3,是屬於預設的 bridge 網路的。
到目前為止,整個網路結構會如下圖:
進入 alpine4 去測試看看:
$ docker exec -it alpine4 sh## 可以用名字來 ping 到 alpine1
/ # ping -c 2 alpine1
PING alpine1 (172.19.0.2): 56 data bytes
64 bytes from 172.19.0.2: seq=0 ttl=64 time=0.067 ms
64 bytes from 172.19.0.2: seq=1 ttl=64 time=0.075 ms--- alpine1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.067/0.071/0.075 ms## 不可以用名字來 ping 到 alpine3,預設的 bridge 網路不能用名字溝通
/ # ping -c 2 alpine3
ping: bad address 'alpine3'## 可以用 ip ping 到 alpine3
/ # ping -c 2 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.110 ms
64 bytes from 172.17.0.2: seq=1 ttl=64 time=0.081 ms--- 172.17.0.2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.081/0.095/0.110 ms
host
這種網路驅動是讓 container 直接採用本機 (host) 網路,也就是不隔離網路了。而在 docker 安裝完成後,也會自動建立一個 host driver 的 host network,這個剛剛我們已經透過 docker network ls
看過了。
那就讓我們來啟動一個 nginx,但指定採用 host 這個 network,來看看會發生什麼事:
# 先確認 host 上 port 80 的使用情況
$ lsof -i -P -n | grep :80# 透過參數 --network 來指定使用何種網路驅動
$ docker run -d --network host nginx
由上圖可以看到,我們在啟動 container 時,透過 --network
參數來指定用 host
網路,這是讓這個 container 直接地使用了 host 的網路,在這個情況下,我們也不需要用 -p
來設定 port 映射,因為當 container 中是要用 80 port 時,他就會直接佔用了我們本機的 80 port。我們也透過了 curl
跟 netstat
來驗證了這件事。
備註1:host 網路驅動僅支援 Linux 作業系統,所以如果你的 host 跟我一樣是 mac 或是 windows,那這個實驗會做不出來喔。
備註2: 如果你用來實驗的 linux 作業系統上 80 port 已經被佔用了的話,這個 container 就會執行失敗,所以實驗前可以先檢查一下。
結語
本篇簡單介紹了兩個網路驅動 bridge 與 host,也展示了如何將 container 中的 port 映射出來,以及 container 間是透過 bridge 來互相溝通與存取外部網路的。Docker 網路的部分還可以有更多、更深入的討論,先讓我們把 docker 基礎的部分都順過一輪,我們再來回頭討論,挫折感比較不會那麼重(?)。
最後要推薦一下 docker 的官方文件,至少在 bridge 與 host 這邊寫得還蠻不錯的,本篇其實也只是記錄了一下我自己做官方文件中提供的實驗的紀錄而已,真心推薦大家親自去看看。
系列文
指令整理
這邊整理本文討論過的指令,方便大家練習與查找:
# 從 image nginx 啟動名稱為 websrv1 的 container,設置為背景執行,並且將 container 的 80 port 映射到本機的 9090
$ docker run --name websrv1 -d -p 9090:80 nginx# 查看名為 alpine1 的 container 的資訊
$ docker inspect alpine1# 查看名為 alpine1 的 container 的資訊,但只看 NetworkSettings 中的 Networks 的 IPAddress
$ docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' alpine1# 查看 docker 目前的網路
$ docker network ls# 查看名為 bridge 這個網路的資訊
$ docker network inspect bridge# 啟動名為 alpine1 的 container,並且其網路設置為 my-net
$ docker run -dit --network my-net --name alpine1 alpine ash# 將 alpine4 這個 container 連結至 bridge 這個網路
$ docker network connect bridge alpine4# 移除 my-net 這個 docker 網路
$ docker network rm my-net