网络
这一课你将能做到
- 解释清楚「容器里的
localhost 不是宿主机也不是别的容器」,并让 Spring Boot 容器用容器名直连 MySQL/Redis
- 创建自定义网络,验证其内置 DNS 能力,说清它与默认 bridge 的核心区别
- 熟练使用
-p 做端口映射(含只绑定 127.0.0.1 的安全写法),排查端口不通问题
为什么要学
容器化 Java 应用最常卡住人的地方不是构建,而是连接:docker run 起了 MySQL,Spring Boot 配置里写 localhost:3306,启动报 Communications link failure。原因是每个容器有自己独立的网络命名空间——你的应用容器里的 localhost 是它自己,不是 MySQL 容器。
网络也是生产安全的分界线:哪些端口该映射到宿主机、哪些只该在容器网络内部互通,直接决定数据库是否暴露在公网。本课把「连通」和「隔离」一起讲清楚。
先建立图像
容器网络的三层视角:
外部用户 ──→ 宿主机:8080 ──(端口映射 -p)──→ 容器A:8080 (Spring Boot)
│
宿主机开发 ──→ 宿主机:6379 ──(-p 6379)──→ 容器B:6379 (Redis)
│
容器A ──(自定义网络 app-net,走内部 DNS)──→ 容器C:3306 (MySQL)
注意:容器A 连的是 mysql:3306,不是 localhost:3306
三种网络模式速览:
| 模式 | 说明 | 场景 |
|---|
| bridge(默认) | 容器接入虚拟网桥,经 NAT 出网 | 绝大多数场景 |
| host | 容器直接用宿主机网络栈,无隔离 | 性能极敏感、少用 |
| none | 无网络 | 离线任务、安全隔离 |
| container:xxx | 共享另一个容器的网络栈 | sidecar 模式(K8s Pod 的原理) |
跟着做
步骤 1:先踩一次「localhost 连不上」的坑
docker run -d --name lonely-mysql -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0
docker run -d --name lonely-app demo-app:1.0 # 假设配置是 jdbc:mysql://localhost:3306/app
docker logs lonely-app --tail 20
# Communications link failure —— app 容器里的 localhost 是它自己,不是 MySQL
结论固化到笔记:跨容器连接永远不用 localhost。宿主机上直连容器,走的是端口映射;容器连容器,走的是容器网络。
步骤 2:认识默认网络与网络命令
docker network ls # bridge / host / none 三个内置网络
docker network inspect bridge # 看网段(通常 172.17.0.0/16)与已连接容器
进入容器看自己的网络身份:
docker exec lonely-mysql hostname -i # 容器自己的 IP
docker exec lonely-mysql cat /etc/hosts
步骤 3:创建自定义网络(本课主角)
docker network create app-net
步骤 4:MySQL、Redis 接入自定义网络
docker run -d --name app-mysql \
--network app-net \
-v mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-e MYSQL_DATABASE=appdb \
mysql:8.0
docker run -d --name app-redis --network app-net redis:7 --requirepass "dev123"
注意:两个容器都没有 -p。它们不需要对外暴露,只需网络内部可达。
步骤 5:验证容器名互访(自定义网络的内置 DNS)
# 用任何同网络容器 ping / 探测其他容器的【容器名】
docker exec app-mysql ping -c 2 app-redis # 能 ping 通(部分镜像无 ping,用下面方式)
docker exec app-mysql getent hosts app-redis # DNS 解析:返回容器 IP
容器名(app-mysql)可直接当域名用,DNS 由 Docker 内置解析器提供。这是与默认 bridge 的核心区别——默认 bridge 里容器名不通,只能互查 IP,而 IP 重启后会变。
步骤 6:Spring Boot 容器接入并连通
docker run -d --name app-backend \
--network app-net \
-p 8080:8080 \
-e SPRING_DATASOURCE_URL=jdbc:mysql://app-mysql:3306/appdb \
-e SPRING_DATASOURCE_USERNAME=root \
-e SPRING_DATASOURCE_PASSWORD=123456 \
-e SPRING_DATA_REDIS_HOST=app-redis \
-e SPRING_DATA_REDIS_PASSWORD=dev123 \
demo-app:1.0
docker logs -f app-backend # HikariPool 启动成功
curl http://localhost:8080/actuator/health
数据源 URL 里的主机名是 app-mysql(容器名),不是 localhost 也不是 IP。
步骤 7:端口映射的细节
docker run -d -p 8080:8080 ... # 宿主机所有网卡的 8080 → 容器 8080
docker run -d -p 127.0.0.1:8080:8080 ... # 只绑定本机回环:外网访问不到,调试更安全
docker run -d -p 8081:80 # 宿主机 8081 → 容器 80(两侧端口可不同)
docker run -d -P ... # 大写 P:把 EXPOSE 声明的端口随机映射
docker port app-backend # 查看某容器的映射关系
步骤 8:验证网络隔离
docker network create other-net
docker run -d --name lonely-redis --network other-net redis:7
docker exec app-backend getent hosts lonely-redis # 无输出——不同网络,DNS 不通
docker network connect app-net lonely-redis # 热接入:立即互通
docker network disconnect app-net lonely-redis # 摘除
讲清楚要点
- 每个容器 = 独立网络命名空间:有自己的 lo、eth0、IP、端口空间。所以容器间 localhost 互不相通;容器内监听的端口想被宿主机访问,必须
-p。
- 自定义 bridge 是多容器部署的标准做法:与默认 bridge 的三大区别——内置 DNS(容器名即域名)、网络隔离(不同业务可分网)、支持动态连接断开(connect/disconnect)。默认 bridge 存在只为兼容旧版。
-p 与 EXPOSE 各司其职:EXPOSE 是元数据声明;-p 是实际的 DNAT 端口转发规则。数据库类容器(MySQL/Redis)在生产上刻意不加 -p,只允许同网络容器访问——纵深防御的第一层。
- 容器 IP 不可靠,容器名才可靠:容器重建后 IP 会变(Docker 按顺序分配),容器名跟着配置走。Spring Boot 配置里永远写容器名/服务名,禁止写死容器 IP。
- 连接地址速查:宿主机 → 容器:
localhost:映射端口;容器 → 容器(同网络):容器名:容器端口;容器 → 宿主机上的服务:host.docker.internal(Docker Desktop;Linux 需 --add-host=host.docker.internal:host-gateway)。
你可能会懵的地方
-
两个容器都在默认 bridge 里,能互相访问吗?
能,通过对方 IP(网桥内互通)。但没有内置 DNS,只能拿 IP 或 /etc/hosts 硬编码——IP 不稳定,所以默认 bridge 里跑多容器架构是反模式,务必用自定义网络。
-
-p 3306:3306 暴露 MySQL 有多危险?
云服务器安全组若放行了 3306,等于数据库裸奔公网(root 弱密码场景必被扫)。规范:MySQL/Redis 不做 -p;确需本地调试时用 -p 127.0.0.1:3306:3306 只绑回环。
-
Spring Boot 在本地 IDE 跑(不在容器里),怎么连容器里的 MySQL?
走宿主机端口映射:容器加 -p 3306:3306,配置写 localhost:3306。注意区分:应用在容器里时写容器名,应用在宿主机时写 localhost。
-
容器如何访问宿主机上的其他服务(比如宿主机的 Kafka)?
Docker Desktop 下用域名 host.docker.internal;Linux 上启动时加 --add-host=host.docker.internal:host-gateway。
-
为什么 compose 文件里我不用自己建网络?
Compose 默认为每个项目创建一个自定义网络,服务名即 DNS 名。这也是第 10 课里服务间直接写 jdbc:mysql://mysql:3306 的原因——你已经在享受自定义网络的便利。
错法对照
| 错法 | 问题 | 正解 |
|---|
容器 A 连 localhost:3306 找容器 B 的 MySQL | localhost 是容器自己 | 同网络容器名直连 mysql:3306 |
| 多容器挤在默认 bridge 里 | 无 DNS,只能靠会变的 IP | docker network create + --network |
数据库容器 -p 3306:3306 暴露公网 | 被扫描爆破只是时间问题 | 不映射端口;调试只绑 127.0.0.1 |
| Spring Boot 配置写死容器 IP | 容器重建 IP 即变 | 写容器名/服务名 |
-p 8080:8080 忘了容器内也监听 8080 | 映射了也连不上 | 对齐:server.port、EXPOSE、-p 三处一致 |
| 排查网络只看配置不看现状 | 盲猜浪费时间 | network inspect + 容器内 getent hosts 实测 |
卡点急救
- Spring Boot 报
Communications link failure:按序检查——两容器是否同一网络(docker network inspect app-net);URL 主机名是否为容器名;MySQL 是否初始化完成(首次启动要几十秒,docker logs app-mysql 看 ready for connections)。
-p 报 port is already allocated:宿主机端口被别的容器或本地进程占用。docker ps 查现有映射;Windows 用 netstat -ano | findstr 8080 找进程。
- 外部机器访问不了映射端口:确认
-p 没绑死 127.0.0.1;云服务器检查安全组/防火墙规则;确认应用监听的是 0.0.0.0 而非仅 127.0.0.1(Spring Boot 默认是 0.0.0.0)。
- 容器名解析失败(getent 无输出):容器不在同一网络,或用的是默认 bridge。
network connect 网络名 容器名 热修。
- 网段冲突:公司内网与 Docker 默认网段重叠导致路由异常。在
daemon.json 配 default-address-pools 自定义网段(如 172.30.0.0/16)。
面试常问
-
容器之间是如何通信的?
同一 bridge 网络内:容器通过虚拟网桥(veth pair + bridge)二层互通,自定义网络由 Docker 内置 DNS 把容器名解析为容器 IP;跨网络默认隔离;出公网走宿主机 NAT。
-
默认 bridge 和自定义 bridge 的区别?(高频)
自定义 bridge 提供内置 DNS(容器名互访)、更好的隔离性(不同网络互不可达)、支持运行时动态 connect/disconnect;默认 bridge 只有 IP 互通,主要为了向后兼容保留。
-
EXPOSE 和 -p 的区别?
EXPOSE 是镜像元数据声明(文档与 -P 的依据),不做任何转发;-p 建立宿主机到容器的 DNAT 映射,真正决定外部可达性。
-
容器化后 Spring Boot 的数据源 URL 应该怎么写?
与应用运行位置有关:应用在容器内且与 MySQL 同网络 → jdbc:mysql://容器名或服务名:3306/db;应用在宿主机调试 → 容器 -p 3306:3306 后写 localhost:3306。核心是搞清楚「相对于谁」。
自测三问
- 为什么容器 A 无法通过
localhost:3306 访问容器 B 的 MySQL?正确的连接方式有哪些?
- 自定义 bridge 相比默认 bridge 多了哪些能力?分别用哪个命令验证?
- 生产环境 MySQL 容器要不要
-p 3306:3306?需要本地调试时怎么写更安全?
小结一句话
容器网络的关键认知是「地址相对于谁」——容器内 localhost 只是自己,跨容器靠自定义网络的容器名 DNS,对外靠 -p 端口映射,数据库端口保持对公网沉默。
延伸
- Docker 网络官方文档
- 下一课预告:Docker Compose——把本课的 app-mysql、app-redis、app-backend 三个容器写进一个 YAML,一键起停。