還折騰於無法訪問容器內部的服務器嗎?

我遇到了許多第一次接觸 Docker 的新同學,披頭第一個提問就是關於 Docker 網絡的問題。

有某一段時間內,這些類似提問的頻率幾乎已經達到每天都問的程度,所以打算寫一篇入門攻略,提供一些文件幫助新同學們摸索。

符合以下敘述情境的同學,本文可能正好就是你的需求,請務必閱讀全文:

  1. 遭遇了任何容器間訪問失敗的情況,如:「服務器明明順利啟動了,怎麼都無法經由外部訪問?」
  2. 從未認真去理解過 Docker 網絡配置,且想要開始去理解。
  3. 向大老提出以下這個問題:「怎麼從容器內訪問宿主?」,卻遭到睥睨關愛的眼神。

Warning 給符合第三點的你,你很可能就是 X-Y 問題的製造者,及早發現問題,及早治療,肯定來得及!


前言

閱讀本文之前,你需要:

  1. 了解 docker run 命令的基本使用方式[1]
  2. 擁有以任一語言應用架設服務器、監聽端口的基本技能。

本文中,你將學會以下 Docker 網絡的概念:

  1. 五種基本網絡類別及正確姿勢。
  2. 如何配置官方推薦的用戶自定義網絡(User Defined Network)[2]

基本網絡類別

以下提及的幾個 Docker 網絡類別,皆可透過 docker run 命令的 --network 選項進行配置[3]

若你使用 Docker 執行容器以來從未主動配置過網絡的話,那你使用的就是默認網絡配置,也就是 bridge 類型的網絡。

--network="none"

顧名思義,就是沒有網絡。

無法與外部建立連接,也無法經由外部訪問,容器內部彷彿世外桃源,安全性極高。

經由 ifconfig 命令觀察的話可以發現具備 lo 網絡界面,表示可以訪問 localhost127.0.0.1),也就是容器內部自己的服務。

--network="host"

先端上一張概念圖。

這種類型的網絡與宿主機器共用了一個網絡界面,只要在 docker run 中加入這一個選項即可讓容器內外合為一體,不需配置端口映射,是最簡單、迅速解決容器網絡問題的方法,但不推薦用於生產環境及位於公網的機器上,若只是早期開發為了快速測試服務,使用這個類別的網路還沒什麼問題,然而應該盡快採用推薦的用戶自定義網絡,才是真正的解決辦法。

會有這個但書是因為容器內外共用了宿主機器的網絡界面,容器內完全可以自由地、不經授權地操作、獲取、訪問、修改宿主的網絡界面,若一個機器上跑了多個容器,容器間也有端口衝突的問題,因為 host 類別的網絡無法配置端口映射。

這就像命名空間汙染的感覺一樣,或許可以暫時解決了當前的問題,但不是一個嚴謹而優雅的方法。

可以由圖中看到 loopback (也就是 localhost127.0.0.1)都是與宿主共用的。

--network="container:<container_name|container_id>"

與前一個 host 類別相比,這個網絡類別是和另一個容器共用網絡界面,主容器可透過名稱或 ID 給定。

指定容器名稱的方式很簡單,只要在 docker run 中加入 --name=<容器名稱> 選項即可。至於 ID 是由 Docker 自動分配,可透過 docker ps 命令的第一個欄位觀察到,若沒有透過 --name=<容器名稱> 給定容器名稱,Docker 也會自動分配一個名稱給容器,一樣可以在 docker ps 中觀察。

1
2
3
4
$ docker ps

CONTAINER 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 的網絡。

在這個模式下,若要使容器可以經由外部訪問,須配置端口映射[4],如上圖中,將宿主的 8080 端口映射到了 container180 端口,所以若要由外部訪問 container1,則需經由宿主機器域名或 IP 上的 8080 端口訪問。

另外兩個容器 container2container3 就沒有辦法經由外部訪問,但容器之間仍可藉由內網 IP 建立連接。

可以由圖中看到容器分別有各自的 loopback,所以若你是因為服務器內監聽了 127.0.0.1localhost 外部無法訪問而閱讀了本文的人,你可以知道問題所在了吧。監聽容器內的 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)[5]當作域名使用、訪問該容器。

如上圖中,container2 要訪問 container3 80 端口上的 HTTP 服務器,則可以使用 http://container3/index.html 這樣的 URL 來訪問。

bridge 類別的網路和用戶自定義網絡其實都是用了 bridge 同名的網絡驅動(Network Driver)[6],所以在架構上看起來幾乎一樣,然而 bridge 類別用的是名為 default 的網絡,而用戶自定義網絡則可以自行命名,並且能夠以容器名稱作為域名訪問指定容器,這是 default 網絡(也就是用了 --network=bridge 的容器)所做不到的。

配置用戶自定義網絡

說了這麼多,讓我們開始學習如何配置用戶自定義網絡。

創建網絡

透過以下指令,我們創建了一個可重複使用的網絡,名為 example 且驅動給定為 bridge

1
$ docker network create --driver bridge example

將容器加入網絡

創建好用戶自定義網絡之後,在執行容器時,我們只需要在 docker run 時使用 --network="<network_name>" 選項將容器加入該網絡即可。

這邊我們跑了一個 httpd 的容器[7],將其命名為 http-server 並加入 example 網絡中。

1
$ docker run --network=example --name http-server -d httpd

由於作為範例,這邊並沒有賦予這個容器任何 volume,所以實際上這個容器並沒有任何網頁檔案,我們透過 -d 讓這個容器於背景執行。

可以透過 docker ps 觀察容器是否順利運行。

1
2
3
4
$ docker ps

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f9ccf8284d13 httpd "httpd-foreground" 10 seconds ago Up 7 seconds 80/tcp http-server

驗證行為

然後我們再跑一個容器來驗證用戶自定義網絡。

1
$ docker run --network=example -it --rm adiazmor/docker-ubuntu-with-ping bash

這邊使用了 adiazmor/docker-ubuntu-with-ping 的鏡像[8],並執行了 bash 指令讓我們可以輸入命令來測試。選項中的 -it 可以拆開成 -i-t 來看,-i 為了保留 stdin 使我們可以與 bash 互動、輸入命令,-t 配置了一個虛擬的 TTY 進入容器內,--rm 代表若容器停止後會立刻刪除。

進入 bash 後,可以使用 ping -c 4 http-server 驗證了可透過 http-server 訪問網頁服務器的容器。

1
2
3
4
5
6
7
8
9
10
$ ping -c 4 http-server

PING 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 容器。

1
$ docker stop http-server && docker rm http-server

你可以來刺激一點的 rm -f

1
$ docker rm -f http-server

好像還有一個問題還沒解決...?

「怎麼從容器內訪問宿主?」

事出必有因,假設你已經了解了 X-Y 問題了,接著你應該嘗試去理解為何大老們會流露出睥睨關愛的眼神。

我可以理解,大多數人接觸到 Docker 並不是因為他們真的想讓生產環境與 OS 去耦合,而是因為他們因緣際會下使用了某個工具需要借助 Docker 才能在當前的環境運行,例如在 Linux 環境下運行酷Q,因為酷Q只能跑在 Windows 上,因此在 Linux 下需要借助 wine 跟 Docker 來達成部屬。

然而既然你已經踏入了這個坑了,換個想法,把你的業務代碼也放進 Docker 裡吧。

DockerHub 上就已經有很多資源,支持了很多語言的運行環境,例如:

  1. Python

    https://hub.docker.com/_/python

  2. Golang

    https://hub.docker.com/_/golang

  3. Node

    https://hub.docker.com/_/node

  4. PHP

    https://hub.docker.com/_/php

又或者你可以從 OS 的鏡像起手,如:

  1. Ubuntu

    https://hub.docker.com/_/ubuntu

  2. Alpine

    https://hub.docker.com/_/alpine

另外還可以去了解一下如何使用 Dockerfile 打包你的業務代碼與環境,那就更完美了!

例如在這個例子中,打包了一個 Python 項目,使用的是 Python 2.7 的鏡像,最後提供一句 CMD 給定 docker run 時的默認命令。


小結

簡而言之,「怎麼從容器內訪問宿主?」其實沒有回答的必要。你已經陷入了 X-Y 問題的邏輯中,假定了業務代碼就是跑在宿主機上,然而這並非最好的做法,換個方面思考,大老睥睨關愛的眼神其實是在暗示你,這個想法已經偏離了正確的姿勢了。

想要進步,就別圖小便宜,永遠追求正確而優雅的姿勢才是邁向大老之路。

作為一個萌新好像太過囂張了,在下牛牛,告辭(閃


謝謝閱讀!



  1. 官方文件「Docker run」:https://docs.docker.com/engine/reference/run ↩︎

  2. 官方文件「用戶自定義網絡」:https://docs.docker.com/v17.09/engine/userguide/networking/#user-defined-networks ↩︎

  3. 官方文件「Docker run」中關於網絡配置的部分:https://docs.docker.com/engine/reference/run/#network-settings ↩︎

  4. 官方文件「端口映射」:https://docs.docker.com/engine/reference/commandline/run/#publish-or-expose-port--p---expose ↩︎

  5. 官方文件「Docker Compose」:https://docs.docker.com/compose/compose-file/#service-configuration-reference ↩︎

  6. 官方文件「網絡驅動」:https://docs.docker.com/network/#network-drivers ↩︎

  7. DockerHub「httpd」:https://hub.docker.com/_/httpd ↩︎

  8. DockerHub「adiazmor/docker-ubuntu-with-ping」:https://hub.docker.com/r/adiazmor/docker-ubuntu-with-ping/ ↩︎