容器化技術 ch07 容器網路:-p 1433:1433 到底做了什麼
下一章→
CH 07 日常必備

容器網路:-p 1433:1433 到底做了什麼

veth pair 與 docker0Port mapping 的真相自訂網路的內建 DNS為什麼要連 sqlserver:1433 而不是 localhost:1433

ch02 說過:每個容器有自己的 network namespace——自己的網卡、自己的 IP、自己完整的一組 port 號碼。這解釋了為什麼兩個容器可以同時用 8080,但也帶來新問題:它被關在自己的網路世界裡,那外面要怎麼連進去?裡面又怎麼連到別人?

這一章回答三個你天天遇到的問題:-p 1433:1433 到底改了什麼、為什麼 Compose 裡的服務可以直接用名字互相連線、以及新手第一名的錯誤——在容器裡把資料庫位址寫成 localhost 然後連不到。

容器有自己的 network namespace,等於一個獨立的網路世界,裡面預設只有一張 loopback(lo)網卡。要能對外通訊,Docker 得幫它接線。

接線的兩個零件

  • veth pair(虛擬網路線):一對永遠成對出現的虛擬網卡,像一條網路線的兩端——從一端送進去的封包,一定從另一端出來。Docker 把一端放進容器的 namespace 當作它的 eth0,另一端留在宿主機。
  • docker0(虛擬網橋):宿主機上一個虛擬的交換器。所有容器留在外面的那一端,全部插在這台交換器上。
   容器 A                容器 B
 ┌─────────┐          ┌─────────┐
 │  eth0   │          │  eth0   │      ← 各自 namespace 裡的網卡
 │172.17.0.2│         │172.17.0.3│
 └────┬────┘          └────┬────┘
      │ veth pair          │ veth pair    ← 虛擬網路線
 ═════╪════════════════════╪══════════  宿主機
      └────────┬───────────┘
          ┌────┴─────┐
          │ docker0  │  172.17.0.1        ← 虛擬交換器(也是容器的預設閘道)
          └────┬─────┘
               │ NAT(偽裝成宿主機 IP)
          ┌────┴─────┐
          │ eth0     │  192.168.1.50      ← 宿主機真實網卡
          └──────────┘ → 外部網路

由這張圖可以直接讀出三件事

  1. 容器之間互通:A 和 B 插在同一台交換器上,彼此的 IP 可以直接連。
  2. 容器可以主動連外:封包經 docker0 到宿主機,做 NAT(網路位址轉換)——把來源位址換成宿主機的 IP 送出去。所以容器連得到網際網路(能 apt-get install),但外面的人不知道它存在。
  3. 外面「不能」主動連進容器:172.17.0.2 是宿主機內部的私有位址,外部網路根本沒有到那裡的路由。這就是為什麼需要 port mapping。
💡 類比:容器們住在一棟公寓(宿主機)裡。docker0 是公寓內部的對講機系統,住戶之間可以互相通話;要打電話出去,統一顯示公寓總機的號碼(NAT)。但外面的人沒辦法直接打進某一戶——除非總機設定了轉接分機,那就是 port mapping。
📌 Windows / macOS 的差異:Docker Desktop 底下跑著一台輕量 Linux VM,上面這整套結構其實在那台 VM 裡。所以你在 Mac 上無法直接 ping 容器的 172.17.x.x(Linux 上可以)。這就是為什麼在這兩個平台上,-p 幾乎是唯一的連線方式。

回到你天天在打的那行設定:

ports:
  - '1433:1433'      # 等同於 docker run -p 1433:1433

它到底做了什麼?

Docker 在宿主機的 iptables(Linux 的封包處理規則表)裡加了一條 DNAT(目的地位址轉換)規則:「凡是送到宿主機 1433 埠的封包,改寄到 172.17.0.2 的 1433 埠。」(並搭配一個在宿主機上監聽該埠的代理程序處理部分情境。)

所以當你用 SSMS 連 localhost:1433 時,封包的旅程是:

SSMS → 宿主機 localhost:1433
     → iptables DNAT 規則改寫目的地
     → docker0 → veth → 容器的 eth0:1433
     → SQL Server 收到

左右順序:一個永遠不會再忘的記法

🚨 -p 宿主機埠:容器埠——左邊是「外面」,右邊是「裡面」。
方向和資料流一致:從外面(左)流向裡面(右)。

範例:-p 14330:1433 表示「你在自己電腦上連 14330,會被轉進容器的 1433」。當你本機已經裝了 SQL Server 佔用 1433 時,就用這招換一個對外的埠——容器裡面完全不用改設定。

四種寫法的差別(含一個資安提醒)

-p 8080:80              # 綁定宿主機所有網路介面 ⚠️ 同網段的人都連得到
-p 127.0.0.1:8080:80    # 只綁 localhost ✅ 只有本機連得到(開發時建議)
-p 80                   # 只寫容器埠 → 宿主機隨機挑一個高位埠
-P                      # 大寫 P:把映像 EXPOSE 的埠全部隨機映射
🚨 很多人不知道的事:-p 5432:5432 預設綁在 0.0.0.0,代表同一個區域網路內的任何人都能連到你的資料庫。而且 Docker 的 iptables 規則可能繞過你設定的 ufw/firewalld 防火牆——你以為防火牆擋住了,其實沒有。開發環境請養成寫 127.0.0.1: 前綴的習慣。

EXPOSE 和 -p 的差別(面試常考)

  • EXPOSE 8080(Dockerfile 裡):純粹是文件,宣告「這個映像的服務在 8080」。它不會打開任何埠,不加 -p 一樣連不進去。
  • -p(執行時):真正建立轉接規則的那個。
  • 唯一的實質作用是搭配 -P(大寫)自動映射,以及讓人知道該開哪個埠。

Docker 有一個預設的 bridge 網路(就是 docker0),但你幾乎永遠不該用它——因為它少了一個關鍵功能。

兩者的決定性差異:內建 DNS

  • 預設 bridge 網路:容器之間只能用 IP 互連,沒有名稱解析。而 IP 是 Docker 動態分配的,容器重啟就可能改變——你的連線字串永遠寫不對。
  • 自訂 bridge 網路:內建 DNS 服務。容器可以直接用容器名稱或服務名稱互相連線,Docker 自動解析成當下正確的 IP。
# 手動建立自訂網路
docker network create my-net
docker run -d --name sqlserver --network my-net mcr.microsoft.com/mssql/server:2022-latest
docker run -d --name app      --network my-net myapp:1.0

# 現在 app 容器裡可以直接:
#   jdbc:sqlserver://sqlserver:1433;databaseName=mydb
#                    ↑ 用名字,不用 IP
好消息:Docker Compose 自動幫你做這件事。你每次 docker compose up,它都會建立一個專屬的自訂網路,並把所有服務加進去。所以 compose 檔裡的服務名稱(sqlserver、redis)天生就是可以直接連的主機名稱——這就是 ch01 說的「compose 悄悄做的六件事」之一。

網路也是隔離邊界

services:
  frontend:
    networks: [front-net]
  backend:
    networks: [front-net, back-net]   # 兩邊都通,當橋樑
  database:
    networks: [back-net]              # 只有 backend 連得到

networks:
  front-net:
  back-net:

不同網路的容器彼此完全看不到。上面這個配置讓資料庫只暴露給後端,前端就算被入侵也碰不到 DB——這是把 AWS 的公有/私有子網概念(AWS ch06)縮小到單機上實作。

其他網路模式(知道有就好)

  • --network host:不建立 network namespace,直接用宿主機的網路。容器監聽 8080 就是宿主機的 8080,不需要 -p。優點是沒有 NAT 開銷(效能敏感時用),缺點是完全沒有網路隔離、埠會衝突。注意:在 Docker Desktop(Mac/Windows)上行為和 Linux 不同,別依賴它。
  • --network none:只有 loopback,完全不連外。適合跑純計算、不需要網路的批次任務(安全性最高)。
  • overlay:跨多台主機的網路,讓不同機器上的容器像在同一個區網。Swarm 和 K8s 的多節點通訊基礎(ch14)。

這是容器新手第一名的錯誤,幾乎每個人都踩過一次。

錯誤現場

# docker-compose.yml
services:
  sqlserver:
    image: mcr.microsoft.com/mssql/server:2022-latest
    ports: ['1433:1433']
  app:
    image: myapp:1.0
    environment:
      # ❌ 錯誤:從容器內看,localhost 是「這個容器自己」
      SPRING_DATASOURCE_URL: jdbc:sqlserver://localhost:1433;databaseName=mydb

啟動後 app 立刻噴 Connection refused。

為什麼?回到 ch02 的 network namespace

localhost 在容器裡指的是「這個容器自己的 network namespace」,不是你的電腦、也不是別的容器。app 容器對自己的 1433 埠喊話,而它自己根本沒有跑資料庫——當然被拒絕。

每個容器都有自己的 localhost。這句話請記牢。

正解:用服務名稱 + 容器內部的埠

      # ✅ 正確
      SPRING_DATASOURCE_URL: jdbc:sqlserver://sqlserver:1433;databaseName=mydb
      #                                       ↑服務名   ↑容器內部埠

第二個容易混淆的點:該用哪個埠號?

假設你為了避開本機衝突改成 ports: ['14330:1433']:

  • 你在自己電腦上用 SSMS 連 → localhost:14330(走 port mapping 的外部埠)
  • app 容器連資料庫 → sqlserver:1433(走內部網路,完全不經過 port mapping,所以用容器內部的原始埠)
⭐ 一句話總結:容器對容器走「服務名 + 內部埠」;你的電腦對容器走「localhost + 映射埠」。兩條路徑完全獨立——其實容器之間互連根本不需要 -p,你寫 -p 純粹是為了讓自己(宿主機)能連進去。

相關情境三:容器要連宿主機上的服務

如果你的資料庫是直接裝在自己電腦上(不是容器),容器裡要怎麼連?

  • 用特殊名稱 host.docker.internal(Docker Desktop 內建;Linux 上需要加 --add-host=host.docker.internal:host-gateway)。
  • jdbc:sqlserver://host.docker.internal:1433;...
💡 三種位址的速查:
• 連同一個 compose 裡的其他服務 → 服務名稱
• 連宿主機上的服務 → host.docker.internal
• 連容器自己 → localhost(這正是它唯一正確的用法)

網路問題最怕亂猜。照這個順序查,幾乎都能在五分鐘內定位。

第 1 步:容器真的活著嗎?

docker compose ps        # STATUS 是 Up 還是 Exited/Restarting?
docker compose logs sqlserver --tail 50

最常見的真相:資料庫容器根本沒起來(密碼太弱被 SQL Server 拒絕、記憶體不足、EULA 沒接受)。連線失敗只是症狀,不是病因。

第 2 步:它們在同一個網路裡嗎?

docker network ls
docker network inspect <網路名>    # Containers 區段列出成員

不同 compose 專案的服務預設在不同網路,彼此看不到——這是跨專案連線失敗的頭號原因。

第 3 步:名稱解析得到嗎?

docker compose exec app ping -c 2 sqlserver
# 或(精簡映像沒有 ping 時)
docker compose exec app getent hosts sqlserver
  • 解析不到 → 網路或服務名稱有問題(回第 2 步)。
  • 解析得到但連不上 → 往下一步。

第 4 步:那個埠真的有人在聽嗎?

docker compose exec sqlserver ss -lntp        # 看容器內監聽的埠
# 或從 app 容器測試連通性
docker compose exec app nc -zv sqlserver 1433
🚨 一個非常隱蔽的坑:有些服務預設只監聽 127.0.0.1(只接受本機連線)。在容器裡這代表只有它自己連得到,別的容器一律拒絕。容器化的服務必須監聽 0.0.0.0(所有介面)。如果你看到 ss 輸出是 127.0.0.1:8080 而不是 0.0.0.0:8080 或 *:8080,這就是原因。

Spring Boot 預設監聽所有介面沒問題;但你若在設定裡寫了 server.address=127.0.0.1 就會中招。

第 5 步:是不是啟動順序問題?

  • 症狀:第一次啟動失敗,手動重啟 app 就好了。
  • 原因:depends_on 只保證「容器被建立」,不保證「服務已經可以接受連線」。SQL Server 從容器啟動到能接受連線要好幾十秒,你的 app 早就衝過去連了。
  • 這是 Compose 的頭號大坑,ch11 會完整拆解與解法(healthcheck + condition: service_healthy,以及應用層的連線重試)。

好用的除錯工具容器

# 精簡映像裡沒有網路工具時,開一個工具容器加入同一個網路
docker run --rm -it --network <網路名> nicolaka/netshoot bash
# 裡面有 ping / dig / curl / ss / tcpdump / nmap 一應俱全
三種位址:什麼時候用哪個
情境用什麼位址用哪個埠為什麼
容器 A 連容器 B(同網路)B 的服務名稱B 的容器內部埠走自訂網路的內建 DNS,完全不經過 port mapping
你的電腦連容器localhost-p 左邊那個宿主機埠走 iptables DNAT 轉接規則
容器連宿主機上的服務host.docker.internal宿主機上該服務的埠Docker 提供的特殊名稱(Linux 需 --add-host)
容器連自己localhost自己監聽的埠每個容器有自己的 network namespace 與 localhost
網路模式對照
模式隔離程度怎麼被連到適用場景
預設 bridge有 namespace 隔離需要 -p;容器間只能用 IP幾乎不該用(沒有 DNS)
自訂 bridge有隔離,且可切多個網段需要 -p;容器間可用名稱標準做法(Compose 自動建立)
host無(直接用宿主機網路)不需 -p,直接就是宿主機的埠極致效能需求;會有埠衝突風險
none最高(只有 loopback)連不到,也連不出去純計算批次任務
overlay跨主機的虛擬網段跨機器的容器可直接互連Swarm / K8s 多節點(ch14)
連線失敗排查對照表
症狀最可能原因怎麼確認
Connection refused(容器間)位址寫成 localhost,應該用服務名稱檢查連線字串;ping 服務名試試
Unknown host不在同一個網路(常見於跨 compose 專案)docker network inspect 看成員清單
第一次啟動失敗,重啟就好depends_on 只等容器建立,不等服務就緒看 app 的失敗時間點是否早於 DB 就緒(ch11)
解析得到但連不上服務只監聽 127.0.0.1,或根本沒起來docker exec ... ss -lntp 看監聽位址
宿主機連不進去忘了 -p,或映射的埠寫反了docker ps 的 PORTS 欄位
同事連得到你的 DB-p 預設綁 0.0.0.0,整個區網都連得到改成 -p 127.0.0.1:1433:1433

練習題 點選選項查看解析

0 / 9
01 / 9
docker run -p 8080:80 中,8080 和 80 分別代表什麼?
A 8080 是容器埠,80 是宿主機埠
B 8080 是宿主機埠,80 是容器埠——左邊是外面、右邊是裡面,方向跟資料流一致
C 兩者都是容器埠
D 8080 是內部埠,80 是外部轉發埠
解析
記法:左外右內,從外面(左)流向裡面(右)。所以 -p 14330:1433 表示你在自己電腦連 14330 會被轉進容器的 1433——當本機已有服務佔用 1433 時就這樣避開,而容器裡完全不用改設定。
02 / 9
-p 1433:1433 這個參數在底層實際做了什麼?
A 在容器內開啟了一個新的網路介面
B 在宿主機的 iptables 加一條 DNAT 規則:送到宿主機 1433 埠的封包,改寄到容器 IP 的 1433 埠
C 修改了容器的防火牆設定
D 建立了一條 VPN 通道
解析
容器的 172.17.x.x 是宿主機內部的私有位址,外部網路沒有到那裡的路由,所以必須靠 DNAT 轉接。順帶一提,Docker 寫的 iptables 規則可能繞過你設定的 ufw/firewalld——你以為防火牆擋住了,其實沒有。
03 / 9
compose 裡 app 服務要連資料庫,SPRING_DATASOURCE_URL 應該怎麼寫?
A jdbc:sqlserver://localhost:1433;databaseName=mydb
B jdbc:sqlserver://sqlserver:1433;databaseName=mydb(服務名稱 + 容器內部埠)
C jdbc:sqlserver://172.17.0.1:1433;databaseName=mydb
D jdbc:sqlserver://host.docker.internal:1433;databaseName=mydb
解析
容器新手第一名的錯誤就是寫 localhost。每個容器有自己的 network namespace,localhost 指的是「這個容器自己」——app 容器自己沒跑資料庫,當然 Connection refused。正解是用服務名稱(Compose 自訂網路的內建 DNS 會解析它),且用容器內部埠,因為容器間通訊完全不經過 port mapping。
04 / 9
compose 設定為 ports: ['14330:1433']。你用 SSMS 連、以及 app 容器連,分別該用什麼?
A 兩者都用 localhost:14330
B SSMS 用 localhost:14330(走 port mapping);app 容器用 sqlserver:1433(走內部網路,用原始內部埠)
C 兩者都用 sqlserver:1433
D SSMS 用 sqlserver:14330;app 用 localhost:1433
解析
兩條路徑完全獨立:你的電腦→容器走 DNAT 轉接規則(所以用映射後的 14330);容器→容器走內部網路(所以用原始的 1433)。推論:容器之間互連根本不需要 -p,你寫 -p 純粹是為了讓自己連得進去。
05 / 9
為什麼應該用自訂網路而不是預設的 bridge 網路?
A 自訂網路速度較快
B 自訂網路內建 DNS,容器之間可以用名稱互連;預設 bridge 只能用 IP,而 IP 會隨容器重啟改變
C 預設網路不支援對外連線
D 自訂網路可以使用更多埠
解析
名稱解析是決定性差異——沒有它,你的連線字串永遠寫不對,因為 IP 是動態分配的。好消息是 Docker Compose 每次都會自動建立一個專屬的自訂網路並把服務加進去,所以服務名稱天生就是可用的主機名稱。
06 / 9
Dockerfile 裡的 EXPOSE 8080 有什麼實際作用?
A 自動把宿主機的 8080 對應到容器的 8080
B 幾乎只是文件用途:宣告這個映像的服務在 8080,不會真的打開任何埠——沒有 -p 一樣連不進去
C 限制容器只能使用 8080 埠
D 在容器內啟動 8080 的監聽
解析
EXPOSE 是給人看的宣告,真正建立轉接規則的是執行時的 -p。它唯一的實質作用是搭配 -P(大寫)自動隨機映射所有 EXPOSE 的埠。這是面試常考的觀念題。
07 / 9
服務在容器裡看似正常啟動,但其他容器就是連不上。用 ss -lntp 看到它監聽在 127.0.0.1:8080。問題是?
A 埠號設定錯誤
B 服務只監聽 loopback,代表只有容器自己連得到;容器化的服務必須監聽 0.0.0.0(所有介面)
C 缺少 -p 參數
D DNS 解析失敗
解析
這是很隱蔽的坑:服務確實在跑、埠也對,但綁定位址錯了。容器內的 127.0.0.1 只服務它自己的 network namespace。Spring Boot 預設監聽所有介面沒問題,但如果設定了 server.address=127.0.0.1 就會中招。
08 / 9
你的 app 第一次 compose up 時連不上資料庫,但手動重啟 app 就正常了。最可能的原因是?
A 網路設定錯誤
B depends_on 只保證容器被建立,不保證服務已能接受連線——SQL Server 要數十秒才就緒,app 早就衝過去連了
C 資料庫密碼錯誤
D port mapping 沒生效
解析
「第一次失敗、重啟就好」是啟動順序問題的典型特徵。這是 Compose 的頭號大坑,ch11 會完整拆解:正解是 healthcheck 搭配 condition: service_healthy,再加上應用層自己的連線重試(因為正式環境沒有 compose 幫你排順序)。
09 / 9
在開發機上用 -p 5432:5432 開放資料庫有什麼風險?
A 沒有風險
B 預設綁定 0.0.0.0,代表同一個區域網路內的任何人都連得到你的資料庫;而且 Docker 的 iptables 規則可能繞過 ufw/firewalld
C 會導致效能下降
D 會佔用太多記憶體
解析
很多人以為 -p 只是「本機能連」,其實是對所有網路介面開放。正解是加上位址前綴:-p 127.0.0.1:5432:5432,只綁 localhost。這在咖啡廳、公司共用網路上尤其重要。

點擊卡片翻面查看答案,共 11 張。

QUESTION
容器怎麼接上網路?兩個零件?
點擊翻面
ANSWER
veth pair:一對虛擬網卡(像網路線兩端),一端當容器的 eth0
docker0:宿主機上的虛擬交換器,所有容器另一端都插在上面
對外經 NAT 偽裝成宿主機 IP
點擊翻回
QUESTION
-p 8080:80 的左右順序怎麼記?
點擊翻面
ANSWER
左外右內:宿主機埠:容器埠
方向跟資料流一致(從外面流向裡面)
-p 14330:1433 = 你連 14330 → 進容器 1433
點擊翻回
QUESTION
-p 底層做了什麼?
點擊翻面
ANSWER
在宿主機 iptables 加一條 DNAT 規則
「送到宿主機 1433 的封包 → 改寄到容器 IP 的 1433」
(容器私有 IP 外部無路由,所以必須轉接)
點擊翻回
QUESTION
-p 的四種寫法?
點擊翻面
ANSWER
-p 8080:80 → 綁 0.0.0.0 ⚠️ 全區網可連
-p 127.0.0.1:8080:80 → 只綁本機 ✅ 開發建議
-p 80 → 宿主機隨機挑埠
-P → 把 EXPOSE 的埠全部隨機映射
點擊翻回
QUESTION
EXPOSE vs -p?
點擊翻面
ANSWER
EXPOSE:Dockerfile 裡的「文件宣告」,不會打開任何埠
-p:執行時真正建立 DNAT 轉接規則的那個
沒有 -p 就是連不進去
點擊翻回
QUESTION
自訂網路比預設 bridge 好在哪?
點擊翻面
ANSWER
內建 DNS → 容器之間可用「名稱」互連
預設 bridge 只能用 IP,而 IP 重啟會變
Compose 每次都自動建自訂網路(所以服務名可直接連)
點擊翻回
QUESTION
三種位址速查?
點擊翻面
ANSWER
連同 compose 的其他服務 → 服務名稱 + 內部埠
連宿主機上的服務 → host.docker.internal
連容器自己 → localhost(唯一正確的用法)
點擊翻回
QUESTION
為什麼容器裡不能把 DB 寫成 localhost?
點擊翻面
ANSWER
每個容器有自己的 network namespace
localhost = 這個容器自己,不是你的電腦也不是別的容器
app 自己沒跑 DB → Connection refused
點擊翻回
QUESTION
連不到的五步排查?
點擊翻面
ANSWER
① 容器活著嗎(logs 常見真相:DB 根本沒起來)
② 在同一個網路嗎(network inspect)
③ 名稱解析得到嗎(ping / getent hosts)
④ 埠真的有人聽嗎(ss -lntp,注意 127.0.0.1 vs 0.0.0.0)
⑤ 是不是啟動順序問題(depends_on 不等就緒)
點擊翻回
QUESTION
容器化服務必須監聽哪個位址?
點擊翻面
ANSWER
0.0.0.0(所有介面)
監聽 127.0.0.1 → 只有容器自己連得到,別的容器一律拒絕
(Spring Boot 若設了 server.address=127.0.0.1 就中招)
點擊翻回
QUESTION
host / none / overlay 模式各是什麼?
點擊翻面
ANSWER
host:不建 namespace,直接用宿主機網路(無隔離、埠會衝突)
none:只有 loopback,完全不連外(純計算任務)
overlay:跨主機虛擬網段(Swarm / K8s 多節點)
點擊翻回