約 5 分鐘

我踩過的 10 個 Docker 網路坑:從 localhost 到 MTU

容器連不到主機、ufw 明明擋了 port 卻還是對外開放、PPPoE 環境下大封包卡死。這 10 個 Docker 網路問題我全部踩過,附診斷指令與修法。

  • #Docker
  • #網路
  • #自架
  • #Linux
  • #除錯

Docker 的網路抽象設計得很好,好到你會以為它不存在。直到某天服務連不上,你花三小時才發現問題出在一個你沒想過的地方。

這篇是我自己踩過的 10 個坑,按「我被卡住的時間」排序。每個都附診斷指令,因為能重現問題比記住答案重要。


坑 1:容器裡的 localhost 不是你的主機

Error: connect ECONNREFUSED 127.0.0.1:5432

容器有自己的 network namespace。容器裡的 127.0.0.1 指的是容器自己,不是你的 Ubuntu 主機。所以主機上跑得好好的 PostgreSQL,容器裡怎麼連都連不到。

修法(依情境選一個):

# 方法 A:用 host network(最快,但失去隔離)
services:
  app:
    network_mode: host

# 方法 B:明確指向主機(Linux 需要 extra_hosts)
services:
  app:
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      DB_HOST: host.docker.internal

方法 B 的 host-gateway 是 Linux 專用的關鍵字。少了它,host.docker.internal 只存在於 Docker Desktop,在 Linux 上不會解析。

但更好的做法是方法 C:把資料庫也放進 compose,用服務名連。


坑 2:容器之間用 IP 連,重啟後就斷

同一個自訂 network 裡,容器可以用服務名互連,Docker 內建 DNS 會處理。用 IP 連的容器,只要對方重建,IP 就會變。

services:
  api:
    environment:
      # ❌ 下次重建就失效
      REDIS_HOST: 172.18.0.4
      # ✅ Docker DNS 會解析
      REDIS_HOST: redis
  redis:
    image: redis:7-alpine

診斷:

docker exec -it api getent hosts redis   # 有解析到就代表 DNS 正常
docker network inspect blog_default | jq '.[0].Containers'

坑 3:ufw 明明沒開 port,服務卻對外開放

這是最危險的一個。你在 ufw 只放行 22/80/443,結果 docker ps 顯示 0.0.0.0:5432->5432/tcp——資料庫對全世界開放。

原因:Docker 直接寫 iptables 的 nat 與 filter 表,走的是 DOCKER 與 DOCKER-USER 鏈,繞過 ufw 的 INPUT 規則。ufw status 看起來一切正常,實際上洞是開的。

修法一:只綁本機

ports:
  - "127.0.0.1:5432:5432"   # 而不是 "5432:5432"

只在同機存取的服務,這一行就能解決 90% 的問題。

修法二:在 DOCKER-USER 鏈擋掉

sudo iptables -I DOCKER-USER -i enp86s0 ! -s 192.168.1.0/24 -j DROP

DOCKER-USER 是唯一保證會在 Docker 規則之前被讀取的鏈。要持久化得寫進 iptables-persistent 或 systemd unit。

驗證(不要相信 ufw status,要從外面掃):

nmap -Pn -p 5432 <你的公網IP>

坑 4:只有「自訂」bridge network 才有 DNS

預設的 docker0 bridge 不提供服務名解析。很多人第一次用 docker run 起兩個容器,想用名字互連,怎麼試都失敗,就是這個原因。

# ❌ 用預設 bridge,沒有 DNS
docker run -d --name a nginx
docker run --rm --link a alpine ping a   # 舊語法,已不建議

# ✅ 建自訂 network
docker network create mynet
docker run -d --name a --network mynet nginx
docker run --rm --network mynet alpine ping a   # 成功

用 compose 的話它會自動建一個 <專案名>_default 的自訂 network,所以你通常不會遇到——直到你手動 docker run 混進來。


坑 5:PPPoE 環境下,大封包直接卡死

這個最隱蔽。小請求正常,登入頁能開,但一上傳檔案或開 HTTPS 大頁面就停住不動。沒有錯誤訊息,就是卡住。

原因:PPPoE 的 MTU 是 1492(不是 1500)。容器預設 MTU 1500,裝了 1500 bytes 的封包出去,路上被丟棄又沒有正確的 ICMP 回報,就會出現「TCP 連上了但資料不動」。

診斷:

# 主機介面 MTU
ip link show ppp0 | grep mtu

# 用不同大小測試(1492 過、1500 不過 → 就是 MTU 問題)
ping -M do -s 1464 -c 2 8.8.8.8
ping -M do -s 1472 -c 2 8.8.8.8

修法(兩個都要做):

# 1. 主機端 clamp MSS(讓對方不要送超過)
sudo iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
  -j TCPMSS --clamp-mss-to-pmtu

# 2. Docker daemon 設 MTU
// /etc/docker/daemon.json
{
  "mtu": 1450
}
sudo systemctl restart docker

MTU 設 1450 而不是 1492,是留給 VPN 等額外封裝的餘裕。我的環境同時跑 WireGuard,1450 是安全值。


坑 6:172.16.0.0/12 跟你的內網打架

Docker 預設從 172.17.0.0/16 開始配發,衝突時往上跳到 172.18、172.19……而 172.16.0.0/12 整個區段都可能被用到。

如果你的公司或家用內網剛好在那個範圍(或你有站對站 VPN 連到用 172.x 的遠端),就會出現「某些 IP 連得到、某些連不到」的詭異現象。

修法:明確指定不會衝突的網段。

networks:
  default:
    ipam:
      config:
        - subnet: 10.88.0.0/16

或全域設定:

// /etc/docker/daemon.json
{
  "default-address-pools": [
    { "base": "10.88.0.0/16", "size": 24 }
  ]
}

坑 7:network_mode: host 之後,ports: 完全失效

services:
  app:
    network_mode: host
    ports:
      - "8080:8080"   # 這行會被忽略,不會報錯

不會有警告,就是靜默失效。用 host network 時,服務直接綁在主機的 port 上,你要改的是應用程式自己的監聽位址,不是 compose 的 ports。

另外:host network 下服務名解析也失效(因為它不在 Docker 的 DNS 裡),要改用 127.0.0.1。


坑 8:compose 專案名不同,network 就對不上

Error: network blog_default declared as external, but could not be found

docker compose 會用資料夾名稱當專案名前綴。你在 ~/blog 跑是 blog_default,在 ~/blog-prod 跑就變成 blog-prod_default——兩個「同一個」服務其實在不同網路上。

修法:明確釘住專案名。

name: blog

或

docker compose -p blog up -d

用 external: true 共用網路時,這一步沒做就一定會出錯。


坑 9:容器重啟後 IP 變了,防火牆規則失效

你寫了 iptables -A DOCKER-USER -s 172.18.0.5 -j ACCEPT,容器一重啟 IP 變成 .6,規則就沒用了。

原則:永遠不要用容器 IP 寫規則。用:

  • 服務名(Docker DNS)
  • 自訂 network 的 subnet(整段放行)
  • 或 --ip 固定 IP(不推薦,容易撞)

坑 10:depends_on 不等於「服務好了」

depends_on:
  - db

這只保證容器啟動順序,不保證資料庫已經能接受連線。應用程式會在 PostgreSQL 還在初始化時就連線,然後 crash。

修法:加 healthcheck。

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 10

  app:
    depends_on:
      db:
        condition: service_healthy

condition: service_healthy 才是「等它真的好了」。


我的除錯順序(三小時縮短到十分鐘)

遇到「連不上」,照這個順序走,不要跳:

1. 服務真的在跑嗎?      docker ps
2. 它 bind 在哪個位址?    docker port <name>
3. 從容器內部連得到嗎?    docker exec -it <name> sh -c "wget -qO- http://localhost:PORT"
4. 從主機連得到嗎?        curl -v http://127.0.0.1:PORT
5. 同網路其他容器連得到?  docker run --rm --network <net> alpine wget -qO- http://<svc>:PORT
6. 外面連得到嗎?          nmap -Pn -p PORT <公網IP>
7. 封包去哪了?            sudo tcpdump -i any -n port PORT

第 3 步跟第 4 步之間就能切開「應用程式問題」與「網路問題」,通常到這裡就結束了。


一句話總結

Docker 網路的坑幾乎都來自同一個誤解:容器不是資料夾,是一台有自己網路 stack 的小機器。

用「另一台機器」的直覺去想它——它有自己的 localhost、自己的 IP、自己的 DNS、自己的 MTU——大部分的困惑就會消失。

最後更新:2026年10月18日