跳到主要内容

05 数据卷与网络

这一章解决两个实际问题:容器删了数据怎么办(数据卷),以及容器之间怎么互相访问(网络)。

1. 为什么需要数据卷​

容器的文件系统是随容器走的:容器一删,里面写的数据全没。数据库容器如果不做持久化,docker rm 一下数据就灰飞烟灭了。

解决办法:把数据存到容器外面,有两种方式。

2. 两种挂载方式​

2.1 绑定挂载(Bind Mount):挂宿主机目录​

把宿主机的某个目录直接挂进容器:

# -v 宿主机路径:容器路径
docker run -d --name web -p 8080:80 \
-v /data/html:/usr/share/nginx/html \
nginx:1.25

宿主机 /data/html 里的文件,容器里 /usr/share/nginx/html 立刻可见,双向实时同步。

适合场景:挂配置文件、挂代码、挂前端静态资源——你需要直接在宿主机上编辑这些文件。

还可以加 :ro 设为只读,防止容器改动宿主机文件:

-v /data/nginx.conf:/etc/nginx/nginx.conf:ro

2.2 命名卷(Named Volume):交给 Docker 管理​

# 创建一个卷(也可以不创建,docker run 时自动建)
docker volume create mysql-data

# 挂载卷启动 MySQL
docker run -d --name db \
-e MYSQL_ROOT_PASSWORD=123456 \
-v mysql-data:/var/lib/mysql \
mysql:8.0

数据实际存在宿主机的 /var/lib/docker/volumes/mysql-data/_data,由 Docker 统一管理。

适合场景:数据库数据目录这类「不需要人肉直接编辑,但必须持久化」的数据。

2.3 怎么选​

绑定挂载命名卷
写法-v /宿主机绝对路径:/容器路径-v 卷名:/容器路径
数据位置你指定的目录Docker 管理的目录
适合配置文件、代码、静态资源数据库数据、应用产生的数据

区分技巧:-v 冒号前面带 / 的是路径(绑定挂载),不带 / 的是卷名(命名卷)。

2.4 卷的管理命令​

docker volume ls # 列出所有卷
docker volume inspect mysql-data # 查看卷详情(含实际存储路径)
docker volume rm mysql-data # 删除卷(数据会丢,慎重)
docker volume prune # 清理没有被任何容器使用的卷

3. 验证数据持久化​

做个实验,感受数据卷的作用:

# 1. 带卷启动 MySQL,建个库
docker run -d --name db -e MYSQL_ROOT_PASSWORD=123456 -v mysql-data:/var/lib/mysql mysql:8.0
docker exec -it db mysql -uroot -p123456 -e "CREATE DATABASE demo;"

# 2. 删掉容器!
docker rm -f db

# 3. 用同一个卷重新起一个容器
docker run -d --name db2 -e MYSQL_ROOT_PASSWORD=123456 -v mysql-data:/var/lib/mysql mysql:8.0

# 4. demo 库还在
docker exec -it db2 mysql -uroot -p123456 -e "SHOW DATABASES;"

4. 容器网络​

网络是多容器部署的地基。后面用 Docker Compose 一波部署 MySQL + Redis + 后端 + Nginx 时,服务之间怎么互相访问、端口该怎么写,答案全在这一节。

4.1 问题:后端容器怎么连数据库容器?​

假设后端应用和 MySQL 各跑一个容器,后端连数据库时写 localhost:3306?不行——容器里的 localhost 指的是容器自己,不是宿主机,更不是别的容器。

正确做法:把两个容器放进同一个自定义网络,然后用容器名当主机名访问。

4.2 先建立画面感:网络就是虚拟交换机​

把每个容器想象成一台独立的小电脑:有自己的 IP、自己的 localhost。而 Docker 网络就是一台虚拟交换机——插在同一台交换机上的容器互相能连通,不在同一个网络里的互相看不见。

两个关键事实:

  1. 自定义网络自带 DNS:容器名会自动解析成对应容器的 IP。容器的 IP 每次重启可能变,但名字不变,所以互访永远写名字、不写 IP。
  2. 网络就是隔离边界:不同网络里的容器互相不通。这既是坑(忘了加同一个网络就连不上),也是安全手段(数据库只放内网,外面摸不到)。

4.3 自定义网络实战​

# 1. 创建网络
docker network create mynet

# 2. 启动 MySQL,加入网络
docker run -d --name db --network mynet \
-e MYSQL_ROOT_PASSWORD=123456 \
-v mysql-data:/var/lib/mysql \
mysql:8.0

# 3. 启动应用容器,加入同一个网络
docker run -d --name api --network mynet -p 8000:8000 myapi:1.0

现在在 api 容器里,直接用 db 这个名字就能访问 MySQL:

# 应用里的数据库连接串写:
mysql+pymysql://root:123456@db:3306/demo
# ^^ 容器名即主机名

这就是 4.2 说的内置 DNS 在起作用:db 被自动解析成 db 容器的 IP。这就是容器互联的标准姿势。

验证一下:

# 在 api 容器里 ping db
docker exec -it api ping db

4.4 最容易搞混的问题:该连哪个端口?​

假设 MySQL 容器这样启动(宿主机 3306 被占了,映射到 13306):

docker run -d --name db --network mynet -p 13306:3306 mysql:8.0

现在出现了两个端口,谁用哪个?

  • 同一网络里的容器互访:用服务的内部端口(db:3306),跟 -p 映射成多少完全无关——容器之间走的是虚拟交换机,根本不经过宿主机端口
  • 宿主机(或外部)访问容器:才走映射端口(127.0.0.1:13306)

记一句话:-p 是给「网络外面的人」开的门,网络里面的容器串门不走这个门。 反过来也成立——像 4.3 里的 db 根本没写 -p,api 照样能连它,只是宿主机连不上而已。

4.5 默认网络的坑​

不指定 --network 时,容器会进默认的 bridge 网络。默认网络里不支持用容器名互相访问(只能用 IP,而 IP 每次重启可能变)。所以:

只要涉及容器互联,就创建自定义网络。

4.6 到了 Compose,这一切全自动​

上面手动建网络、逐个 --network 的操作,Docker Compose 会全包了。执行 docker compose up 时它自动做三件事:

  1. 建一个专属网络,名字是 项目目录名_default
  2. 把 compose.yaml 里的所有服务都加进这个网络
  3. 服务名自动成为主机名——这就是为什么 Compose 项目里连接串直接写 mysql:3306、redis:6379
services:
api:
build: .
ports:
- "8000:8000" # 只有需要被浏览器/外部访问的服务才写 ports
environment:
- DB_URL=mysql+pymysql://root:123456@mysql:3306/demo # 主机名 = 服务名
mysql:
image: mysql:8.0 # 没写 ports:只有同网络的 api 能访问它,最安全
redis:
image: redis:7

对照 4.4 的结论看:mysql、redis 不写 ports 一点问题没有,api 走的是容器网络;而 nginx/api 这类要接待外部流量的服务才需要 ports。**「只有入口服务映射端口,其余全藏在网络里」**就是 Compose 一波部署 MySQL + Redis + 前后端时的标准布局,后面的 Compose 全栈实战 会完整走一遍。

需要更细的隔离(比如 nginx 不允许直连数据库)时,才在 Compose 里手写多个网络,这个在 Compose 配置详解的 networks 一节讲。

4.7 网络管理与排查命令​

docker network ls # 列出网络
docker network inspect mynet # 查看网络详情(哪些容器在里面、各自的 IP)
docker network connect mynet 容器名 # 把已运行的容器加进网络
docker network rm mynet # 删除网络

容器 A 连不上容器 B 时,按这个顺序排查:

# 1. 两个容器在同一个网络里吗?看 Containers 列表
docker network inspect mynet

# 2. 名字能解析、网络能通吗?
docker exec -it api ping db

# 3. 网络通但服务没响应?多半是 B 里的应用没起来或没监听对
docker exec -it api curl http://web:8000/health # B 是 HTTP 服务就 curl 它
docker logs db # B 是数据库就看它的启动日志

第 3 步很常见:MySQL 容器状态是 Up,但内部还在初始化,连接会被拒。这不是网络问题,是「启动了 ≠ 就绪了」,到 Compose 里用 healthcheck 解决。

4.8 容器访问宿主机​

有时容器里的应用要访问跑在宿主机上的服务(比如宿主机直接装的 MySQL):

  • Windows / macOS:用特殊域名 host.docker.internal
  • Linux:启动时加 --add-host=host.docker.internal:host-gateway,然后同样用这个域名

5. 端口映射回顾​

-p 参数本质也是网络知识,完整格式:

-p 8080:80 # 宿主机所有网卡的 8080 → 容器 80
-p 127.0.0.1:3306:3306 # 只允许本机访问,不对外网开放(数据库推荐)
-p 80:80 -p 443:443 # 可以写多个

安全提醒:-p 3306:3306 会把数据库直接暴露到公网(Docker 还会自动绕过部分防火墙规则)。生产环境数据库要么不映射端口(只走容器网络),要么绑定 127.0.0.1。

小结​

  • 数据卷解决持久化:配置/代码用绑定挂载,数据库数据用命名卷
  • 删容器不删卷,数据就不会丢
  • 容器互联:创建自定义网络 + 用容器名当主机名,容器里的 localhost 是容器自己
  • 端口分两种:容器互访走内部端口,外部访问才走 -p 映射端口,两者无关
  • Compose 会自动建网络、自动把服务名变成主机名——只有入口服务映射端口,其余全藏在网络里
  • 数据库端口不要随便暴露到公网

Docker 基础到此完成。但你可能已经感觉到:一个应用要敲好几条长长的 docker run,太麻烦了。下一部分的 Docker Compose 就是来解决这个问题的:01-认识Compose