我們研發開源了一款基於 Git 進行技術實戰教程寫作的工具,我們圖雀社群的所有教程都是用這款工具寫作而成,歡迎 Star 哦
如果你想快速瞭解如何使用,歡迎閱讀我們的 教程文件哦
在實際應用中,不同的服務之間是需要通訊的,例如後端 API 和資料庫;幸運的是,Docker 為我們提供了網路(Network)機制,能夠輕鬆實現容器互聯。這篇文章將帶你輕鬆上手 Docker 網路,學會使用預設網路和自定義網路,成為一名能夠連線多個“夢境”的築夢師!
在上一篇教程中,我們帶你瞭解了映象和容器這兩大關鍵的概念,熟悉了常用的 docker 命令,併成功地容器化了第一個應用。但是,那只是我們“築夢之旅”的序章。接下來,我們將實現後端 API 伺服器 + 資料庫的容器化。
我們為你準備好了應用程式程式碼,請執行以下命令:
# 如果你看了上一篇教程,倉庫已經克隆下來了
cd docker-dream
git fetch origin network-start
git checkout network-start
# 如果你打算直接從這篇教程開始
git clone -b network-start https://github.com/tuture-dev/docker-dream.git
cd docker-dream
和之前容器化前端靜態頁面伺服器相比,多了一個難點:伺服器和資料庫分別是兩個獨立的容器,但是伺服器需要連線和訪問資料庫,怎麼實現跨容器之間的通訊?
在《盜夢空間》中,不同的夢境之間是無法連線的,然而幸運的是在 Docker 中是可以的——藉助 Docker Network。
提示
在早期,Docker 容器可以通過 docker run 命令的
--link
選項來連線容器,但是 Docker 官方宣佈這種方式已經過時,並有可能被移除
(參考文件)。而本文將講解 Docker 官方推薦的方式連線容器:自定義網路(User-defined Networks)。
Network 型別
Network,顧名思義就是“網路”,能夠讓不同的容器之間相互通訊。首先有必要要列舉一下 Docker Network 的五種驅動模式(driver):
bridge
:預設的驅動模式,即“網橋”,通常用於單機(更準確地說,是單個 Docker 守護程式)overlay
:Overlay 網路能夠連線多個 Docker 守護程式,通常用於叢集,後續講 Docker Swarm 的文章會重點講解host
:直接使用主機(也就是執行 Docker 的機器)網路,僅適用於 Docker 17.06+ 的叢集服務macvlan
:Macvlan 網路通過為每個容器分配一個 MAC 地址,使其能夠被顯示為一臺物理裝置,適用於希望直連到物理網路的應用程式(例如嵌入式系統、物聯網等等)none
:禁用此容器的所有網路
這篇文章將圍繞預設的 Bridge 網路驅動展開。沒錯,就是連線不同夢境的那座“橋”。
小試牛刀
我們還是通過一些小實驗來理解和感受 Bridge Network。與上一節不同的是,我們將使用 Alpine Linux 映象作為實驗原材料,因為:
- 非常輕量小巧(整個映象僅 5MB 左右)
- 功能豐富,比“瑞士軍刀” Busybox 還要完善
網橋網路可分為兩類:
- 預設網路(Docker 執行時自帶,不推薦用於生產環境)
- 自定義網路(推薦使用)
讓我們分別實踐一下吧。
預設網路
這個小實驗的內容如下圖所示:
我們會在預設的 bridge
網路上連線兩個容器 alpine1
和 alpine2
。 執行以下命令,檢視當前已有的網路:
docker network ls
應該會看到以下輸出(注意你機器上的 ID 很有可能不一樣):
NETWORK ID NAME DRIVER SCOPE
cb33efa4d163 bridge bridge local
010deedec029 host host local
772a7a450223 none null local
這三個預設網路分別對應上面的 bridge
、host
和 none
網路型別。接下來我們將建立兩個容器,分別名為 alpine1
和 alpine2
,命令如下:
docker run -dit --name alpine1 alpine
docker run -dit --name alpine2 alpine
-dit
是 -d
(後臺模式)、-i
(互動模式)和 -t
(虛擬終端)三個選項的合併。通過這個組合,我們可以讓容器保持在後臺執行而不會退出(沒錯,相當於是在“空轉”)。
用 docker ps
命令確定以上兩個容器均在後臺執行:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
501559d2fab7 alpine "/bin/sh" 2 seconds ago Up 1 second alpine2
18bed3178732 alpine "/bin/sh" 3 seconds ago Up 2 seconds alpine1
通過以下命令檢視預設的 bridge
網路的詳情:
docker network inspect bridge
應該會輸出 JSON 格式的網路詳細資料:
[
{
"Name": "bridge",
"Id": "cb33efa4d163adaa61d6b80c9425979650d27a0974e6d6b5cd89fd743d64a44c",
"Created": "2020-01-08T07:29:11.102566065Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16",
"Gateway": "172.17.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"18bed3178732b5c7a37d7ad820c111fac72a6b0f47844401d60a18690bd37ee5": {
"Name": "alpine1",
"EndpointID": "9c7d8ee9cbd017c6bbdfc023397b23a4ce112e4957a0cfa445fd7f19105cc5a6",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
},
"501559d2fab736812c0cf181ed6a0b2ee43ce8116df9efbb747c8443bc665b03": {
"Name": "alpine2",
"EndpointID": "da192d61e4b2df039023446830bf477cc5a9a026d32938cb4a350a82fea5b163",
"MacAddress": "02:42:ac:11:00:03",
"IPv4Address": "172.17.0.3/16",
"IPv6Address": ""
}
},
"Options": {
"com.docker.network.bridge.default_bridge": "true",
"com.docker.network.bridge.enable_icc": "true",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "docker0",
"com.docker.network.driver.mtu": "1500"
},
"Labels": {}
}
]
我們重點要關注的是兩個欄位:
IPAM
:IP 地址管理資訊(IP Address Management),可以看到閘道器地址為172.17.0.1
(由於篇幅有限,想要了解閘道器的同學可自行查閱計算機網路以及 TCP/IP 協議方面的資料)Containers
:包括此網路上連線的所有容器,可以看到我們剛剛建立的alpine1
和alpine2
,它們的 IP 地址分別為172.17.0.2
和172.17.0.3
(後面的/16
是子網掩碼,暫時不用考慮)
提示
如果你熟悉 Go 模板語法,可以通過
-f
(format
)引數過濾掉不需要的資訊。例如我們只想檢視bridge
的閘道器地址:$ docker network inspect --format '{{json .IPAM.Config }}' bridge [{"Subnet":"172.17.0.0/16","Gateway":"172.17.0.1"}]
讓我們進入 alpine1
容器中:
docker attach alpine1
注意
attach
命令只能進入設定了互動式執行的容器(也就是在啟動時加了-i
引數)。
如果你看到前面的命令提示符變成 / #
,說明我們已經身處容器之中了。我們通過 ping
命令測試一下網路連線情況,首先 ping 一波圖雀社群的主站 tuture.co(-c
引數代表傳送資料包的數量,這裡我們設為 5):
/ # ping -c 5 tuture.co
PING tuture.co (150.109.19.98): 56 data bytes
64 bytes from 150.109.19.98: seq=2 ttl=37 time=65.294 ms
64 bytes from 150.109.19.98: seq=3 ttl=37 time=65.425 ms
64 bytes from 150.109.19.98: seq=4 ttl=37 time=65.332 ms
--- tuture.co ping statistics ---
5 packets transmitted, 3 packets received, 40% packet loss
round-trip min/avg/max = 65.294/65.350/65.425 ms
OK,雖然丟了幾個包,但是可以連上(取決於你的網路環境,全丟包也是正常的)。由此可見,容器內可以訪問主機所連線的全部網路(包括 localhost)。
接下來測試能否連線到 alpine2
,在剛才 docker network inspect
命令的輸出中找到 alpine2
的 IP 為 172.17.0.3
,嘗試能否 ping 通:
/ # ping -c 5 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.147 ms
64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.103 ms
64 bytes from 172.17.0.3: seq=2 ttl=64 time=0.102 ms
64 bytes from 172.17.0.3: seq=3 ttl=64 time=0.125 ms
64 bytes from 172.17.0.3: seq=4 ttl=64 time=0.125 ms
--- 172.17.0.3 ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max = 0.102/0.120/0.147 ms
完美!我們能夠從 alpine1
中訪問 alpine2
容器。作為練習,你可以自己嘗試一下能否從 alpine2
容器中 ping 通 alpine1
哦。
注意
如果你不想讓
alpine1
停下來,記得通過 Ctrl + P + Ctrl + Q(按住 Ctrl,然後依次按 P 和 Q 鍵)“脫離”(detach,也就是剛才attach
命令的反義詞)容器,而不是按 Ctrl + D 哦。
自定義網路
如果你跟著上面一路試下來,會發現預設的 bridge 網路存在一個很大的問題:只能通過 IP 地址相互訪問。這毫無疑問是非常麻煩的,當容器數量很多的時候難以管理,而且每次的 IP 都可能發生變化。
而自定義網路則很好地解決了這一問題。在同一個自定義網路中,每個容器能夠通過彼此的名稱相互通訊,因為 Docker 為我們搞定了 DNS 解析工作,這種機制被稱為服務發現(Service Discovery)。具體而言,我們將建立一個自定義網路 my-net
,並建立 alpine3
和 alpine4
兩個容器,連上 my-net
,如下圖所示。
讓我們開始動手吧。首先建立自定義網路 my-net
:
docker network create my-net
# 由於預設網路驅動為 bridge,因此相當於以下命令
# docker network create --driver bridge my-net
檢視當前所有的網路:
docker network ls
可以看到剛剛建立的 my-net
:
NETWORK ID NAME DRIVER SCOPE
cb33efa4d163 bridge bridge local
010deedec029 host host local
feb13b480be6 my-net bridge local
772a7a450223 none null local
建立兩個新的容器 alpine3
和 alpine4
:
docker run -dit --name alpine3 --network my-net alpine
docker run -dit --name alpine4 --network my-net alpine
可以看到,我們通過 --network
引數指定容器想要連線的網路(也就是剛才建立的 my-net
)。
提示
如果在一開始建立並執行容器時忘記指定網路,那麼下次再想指定網路時,可以通過
docker network connect
命令再次連上(第一個引數是網路名稱my-net
,第二個是需要連線的容器alpine3
):docker network connect my-net alpine3
進入到 alpine3
中,測試能否 ping 通 alpine4
:
$ docker attach alpine3
/ # ping -c 5 alpine4
PING alpine4 (172.19.0.3): 56 data bytes
64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.247 ms
64 bytes from 172.19.0.3: seq=1 ttl=64 time=0.176 ms
64 bytes from 172.19.0.3: seq=2 ttl=64 time=0.180 ms
64 bytes from 172.19.0.3: seq=3 ttl=64 time=0.176 ms
64 bytes from 172.19.0.3: seq=4 ttl=64 time=0.161 ms
--- alpine4 ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max = 0.161/0.188/0.247 ms
可以看到 alpine4
被自動解析成了 172.19.0.3
。我們可以通過 docker network inspect
來驗證一下:
$ docker network inspect --format '{{range .Containers}}{{.Name}}: {{.IPv4Address}} {{end}}' my-net
alpine4: 172.19.0.3/16 alpine3: 172.19.0.2/16
可以看到 alpine4
的 IP 的確為 172.19.0.3
,解析是正確的!
一些收尾工作
實驗做完了,讓我們把之前所有的容器全部銷燬:
docker rm -f alpine1 alpine2 alpine3 alpine4
把建立的 my-net
也刪除:
docker network rm my-net
動手實踐
容器化伺服器
我們首先對後端伺服器也進行容器化。建立 server/Dockerfile
,程式碼如下:
FROM node:10
# 指定工作目錄為 /usr/src/app,接下來的命令全部在這個目錄下操作
WORKDIR /usr/src/app
# 將 package.json 拷貝到工作目錄
COPY package.json .
# 安裝 npm 依賴
RUN npm config set registry https://registry.npm.taobao.org && npm install
# 拷貝原始碼
COPY . .
# 設定環境變數(伺服器的主機 IP 和埠)
ENV MONGO_URI=mongodb://dream-db:27017/todos
ENV HOST=0.0.0.0
ENV PORT=4000
# 開放 4000 埠
EXPOSE 4000
# 設定映象執行命令
CMD [ "node", "index.js" ]
可以看到這個 Dockerfile 比上一篇教程中的要複雜不少。每一行的含義已經註釋在程式碼中了,我們來看一看多了哪些新東西:
RUN
指令用於在容器中執行任何命令,這裡我們通過npm install
安裝所有專案依賴(當然之前配置了一下 npm 映象,可以安裝得快一點)ENV
指令用於向容器中注入環境變數,這裡我們設定了 資料庫的連線字串MONGO_URI
(注意這裡給資料庫取名為dream-db
,後面就會建立這個容器),還配置了伺服器的HOST
和PORT
EXPOSE
指令用於開放埠 4000。之前在用 Nginx 容器化前端專案時沒有指定,是因為 Nginx 基礎映象已經開放了 8080 埠,無需我們設定;而這裡用的 Node 基礎映象則沒有開放,需要我們自己去配置CMD
指令用於指定此容器的啟動命令(也就是docker ps
檢視時的 COMMAND 一列),對於伺服器來說當然就是保持執行狀態。在後面“回憶與昇華”部分會詳細展開。
注意
初次嘗試容器的朋友很容易犯的一個錯誤就是忘記將伺服器的
host
從localhost
(127.0.0.1
)改成0.0.0.0
,導致伺服器無法在容器之外被訪問到(我自己學習的時候也浪費了很多時間)。
與之前前端容器化類似,建立 server/.dockerignore
檔案,忽略伺服器日誌 access.log
和 node_modules
,程式碼如下:
node_modules
access.log
在專案根目錄下執行以下命令,構建伺服器映象,指定名稱為 dream-server
:
docker build -t dream-server server
連線伺服器與資料庫
根據之前的知識,我們為現在的“夢想清單”應用建立一個自定義網路 dream-net
:
docker network create dream-net
我們使用官方的 mongo
映象建立並執行 MongoDB 容器,命令如下:
docker run --name dream-db --network dream-net -d mongo
我們指定容器名稱為 dream-db
(還記得這個名字嗎),所連線的網路為 dream-net
,並且在後臺模式下執行(-d
)。
提示
你也許會問,為什麼這裡開啟容器的時候沒有指定埠對映呢?因為在同一自定義網路中的所有容器會互相暴露所有埠,不同的應用之間可以更輕鬆地相互通訊;同時,除非通過
-p
(--publish
)手動開放埠,網路之外無法訪問網路中容器的其他埠,實現了良好的隔離性。網路之內的互操作性和網路內外的隔離性也是 Docker Network 的一大優勢所在。
危險!
這裡我們在開啟 MongoDB 資料庫容器時沒有設定任何鑑權措施(例如設定使用者名稱和密碼),所有連線資料庫的請求都可以任意修改資料,在生產環境是極其危險的。後續文章中我們會講解如何在容器中管理機密資訊(例如密碼)。
然後執行伺服器容器:
docker run -p 4000:4000 --name dream-api --network dream-net -d dream-server
檢視伺服器容器的日誌輸出,確定 MongoDB 連線成功:
$ docker logs dream-api
Server is running on http://0.0.0.0:4000
Mongoose connected.
接著你可以通過 Postman 或者 curl 來測試一波伺服器 API (localhost:4000
),這裡為了節約篇幅就省略了。當然你也可以直接跳過,因為馬上我們就可以通過前端來運算元據了!
容器化前端頁面
正如上一篇文章所實現的那樣,在專案根目錄下,通過以下命令進行容器化:
docker build -t dream-client client
然後執行容器:
docker run -p 8080:80 --name client -d dream-client
可以通過 docker ps
命令檢驗三個容器是否全部正確開啟:
最後,訪問 localhost:8080
:
可以看到,我們在最後重新整理了幾次頁面,資料記錄也都還在,說明我們帶有資料庫的全棧應用跑起來了!讓我們通過互動式執行的方式進入到資料庫容器 dream-db
中,通過 Mongo Shell 簡單地查詢一波剛才的資料:
$ docker exec -it dream-db mongo
MongoDB shell version v3.4.10
connecting to: mongodb://127.0.0.1:27017
MongoDB server version: 3.4.10
Welcome to the MongoDB shell.
For interactive help, type "help".
> use todos
switched to db todos
> db.getCollection('todos').find()
{ "_id" : ObjectId("5e171fda820251a751aae6f5"), "completed" : true, "text" : "瞭解 Docker Network", "timestamp" : ISODate("2020-01-09T12:43:06.865Z"), "__v" : 0 }
{ "_id" : ObjectId("5e171fe08202517c11aae6f6"), "completed" : true, "text" : "搭建預設網路", "timestamp" : ISODate("2020-01-09T12:43:12.205Z"), "__v" : 0 }
{ "_id" : ObjectId("5e171fe3820251d1a4aae6f7"), "completed" : false, "text" : "搭建自定義網路", "timestamp" : ISODate("2020-01-09T12:43:15.962Z"), "__v" : 0 }
完美!然後按 Ctrl + D 就可以退出來了。
回憶與昇華
理解命令:夢境的主旋律
每個容器自從被建立之時,就註定要執行一道命令(Command),就好像在築夢時要安排一個主旋律、一個基調那樣。之前在執行 docker ps
的時候,你應該也注意到了 COMMAND
一欄,正是每個容器所執行的命令。那麼我們怎麼指定容器的命令呢?又能不能執行新的命令呢?
首先,我們主要通過兩種方式指定容器的命令:
通過 Dockerfile 提供預設命令
在構建映象時,我們可以在 Dockerfile
的最後通過 CMD
指令指定命令,例如在構建後端伺服器時的 [ "node", "server.js" ]
命令。在指定命令時,我們有三種寫法:
CMD ["executable","param1","param2"]
(exec 格式,推薦)CMD ["param1","param2"]
(需要結合 Entrypoint 使用)CMD command param1 param2
(shell 格式)
其中 executable
代表可執行檔案的路徑,例如 node
、/bin/sh
;param1
、param2
代表引數。我們在後續討論 Dockerfile 的高階使用時會討論 Entrypoint 的使用,這篇文章不會涉及。
注意
在使用第一種 exec 格式時,必須使用雙引號,因為整個命令將以 JSON 格式被解析。
提示
如果要執行變數替換等 Shell 操作,例如
echo $HOME
,直接寫成["echo", "$HOME"]
是無效的,需要改寫成["sh", "-c", "echo $HOME"]
。
建立或執行容器時指定命令
在建立或執行容器時,通過新增命令引數可以覆蓋構建映象時指定的命令,例如:
docker run nginx echo hello
通過指定 echo hello
命令引數,就會讓這個容器輸出一個 hello 然後退出,而不會執行預設的 nginx -g 'daemon off;'
。
當然,正如第一篇文章所實踐的,我們還可以指定命令為 bash
(或 sh
、mongo
、node
等其他互動式程式),然後結合 -it
選項,就可以進入容器中互動式執行了。
通過 exec 執行新的命令
通過 docker exec
,我們可以讓已經執行中的容器執行新的命令。例如,對於我們之前的 dream-db
容器,我們通過 mongodump
命令來建立資料庫備份:
docker exec dream-db mongodump
然後可以進一步通過 docker exec -it
來進入 dream-db
中進行互動式執行,檢查剛才匯出的 dump
目錄:
$ docker exec -it dream-db bash
root@c51d9355d8da:/# ls dump/
admin todos
同樣地,按 Ctrl + D 退出就可以了。
提示
你也許會好奇,為什麼在
docker run
互動式執行的時候按 Ctrl + D 就容器就直接停止了,而在docker exec
的情況下退出卻不會導致容器停止呢?因為docker exec -it
相當於在現有的容器上執行了一個新的終端程式,而不會影響之前的主命令程式。只要主程式不結束,容器就不會停止。
小訣竅:如何輕鬆記住幾十個 Docker 命令?
在剛才的實戰中,我們也接觸了很多新的 Docker 命令,怎麼記住那麼多命令呢?其實 docker 大部分命令都符合以下格式:
docker <物件型別> <操作名稱> [其他選項和引數]
- 物件型別:到目前,我們接觸的 Docker 物件型別包括容器
container
、映象image
和網路network
- 操作名稱:操作可以分為兩大類:1)適用於所有物件的操作,例如
ls
、rm
、inspect
和prune
等等;2)物件專屬操作,例如容器專有的run
操作,映象專有的build
操作,以及網路專有的connect
操作等等 - 其他選項和引數:可通過
help
命令或--help
查閱每個命令具體的選項和引數
由於部分命令很常用,Docker 還提供了方便的簡寫命令,例如顯示當前所有容器 docker container ls
,可以簡寫成 docker ps
。
我們首先複習一下容器(Container)物件上的命令吧(紅色代表適用於所有物件的操作,藍色代表此物件的專有操作):
再複習一下映象(Image)物件上的命令:
最後複習一下網路(Network)物件上的命令:
至此,這篇教程也結束了。但是我們的築夢之旅才剛剛開始——還有很多問題沒有解決:1)現在前端應用還無法在除了本地以外的環境使用(因為訪問的後端 API 是硬編碼的 localhost
);2)還沒有真正部署到遠端機器;3)MongoDB 還處於“裸奔”的狀態(沒設定密碼)。不要方,我們在接下里的教程中就會去解決哦。
想要學習更多精彩的實戰技術教程?來圖雀社群逛逛吧。
本作品採用《CC 協議》,轉載必須註明作者和本文連結