“Docker 磁盘空间又满了,把镜像目录迁到新盘,改一下 daemon.json 不就行了?”这种话我听过太多次,说实话每次听完都会替对方的镜像捏一把汗。改配置只是告诉 Docker 今后把数据写到哪,并不会帮你把旧数据搬过去;尤其是到了 Docker 29.x 这一代,引擎底层和 containerd 的存储链路绑得更深之后,单改 daemon.json 的翻车概率比早期版本高得多。我前阵子帮朋友做了一次镜像路径迁移,现场就遇到镜像列表全空、容器全部起不来、旧数据还占着原盘的三连翻车。
这篇文章就把这套流程讲透:为什么不能只改 daemon.json、迁移前要准备什么、停机到切换的每一步命令是什么、以及迁移后常见的几个故障怎么排查。适合三类人:系统盘打满准备搬家的运维、自己折腾 NAS 或开发机的朋友,还有写过 Docker 数据迁移脚本但不清楚底层路径结构的人。看完你会发现,镜像路径迁移这件事,真正的难点压根不在改配置,而在操作顺序和数据完整性。
1. 只改 daemon.json 会翻车,先搞明白 Docker 29.x 的数据到底放在哪里
1.1 Docker 29.x 的目录结构已经不是“一个大目录”这么简单
以前网络教程喜欢说:Docker 的所有数据都在 /var/lib/docker,把它搬走就完了。这句话在早期版本基本正确,但放到 Docker 29.x 这一代,各种组件已经拆得很细,目录里包括了 overlay2 存储后端、容器元数据、网络状态、命名卷、buildkit 构建缓存,甚至 containerd 的 snapshotter 数据也可能落在里面或与它紧密关联。
具体来讲,/var/lib/docker 下常见的子目录各有分工,迁移时哪一块都不能漏:
| 路径 | 存放内容 | 迁移时必须注意 |
|---|---|---|
| /var/lib/docker/overlay2 | 镜像层与容器可写层 | 硬链接非常多,rsync 必须带 -H |
| /var/lib/docker/containers | 容器配置、hostname、日志 | 不迁移会导致容器列表丢失 |
| /var/lib/docker/image | 镜像元数据、layerdb | 缺了会导致 docker images 为空 |
| /var/lib/docker/volumes | 命名卷数据 | 有状态服务的数据基本在这 |
| /var/lib/docker/network | 网络状态 | 文件小,但也不能漏 |
| /var/lib/docker/buildkit | 构建缓存 | 占空间大户,迁移前可先清理 |
这意味着你不能只盯 overlay2。很多翻车案例就是只把 overlay2 拷到了新盘,结果容器元数据丢失,docker ps -a 里什么也没有。反过来,如果 containers 和 image 目录都在,但 overlay2 里的硬链接关系没保住,镜像层一样会损坏,容器启动时各种报错轮着来。
在 29.x 版本里还有一个更隐蔽的变化:镜像存储链路里 containerd 的参与度越来越高。如果你打开过 docker info,可能会看到 Storage Driver 后面除了 overlay2,还跟着一些 containerd snapshotter 相关的字段。也就是说,Docker 29.x 里的“镜像数据”不再完全由一个目录统一管理,dockerd 和 containerd 各自维护一部分状态。此时路径迁移如果只改 daemon.json 的>sudo du -sh /var/lib/docker sudo docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" sudo df -h /var/lib/docker
第一组看目录总大小,第二组看有哪些容器在跑,第三组确认当前文件系统使用率。像 MySQL、PostgreSQL、Redis 这类有状态服务,迁移前必须安排一次平滑停机,否则直接 docker stop 也可能让数据文件处于不一致状态。正确做法是进入容器安全停库,再停止容器。
另外别忽略 Docker 自身的“隐藏占地”。常用的是 buildkit 缓存和日志文件,docker system df -v 能列得非常细。我见过很多机器镜像才 5GB,构建缓存却有 40GB。如果缓存不想要,迁移前可以先清理:docker builder prune -af && docker image prune -a。但注意,docker image prune -a 会删掉没有被容器引用的镜像,生产环境要慎重,确认没用了再执行。
2.2 新磁盘先挂载好,再谈迁移目录
把新盘插上,系统不一定自动挂载。一般流程是先用 lsblk 确认盘符,再用 mkfs.xfs 或 mkfs.ext4 做文件系统,然后创建挂载点并临时挂载:
sudo lsblk -f sudo mkfs.xfs /dev/sdb1 sudo mkdir -p /data sudo mount /dev/sdb1 /data这里有个细节:如果你计划把数据放到 /data/docker,更好的做法是把整个 /data 作为一个挂载点,而不是挂到 /data/docker 再让 Docker 自动创建。原因很简单,fstab 持久化配置时按 UUID 挂载更稳,后面系统重启不容易出错。文件系统建议优先 xfs,Docker 的 overlay2 存储驱动对 xfs 支持最成熟,而且必须确认 mount 参数开启了 ftype=1,否则 overlay2 起不来。xfs 默认一般没问题,但如果用了老参数就要注意。
挂载完成后,在 /etc/fstab 里用 UUID 写入一条记录,然后 sudo mount -a 验证。这一步很多人忽略,结果迁移成功了,机器一重启新盘没挂载,dockerd 启动失败或数据又落回系统盘,非常尴尬。
2.3 备份值得做吗?我的答案永远是要做
很多人觉得 rsync 迁移完数据还在原盘,为什么要备份?但迁移过程中,停服、拷贝、改配置、启动这一串操作,任何一个环节出错,原目录可能被覆盖或者权限被打乱。尤其是有状态服务,一个镜像层文件损坏可能不直接体现在 docker images 里,而是在容器运行很长时间后才报错。
我的习惯是,条件允许的情况下先对 /var/lib/docker 做一次完整快照或副本。云主机最好用磁盘快照,物理机就用 rsync 到另外一块离线盘。如果实在没条件,至少把数据库类的容器数据先 dump 出来,比如 MySQL 用 mysqldump,PostgreSQL 用 pg_dump。这不算浪费时间——迁移后真正给你兜底的往往不是那个“大概率没问题的流程”,而是你一开始觉得多余的安全网。
3. 正确的镜像路径迁移实操:停服、搬数据、改配置、启动验证
3.1 完整命令序列:按顺序执行,别跳步
先说完整流程,我再逐步解释每个环节为什么这么写。
# 1. 停容器 docker ps -q | xargs -r docker stop # 2. 停 Docker 服务 sudo systemctl stop docker # 3. 确认服务已停 sudo systemctl status docker # 4. 创建目标目录并用 rsync 搬迁数据 sudo mkdir -p /data/docker sudo rsync -aHAXv --numeric-ids --delete /var/lib/docker/ /data/docker/ # 5. 修改 daemon.json sudo vim /etc/docker/daemon.json # 6. 重新加载守护进程配置并启动 Docker sudo systemctl daemon-reload sudo systemctl start docker第 1 步“先停容器再停服务”很重要。如果你直接 systemctl stop docker,某些容器可能处于运行中被强制杀掉,对一般应用问题不大,但对 MySQL、Redis、ES 这类写入服务,很可能留下未落盘的脏数据。docker stop 会给容器一个优雅退出信号,应用收到后还能自行处理收尾。
第 4 步的 rsync 是整次迁移的核心,参数不是随便加的:
- -a 归档模式,保留权限、时间戳、符号链接
- -H 保留硬链接,这一条对 overlay2 格外关键,镜像层内部大量使用硬链接节省空间,丢了它镜像完整性直接毁
- -A 保留 ACL
- -X 保留扩展属性,在 SELinux 或需要 xattr 的场景不能少
- --numeric-ids 以 uid/gid 数值方式保存属主信息,避免跨环境时用户名映射错乱
- --delete 让目标目录与源完全一致,确保没有残留旧文件
注意 /var/lib/docker/ 和 /data/docker/ 两个路径结尾的斜杠不能弄错。带斜杠的源路径复制的是目录内容,不带斜杠复制的是目录本身。这一步错了,目录层级会多套一层,迁移后必定出问题。
3.2 修改 daemon.json 的正确姿势:合并而不是覆盖
daemon.json 不是给你新建一个就完事的,很多机器上原来已经配了镜像加速、日志、exec-opts 等。我的做法是先把旧文件内容打出来:
cat /etc/docker/daemon.json假设原内容只有镜像加速配置,那么新文件应该保留它,加上>{ "registry-mirrors": ["https://docker.example.com"], "data-root": "/data/docker" }
别用重定向把整个文件覆盖成只有>sudo python3 -m json.tool /etc/docker/daemon.json
这行命令能提前发现逗号多写、引号错配这类低级错误。另外要提醒一点:语法校验过了,不代表字段名一定对。如果 daemon.json 里把>sudo docker info --format '{{.DockerRootDir}}' sudo docker images sudo docker ps -a
第一条命令直接确认当前 root dir 是否变成 /data/docker。第二条看镜像列表数量和迁移前是否一致,第三条看所有容器是否还在且状态正常。如果容器状态是 Exited,就手动启动看日志:
sudo docker start 容器名 sudo docker logs --tail 50 容器名有时容器启动失败不是数据迁移问题,而是容器依赖的外部卷或网络在迁移时没同步;这时 docker inspect 可以看到挂在哪个路径,逐个补齐即可。我的经验是,在一个状态正常的 Docker 环境里搬数据,成功标志不是 docker service 起来了,而是所有镜像都在,所有容器的状态和迁移前完全一致。
3.4 旧数据什么时候删?让它多活一阵
迁移完成后,旧目录 /var/lib/docker 仍然占着原来的磁盘空间。我的建议是别急着删,至少保留一个完整的业务验证周期,比如一个星期。确认所有容器运行正常、日志没有异常、数据库备份恢复验证过之后,再执行:
sudo rm -rf /var/lib/docker有些同学图省事,用 mv /var/lib/docker /tmp/old-docker 来“删除”,但同一文件系统内 mv 是改个名字很快,跨文件系统 mv 会变成全量复制,等于把数据又写了一遍,反而拖慢时间。直接 rm -rf 在处理大量文件时反而干净利落。如果迁移后磁盘空间依然紧张,先用 df -h 确认旧目录已经释放,再检查是不是系统盘里还有别的日志在长胖。
4. 迁移后的高频故障:每一条我都实际踩过
4.1 Docker 启动报错,graphdriver 初始化失败
迁移后 systemctl start docker 直接失败,journalctl -u docker 里出现类似 error initializing graphdriver: failed to read the metadata 的报错,大概率是新目录权限不对,或者父目录没有挂载好。排查步骤:
- 检查 /data 是否真的挂载成了新盘:mount | grep /data
- 检查目录属主是否为 root:ls -ld /data/docker
- 如果有 SELinux,检查上下文:ls -Zd /data/docker,必要时执行 restorecon -Rv /data/docker
很多人以为 Docker 会自己创建目录,于是只 mkdir 了外层,结果目录属主和权限被某步操作改乱。Docker 对数据目录的权限要求是 root 所有,目录本身 700 或 755 都行,但父目录不能有奇怪的写权限设置。这套问题在云主机上还比较好排查,到了 NAS 或 DIY 服务器上,经常遇到挂载时以 root 执行,但 rsync 时用了普通用户,属主错位,启动时自然读不到。
4.2 改了配置重启后,docker images 是空的
这几乎是镜像路径迁移最经典的翻车现场。原因无非两种:第一种是根本改完配置没迁移数据,第二种是迁移了但只搬了部分目录。判断方法很简单:
sudo du -sh /data/docker /var/lib/docker如果 /data/docker 只有几 MB,而 /var/lib/docker 还有十几个 GB,那就是没搬完。如果两边大小接近,再去 /data/docker 里看 overlay2 和 image 目录是否存在,结构是否完整。缺 overlay2 就是文件系统层没搬上去,缺 image 则是运行时元数据没带过来。补一次完整 rsync 通常能解决。
还有一类 29.x 特有的情况:你已经设置了>docker inspect 容器名 | grep -A10 Mounts
如果是命名卷(volume)路径不见了,去 /data/docker/volumes 下找对应目录。如果是 bind mount 指向宿主机某个路径,那和 Docker 数据迁移无关,是宿主机路径本身的问题。对于 overlay2 层的文件完整性,可以用 docker diff 或 docker export 导出容器文件系统做健康检查。
修复权限问题,最常见的是 SELinux 下执行 restorecon,或者临时把目标挂载点上下文设成 docker 专用。等容器能稳定启动再看日志,一条条解决,切忌一上来就删容器重建,因为容器配置里的环境变量、端口映射、restart 策略可能没有记录,重建容易丢东西。
4.4 系统重启后 Docker 又启动失败,镜像数据“丢”了
有一种很隐蔽的情况:迁移当时一切正常,但服务器重启后 Docker 启动失败,或者 /data/docker 目录变成空目录。十有八九是 fstab 没写好,导致新盘根本没挂载,Docker 启动时把 /data 当成了一个普通目录去初始化,甚至可能建了新的空目录结构。
检查方法:
cat /etc/fstab | grep /data mount -a df -h | grep /datafstab 里挂载新盘时,一定要用设备的 UUID 而不是设备名,比如 /dev/sdb1。原因很简单,设备名在服务器重启后可能因为磁盘顺序变化而改变,UUID 则跟文件系统绑定,永远一致。我一般这样写:
UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx /data xfs defaults 0 2最后两个数字里,一个是 dump 备份标志,0 表示不备份;一个是 fsck 检查顺序,2 表示在根文件系统之后检查。如果这行写错,轻则重启后数据不挂载,重则直接卡在开机检查阶段,所以写完后务必 sudo mount -a 实测一次。
4.5 迁移后磁盘空间还是满的:问题不一定在路径
迁移完数据到 /data/docker,系统盘空间却还是没有显著释放,这不是迁移失败,大概率有两个原因。一是旧目录 /var/lib/docker 还没删除,自然占着空间;二是旧目录删除后空间没有释放,说明有进程还持有已删除文件的文件句柄。后者在长时间运行的容器环境里尤其常见。
排查方法:
sudo lsof +L1 | grep docker sudo docker system df如果 lsof 里列出了 docker 相关进程持有 deleted 文件,重启对应容器或重启 docker 就能释放。如果 docker system df 显示 build cache 很大,说明 buildkit 缓存是主要占用,迁移前没清理干净,迁移后可以安全地执行 docker builder prune -af 删掉未使用的构建缓存。别一看到磁盘满就琢磨着再迁移一次,很多问题不是路径不对,是数据本身太大。
4.6 故障速查表
| 症状 | 最可能原因 | 快速处理 |
|---|---|---|
| Docker 启动失败,报 graphdriver 初始化错误 | 新目录权限不对,或 SELinux 上下文异常 | 检查挂载与目录属主,执行 restorecon |
| docker images 为空 | 没迁移数据,或迁移不完整 | 对比两个目录大小,补跑完整 rsync |
| 容器启动即退出,报找不到文件 | rsync 未保留硬链接/扩展属性 | 确认使用 -aHAX 参数,重新迁移 |
| 重启后数据“消失” | fstab 未配置,或设备名冲突 | 改用 UUID 写 fstab,mount -a 验证 |
| 系统盘空间不释放 | 旧目录未删,或进程持有已删除文件句柄 | 删除旧目录,检查 lsof 并重启 Docker |
5. 不改 daemon.json 也能完成的迁移方案:符号链接与挂载点
5.1 符号链接方案:适合不想动配置文件的环境
如果 daemon.json 里有很多历史遗留配置,或者你怕动配置引发风险,可以换一种不碰配置文件的思路:直接把 /var/lib/docker 这个默认路径做成软链,指向新盘目录。
操作流程:
sudo systemctl stop docker sudo mv /var/lib/docker /data/docker sudo ln -s /data/docker /var/lib/docker sudo systemctl start docker注意这里我用的是 mv 而不是 rsync,因为步骤虽然少,但本质上是把整个目录挪到了新盘。mv 在同一文件系统才是真移动,跨文件系统其实还是要复制,时间长短取决于数据量。如果数据量特别大,我更推荐先 rsync 搬过去再改符号链接,这样在切软链之前不会让原目录处于不可用状态。
这个方案的好处是,任何依赖默认路径的工具、脚本、监控 Agent 不用改;docker 读取 root dir 时依然是 /var/lib/docker,但真实落盘已经在新盘。坏处是软链本身会带来一点心智负担,以后排查问题看到软链要能认出来。另外如果服务器上有其他进程也在管理 /var/lib 目录,需要警惕路径冲突。
5.2 直接挂载方案:从起点上杜绝再次写满系统盘
如果你的新盘本身就是用来装 Docker 数据的一块独立盘,最理想的做法不是迁目录,而是把整块盘挂到 Docker 默认的根目录上:
sudo systemctl stop docker sudo mkdir -p /var/lib/docker sudo mount /dev/sdb1 /var/lib/docker在 fstab 里写好持久化配置后,Docker 的数据天然落在新盘,根本不涉及 daemon.json 的修改。这种方案在全新部署的机器上非常干净,镜像、容器、卷都落在独立分区,系统盘再大也不会影响 docker 写数据。缺点是需要一开始就规划好,如果 Docker 已经在跑了,就要先迁移数据再挂盘。
迁移路径这件事,很多人第一反应是改配置,但其实更根本的做法是让数据落在正确的位置。符号链接和直接挂载的本质都是改数据落点,而 daemon.json 只是换了一种表达方式。没有哪一种方案绝对最优,看你的环境约束:配置洁癖选挂载,历史包袱选软链,只有确实需要数据在别处但又不方便改挂载点时,才推荐 daemon.json + rsync 的组合。
我个人在实际操作中最常用的组合是 rsync 搬迁 + daemon.json 切换,因为这套方式可以保留新老目录共存,发现问题随时回滚;把 daemon.json 改回旧路径、把>
小白也能入局!抓住AI风口,高薪逆袭就在眼前!
AI已全面渗透工作与生活,行业大爆发,岗位需求激增。普通人入局AI无需硬核技术门槛,通过AI大模型应用开发可实现高薪逆袭。风口窗口期有限,趁早入局才是硬道理,主动拥抱AI,紧跟时代,稳住未来。 大…
SSM框架员工信息管理系统实战:从数据库建模到部署全解析
简介:面向Java开发学习者的毕业设计/课程设计源码资源,基于SSM(SpringSpringMVCMyBatis)JSPMySQL实现龙腾公司员工信息管理系统,覆盖员工、部门、职位、薪资、培训、考勤六大业务模块,包含前后端完整源码、…
SpringBoot在医疗就诊平台中的高效实践与优化
1. 项目概述:医疗就诊平台的SpringBoot实践医疗信息化建设正在经历从传统HIS系统向互联网化平台转型的关键阶段。去年参与某三甲医院互联网医院项目时,我们团队基于SpringBoot重构的预约挂号模块,将平均响应时间从原来的800ms降低到120ms。这…
VSCode + Qt 开发环境搭建指南:从配置到调试全流程
/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …
AI Agent开发实战:从执行流到生产交付的工程化路径
/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …
本体测试十大常见错误与根因排查指南
1. 本体测试不是“跑个脚本就完事”,而是对知识结构根基的体检“本体测试”这个词,最近在知识图谱、语义建模、智能问答系统和企业级数据治理团队的周会上出现频率直线上升。但很多人一听到“测试”,下意识就想到接口测试、UI自动化或者单元测…