Docker 与容器 列表全部文章
Docker 与容器
Docker 与容器第 3 / 5 篇

Docker 笔记(三)网络与存储篇:bridge/host、端口映射、volume 与挂载权限

容器的「网络不通」和「文件没权限」是运维群里最高频的两类问题,绝大多数不是 Docker 的 bug,而是网络模型与宿主机的差异没搞清。这篇讲四种网络模式、bridge 到底靠什么打通、-p 与 EXPOSE 的区别、容器间怎么互访、localhost 的陷阱,以及 volume/bind mount/tmpfs 怎么选和权限、时区怎么配。

更多

容器的"网络不通"和"文件没权限"是运维群里最高频的两类问题。绝大多数不是 Docker 的 bug,而是网络模型、文件系统权限跟宿主机的差异没搞清。这篇把这两块一次说透。

一、五种网络模式

模式怎么指定特点适用
bridge(默认)--network bridge独立网络命名空间,走 docker0 网桥 + NAT 出网绝大多数场景
host--network host直接用宿主机网络栈,没有隔离,端口直接占宿主机对网络性能极敏感、或需要绑定宿主机真实 IP
none--network none只有 lo,完全无网络离线批处理、安全隔离
container:<name>--network container:app共享另一个容器的网络命名空间sidecar(如抓包、调试容器)
overlayswarm/K8s 环境跨主机容器互通集群编排(一般不用手配)

host 模式的代价要说清楚:容器里 8080 就是宿主机的 8080,起第二个同样端口的容器直接冲突;而且"端口隔离"这层保护没有了。

二、bridge 到底靠什么打通

容器 eth0 ←─veth pair─→ docker0(172.17.0.1) ←─iptables NAT─→ 宿主机 eth0 → 外网
  • veth pair:一对虚拟网卡,一头在容器里(eth0),一头挂在 docker0 网桥上,像一根虚拟网线。
  • 出网(容器访问外网):iptables 做 MASQUERADE(SNAT),把容器内网 IP 换成宿主机 IP。
  • 入网(外部访问容器):靠 -p 生成的 DNAT 规则,把宿主机端口转发到容器 IP:端口。
docker network inspect bridge            # 看网桥、子网、挂了哪些容器
iptables -t nat -L DOCKER -n             # 看端口映射的真实规则
ip addr show docker0                     # 宿主机上的网桥
brctl show 2>/dev/null || bridge link    # 看网桥上挂的接口

-p 与 EXPOSE 是两回事

  • EXPOSE 8080:写在 Dockerfile 里,只是声明,给人和工具看,不产生任何转发规则。
  • -p 8080:80:真正把宿主机 8080 映射到容器 80。
  • -p 127.0.0.1:8080:80:只监听本机,生产环境强烈建议这样写,别把端口直接暴露到公网。
  • -P:把 EXPOSE 声明的端口随机映射到宿主机高位端口(测试用)。

三、容器之间怎么互相访问

关键结论:默认的 bridge(docker0)不支持用容器名互访,只有自定义网络才带内嵌 DNS。

docker network create app-net            # 建一个自定义 bridge

docker run -d --name mysql --network app-net \
  -e MYSQL_ROOT_PASSWORD=xxx -v mysqldata:/var/lib/mysql mysql:8.0

docker run -d --name app --network app-net -p 127.0.0.1:8080:8080 \
  -e SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/app myapp:1.0

上面 jdbc:mysql://mysql:3306 里的 mysql 就是容器名,由 Docker 内嵌 DNS(容器里 /etc/resolv.conf 指向 127.0.0.11)解析。同网络的容器可以直接用名字互访,IP 变了也不用改配置。

docker network connect app-net redis     # 运行中把容器加进网络
docker network disconnect app-net redis
docker exec app getent hosts mysql       # 在容器里验证名字能不能解析

--link 是老方案,官方已标记废弃,别再用。

四、localhost 的陷阱

这是容器里最反直觉的一点:容器里的 localhost 是容器自己,不是宿主机。

  • 容器里 curl localhost:3306 连的是容器内部的 3306——如果容器里没跑 MySQL,就是 Connection refused。
  • 容器里想访问宿主机上的服务,三种办法:
    1. Docker Desktop:用 host.docker.internal;
    2. Linux:用 docker0 网桥地址,通常是 172.17.0.1(ip addr show docker0 确认);
    3. 直接 --network host,代价是失去网络隔离。
  • 反过来,宿主机访问容器要用映射出来的宿主机端口,不是容器端口。

完整的五个"不通"场景和排查步骤见:容器里 curl localhost 永远拒绝连接:Docker 网络的五个经典"不通"与 ufw 那个著名的坑。

五、网络排障清单

docker network ls
docker network inspect app-net                       # 看谁在里面
docker exec app cat /etc/resolv.conf                 # 看 DNS
docker exec app getent hosts mysql                   # 测名字解析
docker exec app sh -c 'nc -zv mysql 3306'            # 测端口通不通(没有 nc 就用 busybox)
docker run --rm --network app-net busybox sh -c 'ping -c 2 mysql'   # 借调试容器

# 容器里啥命令都没有时,共享它的网络命名空间来排查
docker run --rm --network container:app nicolaka/netshoot ss -lntp

# 宿主机上看映射规则
iptables -t nat -L DOCKER -n --line-numbers
ss -lntp | grep 8080

容器里没有 curl/ping/ip 是常态(瘦身镜像)。带一个 busybox 或 netshoot 调试镜像能省很多时间。

六、存储:三种挂载方式

方式写法数据在哪适用
volume(推荐)-v mysqldata:/var/lib/mysqlDocker 管理的目录(/var/lib/docker/volumes/)数据库、需要持久化的一切
bind mount-v /opt/conf:/etc/app宿主机的指定路径配置文件、代码目录、日志采集
tmpfs--tmpfs /tmp内存临时文件、密钥,容器停就没
docker volume create mysqldata
docker volume ls
docker volume inspect mysqldata          # 看真实路径
docker volume prune                      # 清理没被引用的卷(危险,先看清楚)

# --mount 写法更明确,推荐在脚本里用
docker run -d --mount type=volume,src=mysqldata,dst=/var/lib/mysql mysql:8.0
docker run -d --mount type=bind,src=/opt/conf,dst=/etc/app,readonly myapp:1.0

匿名卷(只写 -v /var/lib/mysql 不给名字)会被分配一个随机名,docker volume prune 容易误删,一律用命名卷。

七、Permission denied:uid 对不上

容器里跑的用户 uid 和宿主机文件的 owner 对不上,是最常见的挂载报错。

# 宿主机上看文件归属
ls -ln /opt/data          # 注意是 -n,看数字 uid

# 容器里看当前用户
docker exec app id

三种解法:

  1. 启动时指定 uid:docker run --user $(id -u):$(id -g) ...(注意容器内该 uid 需要有对应权限)。
  2. Dockerfile 里建同名同 uid 的用户:groupadd -g 1000 app && useradd -u 1000 -g app app,跟宿主机对齐。
  3. 改文件权限:chown -R 1000:1000 /opt/data 或 chmod 777(临时方案,不推荐上生产)。

如果宿主机开了 SELinux(CentOS/RHEL),还要加 :z 或 :Z:

docker run -v /opt/data:/data:z myapp:1.0   # z=共享标签,Z=私有标签

八、时区差 8 小时

容器默认 UTC,日志和定时任务就差了 8 小时。两种修法:

# 1. 挂宿主机的 localtime(最省事)
docker run -v /etc/localtime:/etc/localtime:ro -v /etc/timezone:/etc/timezone:ro myapp:1.0

# 2. 传环境变量(alpine 需先装 tzdata)
docker run -e TZ=Asia/Shanghai myapp:1.0

Java 应用还有一层:JVM 有自己的默认时区。TimeZone.getDefault() 优先读 TZ,否则读 /etc/localtime。保险起见可以显式指定:

java -Duser.timezone=Asia/Shanghai -jar app.jar
# 或者代码里:TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));

另外数据库连接的 serverTimezone 也要配(MySQL 8 驱动默认 UTC,存进去的时间会偏)。

九、改了文件不生效

三种情况分开看:

  • bind mount 的宿主机文件:改了立刻生效,但应用要能感知(Java 没有热部署就得重启;Spring Boot DevTools 可以)。
  • 镜像里的文件:必须重新 build,改宿主机文件没用。构建缓存导致的"没反应"见镜像篇第八节。
  • node_modules 之类的目录:如果 Dockerfile 里声明了 VOLUME /app/node_modules,匿名卷会屏蔽镜像里的内容,导致你 COPY 进去的依赖不生效。

这几件事我一次性踩过,写在 挂载目录 Permission denied 及两个"难兄难弟" 里了。

十、命令清单

docker volume create/ls/inspect/rm/prune
docker network create/ls/inspect/connect/disconnect/rm/prune
docker system df -v                                  # 看容器/镜像/卷各占多少磁盘
docker cp ./file app:/tmp/file                       # 往容器里拷文件(临时用,别当部署手段)
docker export app > app.tar                          # 导出容器文件系统(不含历史层)
docker diff app                                      # 看容器层改了哪些文件

备忘

  • 自定义 bridge 网络才能用容器名互访;默认 docker0 不行。
  • 容器里的 localhost 是容器自己;访问宿主机用 host.docker.internal(Desktop)或 172.17.0.1。
  • -p 建议写 127.0.0.1:8080:80,别把端口裸奔在公网。
  • 持久化一律用命名卷;匿名卷会被 prune 误删。
  • 挂载报 Permission denied,先 ls -ln 和 id 对 uid;SELinux 环境加 :z。
  • 时区要同时照顾容器(TZ / localtime)和 JVM(user.timezone)和 JDBC(serverTimezone)。
— 感谢读到这里 —发现错误,或想补充?告诉我 →
TURN KNOWLEDGE INTO SOMETHING REAL

有想法,也想把它做出来?

从技术方案到上线交付,聊聊你的下一个项目。

聊聊需求

一起聊聊

搜索文章

输入关键词,按标题和摘要即时筛选。

浏览分类文章归档

阅读设置

字号
行宽
专注模式

专注模式会隐藏页头与侧栏,只留正文。设置保存在当前浏览器。

稍后阅读

保存在当前浏览器,留给下一次安静阅读。

总浏览 -- 次 · 总访客 -- 人 · 今日浏览 -- 次 · 今日访客 -- 人