我踩過的 10 個 Docker 網路坑:從 localhost 到 MTU
容器連不到主機、ufw 明明擋了 port 卻還是對外開放、PPPoE 環境下大封包卡死。這 10 個 Docker 網路問題我全部踩過,附診斷指令與修法。
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日