1. 存储底子:三类数据落点,先搞懂再动手
玩Docker绕不开一个问题:容器是“一次性”的,我写的配置、生成的数据,容器一删就全没了。不少刚接触Docker的朋友第一次跑数据库容器,重启之后发现数据清空,整个人就慌了。其实这不是Docker的锅,而是没理解容器文件系统和持久化存储的区别。
1.1 镜像层与容器层:为什么容器天生“短命”
Docker镜像本身是分层构建的,每一层都是只读的。容器运行时,Docker会在镜像层之上加一个可写层,所有文件的增删改都发生在这个可写层里。可写层的生命周期和容器完全绑定,容器删除,可写层跟着销毁,数据随之消失。这种设计让容器的启动和销毁变得极其轻量,但也注定了“不适合存数据”。
我见过有人硬生生在容器里改配置文件、放日志数据,结果容器一重建,所有改动烟消云散。排查了半天,最后才发现问题出在“压根没挂载任何存储”。理解这个底层模型之后,你就知道为什么Docker官方一直强调:数据一定要落在三类持久化方案里,别写在容器内。
1.2 三类存储方式对比:卷、绑定挂载、tmpfs怎么选
Docker提供三种主流存储挂载方式,很多教程只列了命令没讲清区别,我带你把它们的底层逻辑和工作原理盘一盘。
| 存储方式 | 数据存储位置 | 持久性 | 典型场景 |
|---|---|---|---|
| 卷(Volume) | Docker管理区域(宿主机特定目录) | 持久 | 数据库数据、应用配置、跨容器共享 |
| 绑定挂载(Bind Mount) | 宿主机任意目录,直接映射 | 持久 | 开发调试、日志收集、配置文件直改 |
| tmpfs | 宿主机内存中 | 临时 | 敏感信息、临时缓存、不想落盘的数据 |
卷是最推荐的方案,因为它由Docker完整管理,不受宿主机目录结构影响,权限控制也更可控。绑定挂载适合开发和调试,因为你可以直接在宿主机上改代码,容器内立刻生效,省去了反复构建镜像的折腾。tmpfs则适合存放临时性数据,比如临时token、密钥缓存,容器停止即数据消失,天然规避了敏感数据落盘的风险。
这里有一个新手特别容易踩的误区:卷和绑定挂载到底是不是同一个东西?不是。卷的目录在Linux上是/var/lib/docker/volumes/下面,Docker自己管理;绑定挂载是你手动指定的宿主机路径,比如/home/user/app直接映射进去。两者命令写起来像,但语义完全不同。
还有一个细节值得注意:绑定挂载如果宿主机的目录不存在,Docker会帮你自动创建。听起来方便,但如果你路径写错了,Docker会悄悄建一个空目录挂进去,容器里看不到原来的文件,排查半天才发现是路径拼错了。
2. 网络模型:容器之间是怎么“通上话”的
Docker的网络是很多人的知识盲区。跑两个容器,A想访问B,发现IP不通或者服务连不上,就开始怀疑是不是镜像有问题。实际上,大部分网络不通的问题都出在对Docker默认网络模型的不理解上。
2.1 三种内置网络:bridge、host、none的底层逻辑
Docker安装完成后,默认会创建三个网络。默认不指定任何参数时,容器加入的是bridge网络。
bridge网络是“桥接”模式,Docker会创建一个虚拟网桥,宿主机上的容器都接入这个网桥,容器之间可以通过IP地址直接通信。这个网桥默认网段通常是172.17.0.0/16,我在生产环境遇到过网段冲突的情况:公司内部网络恰好也用了这个网段,导致宿主机无法访问容器,最后只能自定义网段避开冲突。这类问题特别隐蔽,日志里看不到任何报错,但连接就是不通过。
host网络的意思就是容器直接使用宿主机的网络栈,没有独立的网络命名空间,不占用独立IP。容器监听端口等同于宿主机监听端口,网络性能更好,因为少了NAT转换一层。代价是没法做端口映射,启动时可以省略-p参数,但也意味着多个容器不能同时监听同一端口。
none网络则是“无网络”模式,容器只有一个回环接口,外部无法访问,容器也无法出网访问外部资源。听起来鸡肋,但实际用在离线任务、批处理脚本、安全沙箱里非常好用,从源头断绝了网络暴露面。有安全要求的业务场景,把不需要对外通信的容器挂到none网络,等于从网络层面上锁死了。
2.2 为什么我强烈建议用自定义网络
默认bridge网络最大的痛点是:容器之间只能通过IP通信,不能通过容器名。容器重启后IP可能变化,如果服务配置里写的是IP,一旦容器重启重建,配置就失效了。
Docker内置了DNS解析功能,但在自定义网络中才会启用容器名自动解析。这个特性在编排环境里特别关键。举个例子,你跑了一个配置中心和一个业务服务,业务服务连接配置中心的地址如果写的是容器名而非IP,即使配置中心容器重启、IP变了,业务服务都能自动找到它,无需重启配置。
手动创建一个自定义桥接网络,命令非常简洁:
docker network create --driver bridge my-network创建完成后,启动容器时指定--network my-network,同一网络下的容器自动支持通过容器名互相解析。这个操作只需要几秒钟,却直接改变了容器之间通信的可靠性。我在实际项目中,凡是涉及多容器协作的场景,基本不会再用默认网络。
自定义网络还有一个附带好处:隔离性。不同网络的容器默认互相隔离,无法通信。你不需要费劲配置防火墙规则,只要把不同业务的容器放到不同网络里,网络层面就天然隔离了。比如业务服务放在backend-net,管理面板放在admin-net,再通过服务端应用内部调用打通,安全性比“全放一个网络”高出不少。
2.3 端口映射:外部访问容器的唯一通道
默认情况下,容器的网络是“封闭”的,宿主机之外的机器无法访问容器内的端口。要让外部访问容器里的服务,必须做端口映射:把宿主机的某个端口映射到容器的端口上。
docker run -d -p 8080:80 nginx:latest这行命令的意思是:宿主机上的8080端口,转发到容器的80端口。外部用户访问宿主机IP:8080就能到达容器内的Nginx服务。你可以把-p理解成“快递驿站”,宿主机端口是驿站的取件窗口,容器端口是真正的收件人。外部快递员不需要知道收件人具体在哪,只要把包裹送到窗口,驿站自然会送到收件人手里。
端口映射有几个细节值得注意。第一,-p只映射一个端口,-P则会把容器里所有暴露的端口随机映射到宿主机高位端口,一般不建议生产使用,映射关系太随机不好排查。第二,端口映射只对bridge网络生效,host网络模式下不需要映射,端口直接就是宿主机的端口。第三,修改端口映射必须重新创建容器,docker run里设置的参数没法动态修改。
我踩过的坑是:宿主机端口被别的进程占用时,Docker不会报错,容器也能正常启动,但外部访问始终不通。排查时用ss -lntp看端口监听情况,发现8080被另一个程序占用了,根本走的不是容器这条链。所以启动前先确认宿主机端口是干净的,避免自找麻烦。
3. 实操演练:从零搭建带持久化和网络的应用
前面把原理捋了一遍,这一节我用一个完整的场景带你把存储和网络串起来。目标:跑一个数据库容器和一个应用容器,数据库的数据放在卷里,应用容器通过网络访问数据库,数据库不对外暴露端口。
3.1 存储实操:创建卷并挂载到容器
先创建一个独立的卷,用于存放数据库的数据文件:
docker volume create mysql-data查看卷信息:
docker volume inspect mysql-data输出里能看到卷的挂载点在宿主机上的实际路径。Docker会帮你创建好,不需要手动干预目录权限。启动数据库容器并挂载卷:
docker run -d \ --name mysql-server \ --network app-network \ -e MYSQL_ROOT_PASSWORD=my-secret-pw \ -e MYSQL_DATABASE=testdb \ -v mysql-data:/var/lib/mysql \ mysql:8.0注意这里我把--network app-network也加上了,前提是你已经提前创建了这个网络。如果还没创建,先跑:
docker network create app-network挂载卷的过程就是把宿主机的卷目录映射到容器内的/var/lib/mysql。MySQL往这个目录写的数据都会落到卷里,即使容器删除重建,只要挂载同一个卷,数据都还在。
验证一下数据持久化是否有效。先进入容器创建一个测试表:
docker exec -it mysql-server mysql -uroot -pmy-secret-pw -e "CREATE DATABASE IF NOT EXISTS test; USE test; CREATE TABLE t(id INT PRIMARY KEY);"然后强制删除容器,重新创建并挂载同一个卷:
docker rm -f mysql-server docker run -d --name mysql-new ... (参数同上)进入新容器查询,之前创建的数据库和表都还在,说明卷确实写入了宿主机目录,容器本身销毁不影响数据。
3.2 网络实操:验证容器间通信与DNS解析
既然是同一个自定义网络,应用容器可以直接通过容器名访问数据库,不用写IP。
启动一个测试用的应用容器:
docker run -d --name app-server --network app-network alpine sleep 3600进入应用容器测试连通性:
docker exec -it app-server ping mysql-server能看到mysql-server这个域名被解析成了对应IP,说明Docker内置DNS解析生效。如果这一步用的是默认bridge网络,ping mysql-server会直接报错,因为默认网络不支持容器名解析。
这里我再强调一遍:自定义网络解决了两个核心问题——稳定的通信入口和网络隔离边界。我在部署监控系统时,把监控探针和业务服务放在同一个网络,外部其它容器默认无法访问业务服务,这比任何防火墙规则都更简单可靠。
3.3 组合场景:一个可复用的编排命令总结
上面几步单独执行可以看清每步的细节,实际生产环境中你更可能用编排工具一次把网络和存储全建好。这里给一段可直接复用的命令模板,省去你到处翻文档。
# 1. 创建自定义网络 docker network create --driver bridge app-network # 2. 创建数据卷 docker volume create mysql-data # 3. 启动数据库容器(不对外暴露端口) docker run -d \ --name mysql-server \ --network app-network \ -v mysql-data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=my-secret-pw \ mariadb:10.11 # 4. 启动后端应用容器(通过容器名访问数据库) docker run -d \ --name backend-server \ --network app-network \ -p 8080:8080 \ -e DB_HOST=mysql-server \ -e DB_USER=root \ -e DB_PASSWORD=my-secret-pw \ your-backend-image:latest这套组合拳下来,后端服务通过DB_HOST=mysql-server就能直连数据库,外部用户只能通过8080端口访问后端,数据库完全在内部网络里,对外零暴露。这种架构模式是微服务上容器后最常见的一种基础范式。
4. 排错实录:存储和网络最常见的问题清单
存储和网络出现问题,排查起来往往比业务代码问题更费劲,因为报错信息不一定直观。我把这几年代运维和实操里踩过的高频问题整理成了一张速查表,按图索骥能省很多时间。
| 现象 | 根本原因 | 解决方式 |
|---|---|---|
| 容器启动后报权限拒绝 | 挂载目录权限不一致,宿主机目录属主和容器内用户不匹配 | 显式指定用户ID,或用卷替代绑定挂载 |
| 数据丢失,容器重建后空数据 | 忘了挂载卷,数据写在容器可写层 | 创建卷并挂载到数据目录 |
| 容器A访问容器B超时 | 不在同一个网络,或网络隔离导致不相通 | 统一加入同一自定义网络 |
| 使用容器名ping不通 | 默认bridge网络不支持DNS解析 | 使用自定义网络 |
| 外部访问服务不通 | 端口映射未生效或宿主机端口被占用 | 检查docker ps的端口映射,用ss查看宿主机端口 |
| 网络占用了内部网段,无法访问 | 默认网段和公司内部网段冲突 | 自定义网络并指定不冲突的网段 |
4.1 卷权限问题的排查实录
权限问题是存储方向翻车率最高的一类。典型的场景:挂载了一个宿主机目录到容器内,容器内应用启动时报Permission denied。原因很简单,宿主机目录的属主是uid=1000(比如普通用户),但容器内进程是以root身份运行的,Linux的权限模型要求进程有足够的权限访问挂载点,一旦容器内用户和宿主机目录属主不匹配,就可能碰到权限墙。
解决方式我在项目里用得最多的是两种。第一,启动时指定用户ID,让容器内的进程以宿主机目录属主的身份运行,比如docker run --user 1000:1000。第二,直接把卷的属主改成容器内运行用户的ID,例如chown -R 1000:1000 /data。无论哪种方式,核心逻辑都是“让双方的身份对齐”。遇到这个问题别硬扛,权限模型是Linux的基础规则,绕不过去就配合它。
4.2 容器网络排障的标准流程
容器网络出问题,我的排查顺序几乎固定:先确认容器是否在同一个网络,再确认网络内DNS解析是否正常,最后检查端口监听状态。
第一步,查看容器所在网络:
docker inspect -f '{{.NetworkSettings.Networks}}' container-name如果两个容器不在同一网络,通信肯定不通,把它们加入同一个自定义网络即可:
docker network connect app-network container-name第二步,确认容器名解析正常。在容器内执行:
docker exec -it app-server getent hosts mysql-server如果没解析出IP,说明DNS解析链路有问题。正常情况下自定义网络会自动配置DNS,不会出现这种问题;出现的话多半是容器加入网络时出了岔子,把容器重新连接到网络就好。
第三步,检查应用端口是否真的在监听。有时容器活着,但应用进程挂了,端口自然不通。进入容器看进程状态,或者用curl测一下容器内端口,能快速定位问题是出在网络层还是应用层。这个步骤很多人会忽略,一上来就怀疑网络配置,折腾半天才发现是应用自己崩了。
4.3 关于网段冲突的特别提醒
网段冲突是我这几年实际运维中遇到的最隐蔽的网络问题。默认bridge网段172.17.0.0/16可能与公司现有网络重叠,导致从宿主机访问容器时路由混乱,表现为“有时候通,有时候不通”。
解决方法其实很简单:创建自定义网络时指定一个不容易冲突的网段:
docker network create \ --driver bridge \ --subnet=10.88.0.0/16 \ app-network这步操作花不了几秒,但能帮你避开很多诡异的路由问题。如果你发现宿主机能ping通容器IP,但业务访问时通时断,第一反应就该去查网段是否冲突,而不是盯着容器日志瞎猜。
4.4 还容易忽略的三个小问题
除了上面这些主场景,还有三个小问题我经常在项目里碰到。第一个是挂载目录存在性误判,绑定挂载时路径敲错了,Docker自动建了新目录,结果容器启动正常但看不到宿主机的文件。这种情况要对比宿主机和容器内的目录内容来排查。
第二个是卷的孤儿问题,容器删除了但卷还占着磁盘空间。尤其是日志类容器,数据量大得离谱,删容器时忘了加-v,磁盘空间不知不觉就被占满了。建议定期执行docker volume prune清理无用的卷。
第三个是tmpfs的容量上限问题。tmpfs默认占用宿主机内存的一半,如果往里写大文件,内存可能会被撑爆。使用--tmpfs /data:size=100m限制大小,避免影响宿主机其它进程。
5. 一些心得收尾
最后分享几个我长期使用下来的小建议。存储和网络这类基础能力,平时不显山不露水,出问题时才让人抓狂,所以最好一开始就按规范搭好。凡是数据,一律用卷;凡是容器间通信,一律自定义网络;凡是外部访问,一律显式声明端口映射。这三条是底线,别的可以灵活,这三条我从不在项目里妥协。
还有一个小细节:Docker命令敲多了之后,建议把常用命令写成脚本或者养成通过配置文件管理容器习惯,这样容器重建时不会漏掉卷和网络参数。我在实际项目里吃过亏——手动敲命令时忘了加--network,结果容器连不上数据库,排查了整整一个下午才发现是少了一个参数。
存储和网络是Docker的任督二脉,通了之后你会发现更多高级玩法都变得顺理成章。多动手、多模拟故障场景,比看十遍文档都管用。