Docker 端口映射做了,局域网却连不上,问题出在监听地址
起因
周末在 NAS 上起了个新的 Web 服务,用容器跑的:
docker run -d --name dashy -p 8080:8000 my-dash-app
docker ps
docker logs dashy --tail 20
容器状态 Up,日志里干干净净没报错,宿主机上 curl http://127.0.0.1:8080 也能正常返回页面。但我转到书房的笔记本,访问 http://192.168.1.20:8080 就是 Connection refused,telnet 端口秒拒。
这种症状挺典型:本机通、别机拒,说明包根本没走到应用层,先别急着怪网络。
看监听地址
第一步永远是确认这个端口到底监听在哪个地址上:
ss -lntp | grep 8080
输出是这样的:
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("docker-proxy",pid=1234,fd=4))
宿主机这一层看着没问题,0.0.0.0:8080 已经在收了。但要注意,这里 LISTEN 的是 docker-proxy,不是真正的服务进程。也就是说,-p 8080:8000 只是把宿主机 8080 转发到容器里的 8000,宿主机这侧的监听没问题,不代表容器里面也没问题。
进容器再看一次:
docker exec -it dashy sh -c 'netstat -lntp || ss -lntp'
结果:
tcp 0 0 127.0.0.1:8000 0.0.0.0:* LISTEN 1/python
根因找到了。应用在容器里只绑了 127.0.0.1,而 docker-proxy 转发过来的目标地址是容器 eth0 的那张网卡 IP(一般是 172.17.x.x),不是 loopback。目标地址不匹配,内核直接回 RST,客户端看到的就是 refused。
改成绑 0.0.0.0 就好了。不同服务改的地方不一样:
# gunicorn / uvicorn
gunicorn -b 0.0.0.0:8000 app:app
uvicorn --host 0.0.0.0 --port 8000 app:app
# Flask 开发服务器
app.run(host="0.0.0.0", port=8000)
# nginx
server { listen 0.0.0.0:8000; ... }
顺带一提,Redis 的 bind 127.0.0.1、MySQL 的 bind-address = 127.0.0.1 都是同一类东西。默认值往往是安全的那一档,方便本地调试,代价就是跨机访问一律连不上。
监听没问题的话,看包到没到
如果 ss 显示已经是 0.0.0.0:8000 了还不通,就该确认数据包到底有没有落到网卡上。在服务端抓:
sudo tcpdump -ni any port 8080
然后从另一台机器 curl 一下,看终端:
- 一条 SYN 都没有 —— 包压根没到,去查路由、交换、防火墙、以及云厂商的安全组;
- 有 SYN 但没有 SYN,ACK —— 到了网卡但没人应答,回头再查一遍应用配置和 backlog;
- 握手正常但立刻 RST —— 应用层 accept 之后主动断了,看应用日志。
我第一次用云服务器部署的时候,iptables 全都放通了还是不通,折腾了半小时才想起控制台里还有一层安全组。现在这个习惯固定下来了:只要换机器部署,先去看控制台的安全组规则。
防火墙别漏掉
本机端口放通是第二道关卡,两台发行版两套命令:
# Debian / Ubuntu
sudo ufw status verbose
sudo ufw allow 8080/tcp
# CentOS / RHEL
sudo firewall-cmd --list-all
sudo firewall-cmd --add-port=8080/tcp --permanent
sudo firewall-cmd --reload
这里有个容易忽略的点:Docker 启动容器时会自己往 iptables 写规则(走的是 FORWARD 链),而 ufw 主要管 INPUT。-p 映射出去的端口有可能绕过 ufw 的规则直接对外可达 —— 你以为被防火墙挡着,其实早就在公网裸奔了。想让两者一致,要么别混用,要么补一条 ufw route allow proto tcp from any to any port 8080,具体得看机器上的实际规则(sudo iptables -S 看全貌最靠谱)。
关于 IPv6 的一个误会
ss 里有时候会看到 *:8080 或者 [::]:8080,不少人以为只监听了 IPv6。实际上 Linux 上绑定 :: 默认是双栈的,IPv4 请求会以 v4-mapped 的形式照样进来,nginx 里写 listen [::]:80; 也是能接 IPv4 的。真正要留意的是反过来:应用显式设了 IPV6_V6ONLY,那才是只认 IPv6。
最后留个清单
这次之后我把排查顺序写在便签上了,下次遇到直接顺着走:
- 目标机
ss -lntp | grep 端口,看是0.0.0.0还是127.0.0.1; - 如果是容器,一定要
docker exec进去再看一遍容器内那个端口; - 从别的机器测,本机 curl 通过不能说明任何问题;
tcpdump确认 SYN 到底有没有到网卡;- 查 iptables/ufw/firewalld,云主机额外查控制台安全组;
- 握手能建立但服务不响应,再看 backlog 和 fd:
ss -tan state syn-recv sport = :8080、ulimit -n。
六个步骤走完,这类"端口在却连不上"基本都能定位。我这次卡了二十分钟,全浪费在第 2 步 —— 一直在宿主机上看那个 docker-proxy,忘了真正的应用在另一层。
- 点赞
- 收藏
- 关注作者
评论(0)