在實際應用中,不一樣的服務之間是須要通訊的,例如後端 API 和數據庫;幸運的是,Docker 爲咱們提供了網絡(Network)機制,可以輕鬆實現容器互聯。這篇文章將帶你輕鬆上手 Docker 網絡,學會使用默認網絡和自定義網絡,成爲一名可以鏈接多個「夢境」的築夢師!前端
在Docker一杯茶教程中,咱們帶你瞭解了鏡像和容器這兩大關鍵的概念,熟悉了經常使用的 docker 命令,併成功地容器化了第一個應用。可是,那只是咱們「築夢之旅」的序章。接下來,咱們將實現後端 API 服務器 + 數據庫的容器化。node
若是您以爲咱們寫得還不錯,記得 點贊 + 關注 + 評論 三連,鼓勵咱們寫出更好的教程💪
咱們爲你準備好了應用程序代碼,請運行如下命令:linux
# 若是你看了上一篇教程,倉庫已經克隆下來了 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
和以前容器化前端靜態頁面服務器相比,多了一個難點:服務器和數據庫分別是兩個獨立的容器,可是服務器須要鏈接和訪問數據庫,怎麼實現跨容器之間的通訊?nginx
在《盜夢空間》中,不一樣的夢境之間是沒法鏈接的,然而幸運的是在 Docker 中是能夠的——藉助 Docker Network。git
提示在早期,Docker 容器能夠經過 docker run 命令的
--link
選項來鏈接容器,可是 Docker 官方宣佈這種方式已通過時,並有可能被移除
(參考文檔)。而本文將講解 Docker 官方推薦的方式鏈接容器:自定義網絡(User-defined Networks)。github
Network,顧名思義就是「網絡」,可以讓不一樣的容器之間相互通訊。首先有必要要列舉一下 Docker Network 的五種驅動模式(driver):mongodb
bridge
:默認的驅動模式,即「網橋」,一般用於單機(更準確地說,是單個 Docker 守護進程)overlay
:Overlay 網絡可以鏈接多個 Docker 守護進程,一般用於集羣,後續講 Docker Swarm 的文章會重點講解host
:直接使用主機(也就是運行 Docker 的機器)網絡,僅適用於 Docker 17.06+ 的集羣服務macvlan
:Macvlan 網絡經過爲每一個容器分配一個 MAC 地址,使其可以被顯示爲一臺物理設備,適用於但願直連到物理網絡的應用程序(例如嵌入式系統、物聯網等等)none
:禁用此容器的全部網絡這篇文章將圍繞默認的 Bridge 網絡驅動展開。沒錯,就是鏈接不一樣夢境的那座「橋」。docker
咱們仍是經過一些小實驗來理解和感覺 Bridge Network。與上一節不一樣的是,咱們將使用 Alpine Linux 鏡像做爲實驗原材料,由於:shell
網橋網絡可分爲兩類:數據庫
讓咱們分別實踐一下吧。
這個小實驗的內容以下圖所示:
咱們會在默認的 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
的最後經過 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
選項,就能夠進入容器中交互式運行了。
經過 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 <對象類型> <操做名稱> [其餘選項和參數]
container
、鏡像 image
和網絡 network
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 還處於「裸奔」的狀態(沒設置密碼)。不要方,咱們在接下里的教程中就會去解決哦。
想要學習更多精彩的實戰技術教程?來 圖雀社區逛逛吧。