☰
Docker服务正常但镜像容器全消失?一文搞懂数据盘挂载排查与恢复
2026/10/20 22:47:56 网站建设 项目流程

早上刚到工位,群里就有同事喊:服务器重启之后,docker 里啥都没了。我第一反应是“镜像丢了?”赶紧上去看,systemctl status docker明明是active (running),再敲docker ps -a和docker images,输出却是干干净净的一片空白。这个场景说实话并不罕见,但每次遇到都相当吓人——因为服务状态看着完全正常,数据却像蒸发了一样。

这篇文章就把这次排错从头到尾拆开讲清楚:为什么 docker 服务运行正常、但镜像容器列表为空,为什么偏偏在系统重启后复现,以及最终如何定位、恢复和防复发。如果你也遇到类似的“假正常”现象,不用慌,大部分情况下数据其实还在,只是 daemon 在错误的时间点里看到了一个错误的目录。下面按我当时的排查链路一步步说。

1. 现象复盘:服务明明是 active,为什么镜像和容器全没了

1.1 一个反直觉的事实:服务正常不等于数据正常

先说最容易被忽略的点:systemd 报出来的active (running),只代表 docker 守护进程这个进程活着、没有退出,不代表它成功加载了你原来的镜像和容器数据。

docker daemon 启动时真正做的事情,是去它认为的“数据根目录”里找/var/lib/docker下的各子目录,比如overlay2、containers、image、networks。如果这个目录存在但里面是空的,daemon 会非常自然地认为这就是一台新装好的机器,直接在里面初始化一套全新的空目录结构,然后对外宣告自己启动成功。

类比一下:你出差回来走进一个房间,发现房间里家具全没了,你不会跟酒店前台说“我之前的行李是不是寄存在这儿了”,你只会默认自己住进了一间新房。docker 也一样,目录空着,它不会报错,它只会当作“新环境”处理。

这个反直觉点,是整个排查链路的核心。后面所有步骤,本质上都是在回答一个问题:daemon 认为的数据目录,和旧数据真正存放的位置,是不是分离了。

1.2 空列表的几类常见原因,先在心里画一张地图

接到“列表为空”的反馈时,我脑子里通常会给可能的原因分个类,避免像无头苍蝇一样乱试。大致是这么几类:

可能原因表现特征数据是否真的丢失
客户端环境变量或 context 指向了别的 daemondocker ps为空,但docker context show指的不是本地 socket没有丢,只是“看错对象”
daemon 实际使用的>docker context show echo $DOCKER_HOST docker version

docker context show会告诉你当前客户端默认连接的是哪个 daemon。正常情况下应该指向default,对应的 socket 是/var/run/docker.sock。如果这里显示的是某个远程上下文,或者DOCKER_HOST环境变量指向了别的地址,那“列表为空”就完全合理了——因为你压根不在看本地这台机器的数据。

这一步在“重启后复现”的场景里有特殊意义:服务器重启后,登录 shell 可能会重新加载/etc/profile、~/.bashrc里的环境变量。如果曾经有人为了测试往里面写过DOCKER_HOST,重启后这个变量会再次生效,把 docker 命令“偷渡”到别的 daemon 上。当时我验证的结果是 context 正常、环境变量为空,于是排除了这一层,继续往下查。

2.2 第二步:用 docker info 拿到 daemon 真正的工作目录

继续执行:

docker info --format '{{.DockerRootDir}}'

输出结果需要仔细看。我当时第一次看的时候,发现Docker Root Dir指向的路径并不是我记忆中存放镜像的位置。这就是一个非常重要的“跑偏信号”。

这里要强调一句:docker info显示的路径,是当前 daemon 进程实际在用的数据根目录,而不是你“以为”它在用的目录。如果这台机器上曾经用-g参数或daemon.json里的>docker info | grep -E 'Storage Driver|Docker Root Dir' sudo journalctl -u docker --since "30 minutes ago" | grep -iE 'error|warning'

如果 journal 里出现no space left on device或者failed to mount overlay之类的记录,那问题更偏向存储驱动或磁盘空间。但我看到的日志基本干净,这就把方向进一步推向“空目录初始化”,而不是“存储驱动坏了”。

2.3 第三步:查 daemon.json 和 systemd 单元文件,搞清“谁在覆盖谁”

docker daemon 最终使用的参数,来源有多个:

  • 编译进二进制的默认值;
  • /etc/docker/daemon.json里的配置;
  • systemd 单元文件ExecStart行里追加的 flag,例如-g /data/docker。

参数优先级上,systemd 单元里显式追加的参数通常比daemon.json更“硬”,而且如果两个地方都写了>sudo systemctl cat docker cat /etc/docker/daemon.json

systemctl cat展示的是单元文件最终合并后的完整视图,这个很关键。如果你之前通过/etc/systemd/system/docker.service.d/下的 override 文件追加过参数,只看/lib/systemd/system/docker.service是看不到的。而daemon.json的读取则是在守护进程启动时执行的,两者叠加逻辑如果理不清,很容易漏掉真正的配置源头。

我当时的检查结果:daemon.json里确实配置了"data-root": "/data/docker",systemd 单元文件也没有额外追加-g参数。理论上 daemon 应该用/data/docker作为数据根目录。那问题会不会出在/data/docker本身?带着这个疑问,我进入了下一环节。

3. 根因锁定:重启之后,数据目录的挂载状态变了

3.1 数据盘没有自动挂载时,系统启动后发生了什么

排查到这一步,怀疑对象已经很集中:/data/docker这个路径本身。我直接做了两件事:

df -h /data/docker ls -A /data/docker

第一眼看上去,/data/docker目录是存在的。但仔细看df -h的输出,它的设备列指向的是根分区/dev/sda1,而不是预期中的独立数据盘/dev/sdb1。

这个细节就是整件事的真相。

服务器上通常是这样规划的:系统盘上只放 OS,数据盘单独挂到某个路径下,docker 的>mount | grep data blkid cat /etc/fstab

mount的输出里没有/data相关的挂载记录,说明数据盘确实没有被挂上。blkid列出了设备分区的 UUID。再看/etc/fstab,里面虽然有数据盘的一行,但仔细核对后发现,这一行写的是设备名/dev/sdb1,而不是 UUID。更致命的是,这行配置位于某条被注释掉的守护进程配置之后,在某种安装工具重写 fstab 时被丢掉了,重启后系统根本没有执行挂载。

这件事给了我很深的教训:fstab 里用设备名本身就不够稳妥,因为设备名在某些环境下重启后可能发生变化;而 fstab 配置在系统启动时如果被其他管理工具重写或覆盖,更是防不胜防。正确的做法是用blkid查到的 UUID 来写挂载配置,并且把 fstab 备份到独立的位置。

由于根分区上还有一个/data目录,docker daemon 启动时并不会报“目录不存在”的错误,它只会安安静静地在空目录里初始化。整个过程没有任何异常日志,看起来就是一台新 docker 主机。这种“乐观初始化”在数据目录为空时不会做任何校验,也是这次事故最迷惑人的地方。

3.3 为什么 docker 不会主动阻止这次“带病启动”

docker daemon 启动时,对数据根目录的处理逻辑其实很简洁:目录不存在,就创建;目录存在,就直接用。它不会主动判断“这个目录里应该有东西但没有”,因为 daemon 无法知道“应该有什么”。这是设计如此,也意味着运维侧需要自己增加保护机制。

这就像一个打包好的行李箱:机场安检不会因为你的箱子太轻就拦下你,它只负责确认箱子符合登机规则。docker 也一样,它只关心目录能不能用,不关心目录里是否躺着你的旧数据。想让它“意识到”目录不对劲,必须由你在启动链路里额外设置一道闸门。

另外要提醒一句:在确认根因之前,千万不要手贱去跑docker system prune、docker volume prune这类清理命令。虽然当时 daemon 工作在一个空目录里,这些操作大概率不会真的删掉旧数据,但基于错误假设执行破坏性命令本身就是高危动作,没必要冒这个险。

4. 修复与加固:恢复正常镜像,并让重启不再是事故

4.1 恢复顺序很重要:先挂数据盘,再重启 docker

确认根因之后,恢复其实不复杂,但顺序一定不能反。我当时是这样做的:

先确认设备名。用lsblk检查磁盘和分区结构,找到对应的数据盘设备。注意不要靠“我记得是 sdb”这种直觉,一定要当场核实:

lsblk

确认设备后执行挂载:

sudo mount /dev/sdb1 /data

挂载完成后,立刻检查旧数据是否可见:

df -h /data/docker ls /data/docker/overlay2 | head

看到overlay2目录下有大量以哈希命名的子目录时,心才算放了下来——旧数据完好无损。接下来重启 docker:

sudo systemctl restart docker docker images docker ps -a

镜像列表、容器列表全部回来了,和重启前一模一样。

这里特别强调一下为什么不能反过来操作:如果 daemon 已经在空的/data/docker目录上完成了初始化并开始运行,你再把数据盘挂到/data上,底层文件系统就发生了替换,正在运行的 daemon 打开的文件句柄会指向已经“消失”的旧目录,极易引发 I/O 错误和状态错乱。所以正确顺序永远是:先让数据目录就位,再让 daemon 启动。

4.2 fstab 自动挂载的正确写法:用 UUID,加 nofail

恢复之后第一件事就是修/etc/fstab,从根本上避免重启后再犯。我当时按照这样写:

sudo blkid /dev/sdb1

输出大概是UUID="16e4f6b1-..." TYPE="ext4",然后在/etc/fstab里加一行:

UUID=16e4f6b1-....-.... /data ext4 defaults,nofail 0 2

这里有三个细节值得展开说。

第一,挂载参数里nofail很重要。它的意思是:如果这块设备在启动时不存在或挂载失败,系统不要因为挂载错误而进入 emergency 模式,也不要阻塞启动流程。对数据盘来说,nofail的好处是启动不会卡住,坏处是数据盘没上来时系统照样启动——所以它必须配合后面的“启动前检查”一起用。

第二,字段最后的0 2表示启动时是否执行 fsck 以及检查顺序。数据盘一般设成2,根分区设成1,其他设备设0。不要全写成 0,否则长期不检查文件系统,一旦断电等意外发生,恢复难度会加大。

第三,写完 fstab 后不能只是盯着看,必须验证:

sudo mount -a

这条命令会按照 fstab 尝试挂载所有配置项,如果有语法错误或字段写错,当场就会报出来。千万不要直接重启来验证,万一 fstab 写错,系统可能起不到正常状态。

为了防止系统重启后 fstab 被某些初始化工具再次覆盖,我还建议把 fstab 备份到独立目录,并且把备份操作记录进维护文档:

sudo cp /etc/fstab /var/backups/fstab.$(date +%Y%m%d%H%M)

4.3 给 docker 数据路径加一道“启动前保护闸门”

光修好 fstab 还不够,因为我见过太多“配置确实写了但就是有人或某个工具改掉”的情况。为了确保以后哪怕配置再乱,docker 也不会在错误目录上带病启动,我还做了两层保护。

第一层,在 systemd 里声明 docker 对/data的挂载依赖。执行:

sudo systemctl edit docker

在打开的 override 文件里写入:

[Unit] RequiresMountsFor=/data

保存后重载 unit:

sudo systemctl daemon-reload

RequiresMountsFor的作用很直接:systemd 在启动 docker.service 之前,会先确保/data是已挂载的文件系统;如果这个路径没有挂载,docker 服务会被阻止启动。这样就不会再出现“数据盘没挂上,daemon 在空目录里正常跑”的假象。需要重启验证一下:把数据盘卸载掉再尝试启动 docker,服务应该会失败或等待,而不是静默进入空目录。

第二层,再加一个 ExecStartPre 检查脚本,作为强校验。还是编辑 docker 的 override 文件,在[Service]段里增加一行:

[Service] ExecStartPre=-/usr/local/bin/check-docker-datadir.sh

对应脚本内容大致是:

#!/bin/bash DATA_ROOT="/data/docker" if [ ! -d "${DATA_ROOT}/overlay2" ] || [ -z "$(ls -A ${DATA_ROOT}/overlay2 2>/dev/null)" ]; then echo "ERROR: Docker data dir is empty. Refusing to start." >&2 exit 1 fi exit 0

脚本逻辑就是检查overlay2目录是否存在且非空。如果为空,说明数据目录没有就位,直接让 docker 启动失败并把错误打到日志里。注意 ExecStartPre 前面加了一个-,它的含义是允许这个前置命令失败而不阻塞服务启动。但在这个场景里,我们恰恰希望它失败时阻塞启动,所以如果加了-,反而会让保护失效。我最终选择不加-,让任何异常都变成启动失败,宁可服务起不来也不要带病运行。

加上这两层之后,重启验证一切正常:

sudo reboot

重启回来之后,docker images和docker ps -a数据完好,journalctl -u docker里也没有任何异常。至此,问题彻底解决。

5. 这次排错给后来者的几点实在建议

5.1 “服务正常数据没了”时的三条快速判断命令

如果类似的事再发生,无论是不是重启导致的,我都建议先按下面顺序跑三条命令,五分钟内圈定问题范围:

docker context show docker info --format '{{.DockerRootDir}}' df -h (上面命令显示的目录)
  • docker context show:排除“客户端看错 daemon”;
  • docker info --format '{{.DockerRootDir}}':确认 daemon 实际用的数据根目录;
  • df -h:确认这个目录到底落在哪块设备上,顺便监控目录所在分区的空间。

这三条组合起来,就能判断出“数据没丢,只是 daemon 换了个空目录”还是“数据真的出了问题”。前者是配置/挂载层的问题,后者才是 data 层需要深度检查的问题。搞清这一点,后面所有动作都不会跑偏。

5.2 平时就应养成的两件小事:快照与备份

经过这次事故,我给自己立了两条规矩。

第一,每个季度的某个固定时间,给 docker 数据目录做一次健康快照:

du -sh /data/docker ls /data/docker > /var/backups/docker-datadir-ls-$(date +%Y%m%d).txt sha256sum /etc/docker/daemon.json > /var/backups/daemon-json-hash-$(date +%Y%m%d).txt

这些文件不需要很大,它们存在的意义是:当异常发生的时候,你能快速知道正常情况下数据目录多大、子目录有哪些、daemon.json 的哈希是否变化,对比起来一目了然。

第二,任何涉及挂载、启动参数、数据路径的配置变更,都先做备份并记录变更原因。我踩过的坑是:fstab 被某个安装工具重写过,当时根本没意识到。所以现在我改/etc/fstab之前一定会cp一份到/var/backups,并且把配置变更同步到维护文档里。

最后再分享一个直觉判断:遇到这种“系统重启后服务正常但数据列表为空”的问题,先别急着去重启服务或重装环境,把 mount 状态、系统启动日志、docker 配置变更记录三者放在一起看,大多数情况下答案就在其中。这次事故本身不复杂,复杂的是病人在你面前看起来一切正常。如果你正在查同类问题,不妨先把docker info里的DockerRootDir和df -h的输出贴给身边同事,正确答案往往就在这两行输出之间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询