容器之间怎么找到对方
一句话总结
网络命名空间不仅会复制接口和路由表,还会把回环地址和端口号空间完整复制一份。因此,容器的 127.0.0.1 与主机的 127.0.0.1 是完全不同的地址。
为什么需要它
初学者经常遇到的三种症状,都可以用上面这句话解释。
- 浏览器提示连接被拒绝
- 相邻容器执行
curl http://api:3000时无法解析名称 - 容器执行
curl http://127.0.0.1:5432时无法连接主机数据库
第三种尤其容易令人困惑。在容器内连接 127.0.0.1,指向的是该容器自身,而不是主机。
工作原理
在默认桥接网络中,容器无法通过容器名称找到彼此。
docker run -d --name db postgres:16
docker run --rm alpine:3.20 ping -c1 db
# ping: bad address 'db'
创建自定义网络并将容器接入即可解决。差异会直接反映在容器内的 /etc/resolv.conf 中:名称服务器被设为一个仅存在于容器命名空间中的地址,请求再转交给运行时的解析器。解析器知道同一网络内容器的名称和别名,因此能够响应。默认桥接网络只是为了向后兼容而保留的;实际工作中始终应创建自定义网络。
第二个重点是应用程序的绑定地址。
docker exec api ss -tlnp
# LISTEN 0 511 127.0.0.1:3000 ...
从主机进入的数据包绝不可能到达这个套接字,因为数据包会抵达 eth0 地址。正确答案是 0.0.0.0:3000。有人会质疑“0.0.0.0 难道不危险吗”,但容器内的 0.0.0.0 只表示该容器的整个网络命名空间;是否对外暴露由发布端口时的绑定地址决定。混淆这两个层次,会在两方面都得出错误结论。
实际现场中的情况
端口发布默认绑定主机的所有接口。
docker run -d -p 5432:5432 postgres:16 # 위험
docker run -d -p 127.0.0.1:5432:5432 postgres:16 # 안전
如果服务器拥有公网 IP,第一行就等于把数据库暴露给互联网。也不能因为主机防火墙进行了阻止就掉以轻心:发往容器的数据包的最终目的地不是主机,因此不会经过常规 INPUT 规则,目的地址在更早阶段就已改变。最安全也最简单的方法,是从一开始就限制绑定地址。
仅供容器之间通信的服务根本不应发布端口。还要记住,容器可以同时接入多个网络。把前端网络和后端网络分开,只让应用同时加入两边,就能使数据库从外部彻底不可见。
网络类型及适用场景
bridge (기본) 가상 스위치. 컨테이너끼리 사설 IP 로 통신
host 호스트 네트워크를 그대로 씀. 격리 없음
none 네트워크 없음. 계산만 하는 작업
container:<이름> 다른 컨테이너의 네임스페이스 공유
默认 bridge 与自定义 bridge 并不相同。 在默认网络中,容器不能按名称找到彼此,必须知道 IP。用户创建的网络带有内置 DNS,容器名称就是主机名。
docker network create app-net
docker run -d --name db --network app-net postgres:16
docker run --rm --network app-net alpine ping -c1 db # 이름으로 닿는다
因此,实际工作中总会创建一个网络。Compose 会自动完成这件事。
端口映射做什么、不做什么
-p 8080:80 是一条把主机 8080 端口连接到容器 80 端口的 NAT 规则。容器之间的通信与该映射无关,它们直接使用容器端口。
호스트에서 → localhost:8080
다른 컨테이너에서 → web:80 ← 8080 이 아니다
常见错误是在容器内调用 localhost:8080。容器的 localhost 是它自己。要调用其他容器,应使用其名称。
像 -p 127.0.0.1:8080:80 这样加上地址,就不会对外暴露。开放开发数据库时养成这个习惯可以避免事故——没有防火墙时,-p 5432:5432 就是在互联网上开放数据库。
从主机查看容器内部
即使容器中没有 ss 或 tcpdump,也可以从主机进入其命名空间。
PID=$(docker inspect -f '{{.State.Pid}}' myapp)
nsenter -t $PID -n ss -ltnp # 컨테이너의 리스닝 포트
nsenter -t $PID -n tcpdump -i any -c 20 port 80
-n 表示只进入网络命名空间,因此可以直接使用主机工具查看容器网络。这是诊断 distroless 镜像最可靠的方法。
下一项实验要做什么
创建自定义网络,使容器能够按名称找到彼此;确认其他网络中的容器确实不可见;最后尝试只在回环地址上发布端口。