給新手的 Docker 網絡入門
還折騰於無法訪問容器內部的服務器嗎?
我遇到了許多第一次接觸 Docker 的新同學,披頭第一個提問就是關於 Docker 網絡的問題。
有某一段時間內,這些類似提問的頻率幾乎已經達到每天都問的程度,所以打算寫一篇入門攻略,提供一些文件幫助新同學們摸索。
符合以下敘述情境的同學,本文可能正好就是你的需求,請務必閱讀全文:
- 遭遇了任何容器間訪問失敗的情況,如:「服務器明明順利啟動了,怎麼都無法經由外部訪問?」
- 從未認真去理解過 Docker 網絡配置,且想要開始去理解。
- 向大老提出以下這個問題:「怎麼從容器內訪問宿主?」,卻遭到關(睥)愛(睨)的眼神。
Warning 給符合第三點的你,你很可能就是 X-Y 問題的製造者,及早發現問題,及早治療,肯定來得及!
前言
閱讀本文之前,你需要:
- 了解
docker run
命令的基本使用方式。 - 擁有以任一語言或應用架設服務器、監聽端口的基本技能。
本文中,你將學會以下 Docker 網絡的概念:
- 五種基本網絡類別及正確姿勢。
- 如何配置官方推薦的用戶自定義網絡(User Defined Network)。
基本網絡類別
以下提及的幾個 Docker 網絡類別,皆可透過 docker run
命令的 --network
選項進行配置。
若你使用 Docker 執行容器以來從未主動配置過網絡的話,那你使用的就是默認網絡配置,也就是 bridge
類型的網絡。
--network="none"
顧名思義,就是沒有網絡。
無法與外部建立連接,也無法經由外部訪問,容器內部彷彿世外桃源,安全性極高。
經由 ifconfig
命令觀察的話可以發現具備 lo
網絡界面,表示可以訪問 localhost
(127.0.0.1
),也就是容器內部自己的服務。
--network="host"
先端上一張概念圖。
這種類型的網絡與宿主機器共用了一個網絡界面,只要在 docker run
中加入這一個選項即可讓容器內外合為一體,不需配置端口映射,是最簡單、迅速解決容器網絡問題的方法,但不推薦用於生產環境及位於公網的機器上,若只是早期開發為了快速測試服務,使用這個類別的網路還沒什麼問題,然而應該盡快採用推薦的用戶自定義網絡,才是真正的解決辦法。
會有這個但書是因為容器內外共用了宿主機器的網絡界面,容器內完全可以自由地、不經授權地操作、獲取、訪問、修改宿主的網絡界面,若一個機器上跑了多個容器,容器間也有端口衝突的問題,因為 host
類別的網絡無法配置端口映射。
這就像命名空間汙染的感覺一樣,或許可以暫時解決了當前的問題,但不是一個嚴謹而優雅的方法。
可以由圖中看到 loopback
(也就是 localhost
、127.0.0.1
)都是與宿主共用的。
--network="container:<container_name|container_id>"
與前一個 host
類別相比,這個網絡類別是和另一個容器共用網絡界面,主容器可透過名稱或 ID 給定。
指定容器名稱的方式很簡單,只要在 docker run
中加入 --name=<容器名稱>
選項即可。至於 ID 是由 Docker 自動分配,可透過 docker ps
命令的第一個欄位觀察到,若沒有透過 --name=<容器名稱>
給定容器名稱,Docker 也會自動分配一個名稱給容器,一樣可以在 docker ps
中觀察。
$ docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f60032684c63 qi “bash” 4 days ago Up 4 days priceless_shirley
讀到這邊各位可能會產生疑惑,那主容器的網絡怎麼配置呢?還是一樣,從這幾個基本的網絡類別裡面去選一個配置啦! 這裡的 container
類別的網絡比較像是分組的概念,但整個分組對外連接或由外部訪問的接口依然要從主容器去做配置。
可以由圖中看到 loopback
是與主容器共用的。
--network="bridge"
bridge
網絡在宿主和容器之間建立了橋接,容器彷彿就像在 NAT 內網下的概念一樣,容器各自都分配到一個內網 IP。在底層其實是一個名為 default
的網絡。
在這個模式下,若要使容器可以經由外部訪問,須配置端口映射,如上圖中,將宿主的 8080
端口映射到了 container1
的 80
端口,所以若要由外部訪問 container1
,則需經由宿主機器域名或 IP 上的 8080
端口訪問。
另外兩個容器 container2
、container3
就沒有辦法經由外部訪問,但容器之間仍可藉由內網 IP 建立連接。
可以由圖中看到容器分別有各自的 loopback
,所以若你是因為服務器內監聽了 127.0.0.1
或 localhost
外部無法訪問而閱讀了本文的人,你可以知道問題所在了吧。監聽容器內的 loopback
只能提供容器內部的訪問,若端口映射都做了,你只需要將監聽的位置改為容器的 IP、0.0.0.0
(監聽所有 IPv4)或是 ::
(監聽所有 IPv6,在啟用 Dual Stack 模式的 OS 下可以連同所有 IPv4 一起監聽)。
--network="<network_name>"
⭐
看到那顆⭐了沒,這代表此題很重要,必考,先給各位高亮了。
這就是用戶自定義網絡,也是官方最推薦的網絡類別。
這種類別的網絡跟 bridge
有幾分神似,然而配置的方式就比 bridge
來得複雜。跟 bridge
在行為上最大的區別為,用戶自定義網絡下可以使用容器名稱、網絡別名(透過 docker run
的 --network-alias
來配置)或是服務名稱(Docker compose 中的 service)當作域名使用、訪問該容器。
如上圖中,container2
要訪問 container3
80
端口上的 HTTP 服務器,則可以使用 http://container3/index.html
這樣的 URL 來訪問。
bridge
類別的網路和用戶自定義網絡其實都是用了 bridge
同名的網絡驅動(Network Driver),所以在架構上看起來幾乎一樣,然而 bridge
類別用的是名為 default
的網絡,而用戶自定義網絡則可以自行命名,並且能夠以容器名稱作為域名訪問指定容器,這是 default
網絡(也就是用了 --network=bridge
的容器)所做不到的。
配置用戶自定義網絡
說了這麼多,讓我們開始學習如何配置用戶自定義網絡。
創建網絡
透過以下指令,我們創建了一個可重複使用的網絡,名為 example
且驅動給定為 bridge
。
$ docker network create — driver bridge example
將容器加入網絡
創建好用戶自定義網絡之後,在執行容器時,我們只需要在 docker run
時使用 --network="<network_name>"
選項將容器加入該網絡即可。
這邊我們跑了一個 httpd
的容器,將其命名為 http-server
並加入 example
網絡中。
$ docker run — network=example — name http-server -d httpd
由於作為範例,這邊並沒有賦予這個容器任何 volume,所以實際上這個容器並沒有任何網頁檔案,我們透過 -d
讓這個容器於背景執行。
可以透過 docker ps
觀察容器是否順利運行。
$ docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f9ccf8284d13 httpd “httpd-foreground” 10 seconds ago Up 7 seconds 80/tcp http-server
驗證行為
然後我們再跑一個容器來驗證用戶自定義網絡。
$ docker run — network=example -it — rm adiazmor/docker-ubuntu-with-ping bash
這邊使用了 adiazmor/docker-ubuntu-with-ping
的鏡像,並執行了 bash
指令讓我們可以輸入命令來測試。選項中的 -it
可以拆開成 -i
和 -t
來看,-i
為了保留 stdin 使我們可以與 bash 互動、輸入命令,-t
配置了一個虛擬的 TTY 進入容器內,--rm
代表若容器停止後會立刻刪除。
進入 bash 後,可以使用 ping -c 4 http-server
驗證了可透過 http-server
訪問網頁服務器的容器。
$ ping -c 4 http-serverPING http-server (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: icmp_seq=0 ttl=64 time=0.104 ms
64 bytes from 172.17.0.2: icmp_seq=1 ttl=64 time=0.073 ms
64 bytes from 172.17.0.2: icmp_seq=2 ttl=64 time=0.049 ms
64 bytes from 172.17.0.2: icmp_seq=3 ttl=64 time=0.048 ms
— — http-server ping statistics — -
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.048/0.068/0.104/0.023 ms
你也可以自行嘗試安裝 wget
後,使用 http://http-server/index.html
訪問服務器。
清理
ctrl
+ C
離開 bash 後,ubuntu 容器便自己悄悄地刪除了。
我們可以優雅地停止並刪除 http-server
容器。
$ docker stop http-server && docker rm http-server
你可以來刺激一點的 rm -f
。
$ docker rm -f http-server
好像還有一個問題還沒解決…?
「怎麼從容器內訪問宿主?」
事出必有因,假設你已經了解了 X-Y 問題了,接著你應該嘗試去理解為何大老們會流露出關(睥)愛(睨)的眼神。
我可以理解,大多數人接觸到 Docker 並不是因為他們真的想讓生產環境與 OS 去耦合,而是因為他們因緣際會下使用了某個工具需要借助 Docker 才能在當前的環境運行,例如在 Linux 環境下運行酷Q,因為酷Q只能跑在 Windows 上,因此在 Linux 下需要借助 wine 跟 Docker 來達成部屬。
然而既然你已經踏入了這個坑了,換個想法,把你的業務代碼也放進 Docker 裡吧。
DockerHub 上就已經有很多資源,支持了很多語言的運行環境,例如:
又或者你可以從 OS 的鏡像起手,如:
另外還可以去了解一下如何使用 Dockerfile 打包你的業務代碼與環境,那就更完美了!
例如在這個例子中,打包了一個 Python 項目,使用的是 Python 2.7 的鏡像,最後提供一句 CMD
給定 docker run
時的默認命令。
小結
簡而言之,「怎麼從容器內訪問宿主?」其實沒有回答的必要。你已經陷入了 X-Y 問題的邏輯中,假定了業務代碼就是跑在宿主機上,然而這並非最好的做法,換個方面思考,大老關(睥)愛(睨)的眼神其實是在暗示你,這個想法已經偏離了正確的姿勢了。
想要進步,就別圖小便宜,永遠追求正確而優雅的姿勢才是邁向大老之路。
作為一個萌新好像太過囂張了,在下牛牛,告辭(閃
謝謝閱讀!
Originally published at cowsay.blog.