做Docker运维或者自己折腾服务器的人,几乎都会遇到同一个问题:磁盘满了。我用Docker跑了一堆服务,镜像、容器、卷、构建缓存全堆在系统盘里,结果/分区被撑到100%,连ssh进去敲命令都卡。后来我仔细看了一眼,才发现Docker默认把所有数据都放在/var/lib/docker,也就是跟着系统盘走。系统盘一般就几十G,根本经不起几个镜像折腾。
那时候我第一反应是把整个系统盘扩容,但云服务器扩系统盘流程麻烦,还要重启实例。后来想明白,最干净利落的办法其实是把Docker的root目录整体迁走,放到数据盘或者容量更大的挂载点上去。这个文章我就把整个迁移过程、方案选型和踩坑记录完整写出来,全程基于我实际操作的经历,跟着做基本都能成。
这个内容适合谁?一类是跟我一样把Docker装在系统盘、后来被磁盘占用逼疯的人;另一类是刚装好Docker、想一开始就把数据目录规划到数据盘上的新用户。不管你是哪种,这篇都能给你一个可以直接复制的迁移路径。
1. 迁移前先搞清楚:为什么要动Docker的root目录
1.1 Docker数据都藏在哪里
Docker安装完成后,默认的数据根目录是/var/lib/docker。这个目录下面分了好几个子目录,各自存的东西不一样:
containers:运行中的容器文件系统,包括容器的可写层、配置、日志。image:镜像的分层数据,这是磁盘占用的绝对大头。volumes:Docker volume里存放的数据,数据库容器如果挂了持久化卷,数据就在这里。overlay2:容器运行时的联合文件系统层,镜像被容器引用之后,运行层也在这里。tmp、buildkit、network等:临时文件、构建缓存、网络配置。
平常我们用docker system df看到的镜像占用、容器占用、构建缓存占用,最后加起来全都落在/var/lib/docker里。系统盘如果只有40G,随便拉几个带系统镜像的依赖,再跑两三个有数据卷的数据库,磁盘瞬间就告急。
1.2 不迁移的话会出什么事
磁盘满了之后首当其冲的是构建和拉镜像。docker pull会直接报no space left on device,构建镜像时中间层写不进去也会报错。比这更麻烦的是容器的日志文件——如果你没限制日志大小,容器运行时间长了以后,日志文件会一直在/var/lib/docker/containers/里涨,直到占满磁盘。
磁盘占满还会影响整个宿主机的稳定性,因为系统临时目录、日志服务、包管理器这些都在同一个分区。我之前遇到过一次tmp目录写满,结果ssh都登录不上,只能走管理后台重启。所以Docker的root目录迁移,本质上是个磁盘空间规划问题,不是“闲着没事折腾”。
1.3 什么时候做迁移最划算
我的经验是两个最佳时间点:
- 刚装完Docker、还没拉几个镜像的时候。这时候数据量小,迁移只要同步几百MB,基本秒级完成。
- 磁盘快满但还有少量余量的时候。比如还剩几个G空间,可以完成一次数据同步。如果已经100%满了,不仅同步麻烦,连Docker本身都可能起不来。
如果你属于后者,而且空间已经一点不剩,那得先手动清一些东西腾出空间,比如docker system prune、删掉没用的镜像,再走下面的迁移流程。别想着硬迁,同步过程中需要临时空间,空间为0就是死局。
2. 三种主流的Docker root目录迁移方案对比
2.1 软链接方案:最古老,也最省事
方案思路:把/var/lib/docker整个目录移动到新位置,然后在新位置创建软链接到原路径。
systemctl stop docker mv /var/lib/docker /data/docker ln -s /data/docker /var/lib/docker systemctl start docker这个方案的好处是Docker本身根本不知道路径变了,因为/var/lib/docker还是一个有效路径,只是它指向了新位置。很多老教程用的就是这个方法。
但它有几个明显问题。第一,mv命令如果跨文件系统,实际上是先复制再删除,数据量大的时候耗时很久,而且如果在mv中途中断,两边数据状态没法保证。第二,软链接本身是个隐患,有些审计工具、备份脚本会识别出这是个链接而不是真实目录,万一哪天有人误删了软链接,Docker就直接找不到数据了。第三,如果未来要重启机器,软链接依赖的挂载点必须提前挂载好,否则Docker启动时数据还是找不到。
2.2 修改daemon.json方案:最推荐,干净利落
Docker守护进程本身支持通过配置文件修改数据根目录,这个配置就是/etc/docker/daemon.json,关键参数是>{ "data-root": "/data/docker" }
配置完成后重启Docker,新数据就全部落到新位置。这种方案的优势在于结构清晰、配置稳定,Docker原生识别新路径,没有软链接这种“中间商”。后续做备份、迁移、扩容,都只需要针对真实路径操作,不会有什么歧义。
docker-compose、docker desktop这些工具,底层的daemon配置也是走同一个机制。比如你本机装了Docker Desktop,在设置界面里其实也有一项可以调整磁盘镜像位置,本质就是改data-root。Linux服务器上没有图形界面,手动改daemon.json是最直接的方式。
2.3 直接挂载数据盘到/var/lib/docker:适合新装场景
如果你的服务器有一块独立数据盘,而且刚好没想好挂到哪,可以直接把这块盘挂到/var/lib/docker。比如:
mkfs.ext4 /dev/vdb mount /dev/vdb /var/lib/docker这样Docker默认路径不变,但底层的存储实体已经换到了新盘。这种方案的好处是路径意义清晰,/var/lib/docker看起来跟默认完全一样。缺点是只适合新装或者低负载场景——如果原目录里已经有大量数据,你得先把旧数据搬到新盘再挂载,这个操作序列和迁移一样麻烦,那就不如直接改daemon.json了。
2.4 我对三套方案的最终选择
在我自己的机器上,最终选的是修改daemon.json。原因很简单:不依赖软链接、迁移逻辑清晰、配置可持久化。就算以后换机器、换磁盘,只要复制整个data-root目录,再在新机器上改同一个配置项,数据就无缝接上了。
提示:不管选哪个方案,迁移前都建议先做一次数据完整性确认,尤其是正在运行数据库容器的机器。最理想的情况是停掉容器再迁,避免文件写入过程中出现数据不一致。
3. 动手迁移前必须确认的五件事
3.1 确认目标磁盘确实挂载了,以及文件系统类型
我们要把数据迁到哪,得先确认这个位置的真实情况。用df -h看一下:
[root@server ~]# df -h Filesystem Size Used Avail Use% Mounted on /dev/vda1 40G 38G 0G 100% / /dev/vdb 200G 10G 190G 1% /data从上面输出能看到,/已经100%满了,而/data还有190G可用。那就把目标目录定为/data/docker。
还要确认文件系统是ext4还是xfs。Docker的数据目录对文件系统类型没有强制要求,但这会影响后续的性能和配额配置,比如xfs支持pquota可以做磁盘配额。用df -T就能看到类型。
[root@server ~]# df -T /data Filesystem Type Size Used Avail Use% Mounted on /dev/vdb xfs 200G 10G 190G 1% /data3.2 确认当前Docker数据占用了多少空间
迁移前先给数据量摸个底。用du直接统计目录大小:
du -sh /var/lib/docker正常情况下这个输出会接近docker system df里的总占用。如果差异特别大,要检查一下是不是有容器日志占用了一大块,或者构建缓存占了不少。
还需要用docker system df看下Docker自己统计的各类占用:
[root@server ~]# docker system df TYPE TOTAL ACTIVE SIZE RECLAIMABLE Images 12 5 3.521GB 1.812GB (51%) Containers 8 3 220.4MB 178MB (81%) Local Volumes 5 3 1.781GB 1.287GB (72%) Build Cache 24 0 2.182GB 2.182GB这里的信息非常关键。如果Images和Build Cache占比很高,迁移后系统盘空间会直接释放一大截;如果Local Volumes占大头,说明有容器挂了很大的持久化卷,迁移后务必要验证卷数据是否完整。
3.3 确认Docker配置里有没有其他路径依赖
有些人的daemon.json里可能已经配置了graph、docker-root等旧字段。graph是老版本Docker里的参数名,新版本里已经淘汰了,统一用>setenforce 0
如果setenforce 0之后问题消失,那基本就能断定是SELinux的上下文问题。解决办法是用chcon或者semanage fcontext给新目录设置与/var/lib/docker相同的标签。
4. 完整迁移过程:从备份到验证的详细操作
4.1 停掉Docker服务和所有相关容器
迁移的第一步是停止Docker服务,这一步没得商量。为什么必须停?因为如果Docker还在运行,/var/lib/docker里的文件会不断被写入,我们同步到一半的数据就会出现不一致,拷贝完成后Docker进程认为数据还是完整的,但实际数据库文件可能已经处于中间状态。
systemctl stop docker如果你的机器上还有其他容器编排工具,比如docker-compose在跑,建议先停掉compose项目再停Docker,这样容器会有优雅终止的机会:
docker compose -f /path/to/docker-compose.yml down systemctl stop docker其实Docker服务停止后,所有容器进程也会被强制终止,相当于容器直接断电。直接停Docker的问题在于容器内的应用可能来不及做正常的退出处理,比如数据库来不及刷新缓存。所以稳妥起见,如果你有状态服务(MySQL、Redis、PostgreSQL),先停服务更稳妥。
另外,还要确认Docker服务真的停掉了,不能只看systemctl输出。因为有些环境下Docker用了socket激活或者其他守护方式挂起,直接看进程是否存在更稳妥:
ps -ef | grep dockerd systemctl status docker如果进程还在,就得kill掉残留进程再继续。
4.2 拷贝数据:推荐rsync而不是直接用mv
很多人迁移时习惯用mv /var/lib/docker /data/docker。这里有个很大的坑:如果新旧目录在同一个文件系统上,mv只是改个名字,瞬间完成;但如果跨文件系统,mv的行为其实是“复制到新位置,然后删除原位置”,和cp没有本质区别。更致命的是,mv在跨文件系统拷贝时中途出错或者被中断,是不会保证数据完整性的,它从头到尾就是一个缺乏断点续传能力的复制。
所以我强烈推荐用rsync来做这一步。它的优势是支持断点续传、进度显示、校验和对比,还可以保留权限、属主、时间戳、硬链接等属性。
同步命令:
mkdir -p /data/docker rsync -avxP --numeric-ids /var/lib/docker/ /data/docker/解释一下这些参数的含义:
-a:归档模式,保留权限、属主、时间戳、软链接等所有属性。这一步很关键,Docker对文件属主和权限是有要求的,比如很多目录和文件是root拥有,漏掉权限同步会导致容器启动异常。-v:verbose,输出详细信息,方便观察进度。-x:不跨文件系统。这个参数我特意加上,是为了防止/var/lib/docker下如果挂了其他独立挂载点,rsync会误把挂载点里的内容也同步过去。-P:等于--partial --progress,显示进度并且支持断点续传。--numeric-ids:以数字形式保留UID/GID,避免不同机器上用户映射不一致导致属主错乱。
同步过程中,如果数据量大,输出会很长,建议用screen或者tmux挂在后台跑,防止ssh断开导致rsync中断。中断了也没关系,重新执行同一个rsync命令,它只同步差异部分,不需要从头再来。
同步完成后,用以下命令做一次快速对比校验:
rsync -avxn --numeric-ids /var/lib/docker/ /data/docker/加一个-n(dry-run模拟执行)参数,如果输出为空,说明源目录和目标目录的文件完全一致,没有任何差异文件。这一步可以在不真正执行同步的情况下,检查两边是否一致。
注意:
rsync -avxn只对比文件名、大小、时间戳等元数据,没有实际对比文件内容。如果对数据完整性要求极严,可以用-c参数进行逐文件校验和对比,但是这会消耗大量时间。常规迁移用-n做元数据校验足够。
4.3 修改Docker配置文件data-root
数据拷贝完成后,修改Docker的配置文件。用vim打开/etc/docker/daemon.json:
vim /etc/docker/daemon.json如果文件本来就不存在,那就是新创建一个。一个完整的配置示例:
{ "data-root": "/data/docker", "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }这里我额外加上了日志大小限制,这是我踩过坑之后强烈建议加上的配置。容器日志如果不限制大小,长时间运行后会累积几个G甚至几十个G,把磁盘撑爆。限制到单文件10M、最多保留3个,日志轮转会自动清理旧文件,避免日志成为磁盘杀手。
改完配置文件后,先验证一下JSON格式是否正确:
python3 -m json.tool /etc/docker/daemon.jsonJSON格式出问题的话,Docker服务启动会直接报错,轻则服务起不来,重则卡在启动循环里。多花十秒钟验证一下,能省很多排查时间。
4.4 启动Docker并验证迁移结果
配置完成后启动Docker:
systemctl start docker systemctl status dockerDocker启动后,用以下命令依次验证:
docker info | grep "Docker Root Dir" docker images docker ps -adocker info输出的Docker Root Dir字段会直接显示当前数据根目录:
[root@server ~]# docker info | grep "Docker Root Dir" Docker Root Dir: /data/docker能看到/data/docker,就说明Docker已经使用新目录了。然后再用docker images看镜像列表是否完整,用docker ps -a看容器列表是否完整。之前建的容器、拉过的镜像、创建的卷,都应该原封不动地出现在列表里。
4.5 验证容器和数据卷的完整性
只看到容器列表还不够,尤其是数据库类容器,必须真正启动一次,然后检查数据内容是否正常。我以前迁移完MySQL容器后,列表里显示容器存在,但是容器启动后数据库一直连不上,最后发现是卷里的数据文件有权限问题。
验证方法其实很简单:
docker start <容器名> docker logs --tail 50 <容器名>启动后看日志有没有报错。如果是MySQL,可以进入容器执行简单的SQL查询;如果是Redis,就执行PING。确认业务数据没问题后,才算是真正迁移完成。
4.6 清理旧目录释放系统盘空间
所有验证都通过之后,旧目录/var/lib/docker里的数据就没有用了,可以清理掉释放系统盘空间。这一步同样不建议直接rm -rf旧目录,尤其是数据量很大的时候。因为删除文件本身也需要耗时,中途如果出现问题可能导致目录残留。比较稳妥的方式是先重命名再删除:
mv /var/lib/docker /var/lib/docker.bak rm -rf /var/lib/docker.bak先重命名,如果Docker运行一切正常,过几天再彻底删除备份目录。万一迁移后有什么隐藏问题,还能把旧目录改名回来,快速回滚。
提示:旧目录改名之后,建议观察至少24小时再删。迁移完短期内一切正常,不代表持久卷里的数据一定没问题,很多数据一致性问题会在容器重启或者写操作频繁时才暴露。
5. 实战记录:一台40G系统盘的服务器迁移全过程
5.1 迁移前这台机器长什么样
我有一台2C4G的云服务器,系统盘40G,数据盘200G,系统盘被Docker塞满了。df -h看过去是这样的:
[root@server ~]# df -h Filesystem Size Used Avail Use% Mounted on /dev/vda1 40G 36G 0.7G 98% / /dev/vdb 200G 30G 170G 15% /data/已经用到98%,只剩0.7G,随时可能爆。再看Docker的占用:
[root@server ~]# docker system df TYPE TOTAL ACTIVE SIZE RECLAIMABLE Images 25 18 11.2GB 5.4GB (48%) Containers 14 10 521.4MB 420.1MB (81%) Local Volumes 8 6 26.4GB 2.1GB (8%) Build Cache 12 0 3.8GB 3.8GB (100%)总量加起来已经超过40G了,但为什么实际占用是36G而不是40G?因为docker system df统计的是逻辑大小,实际磁盘块还要算上文件系统开销。反正结论是:系统盘快爆了,必须迁。
5.2 执行的完整命令序列
我把当时实际执行的命令整理成一份完整序列,供参考:
# 1. 查看当前数据量 du -sh /var/lib/docker # 2. 停掉Docker systemctl stop docker # 3. 确认Docker进程已退出 ps -ef | grep dockerd # 4. 创建目标目录 mkdir -p /data/docker # 5. 使用rsync同步数据 rsync -avxP --numeric-ids /var/lib/docker/ /data/docker/ # 6. 对比源目录和目标目录差异 rsync -avxn --numeric-ids /var/lib/docker/ /data/docker/ # 7. 备份原配置文件 cp /etc/docker/daemon.json /etc/docker/daemon.json.bak # 8. 修改配置文件 vim /etc/docker/daemon.json # 9. 校验JSON格式 python3 -m json.tool /etc/docker/daemon.json # 10. 启动Docker systemctl start docker # 11. 检查Docker根目录 docker info | grep "Docker Root Dir" # 12. 检查镜像和容器列表 docker images docker ps -a # 13. 启动几个核心容器,验证数据 docker start mysql docker logs --tail 30 mysql docker exec -it mysql mysql -uroot -p -e "show databases;" docker start redis docker exec -it redis redis-cli PING # 14. 确认一切正常后,处理旧目录 mv /var/lib/docker /var/lib/docker.bak rm -rf /var/lib/docker.bak整套流程走下来,Docker的root目录就从/var/lib/docker切到了/data/docker。系统盘从98%直接降到不到20%,数据盘多占了40G。
5.3 docker compose项目怎么处理
如果项目里有用docker compose管理的一堆服务,迁移后compose文件的配置基本不用动,因为compose文件只是定义容器结构,真正落地到磁盘的数据都在Docker root目录里。但有一点要注意:compose项目里用到的named volume,这些卷的数据存储路径在Docker root目录下的volumes文件夹里,迁移后它们会跟着一起走。启动compose项目前建议做一次docker compose config检查配置,确认没有使用宿主机绝对路径挂载。
如果你的compose文件本来就是用- ./data:/app/data这种方式挂载宿主目录,那这部分数据跟Docker root目录无关,迁移不影响。
5.4 迁移后磁盘空间有多少进账
迁移完成后,我再看df -h:
[root@server ~]# df -h Filesystem Size Used Avail Use% Mounted on /dev/vda1 40G 6.2G 31.8G 17% / /dev/vdb 200G 70G 130G 35% /data系统盘从几乎爆满降到17%,数据盘多了差不多40G的数据。后来我又写了个定时清理脚本,定时执行docker system prune -af --filter "until=168h"清理构建缓存和悬空镜像,数据盘的占用也一直保持稳定。
6. 迁移过程中最容易踩的五个坑
6.1 容器启动失败:指向卷文件权限错乱
迁移后容器启动不了,日志里报Permission denied或者单纯起不来,优先查目录权限。rsync虽然用了-a参数保留了权限,但如果你之前用的是mv跨文件系统拷贝,某些目录的权限可能变了。
排查方法:
ls -ldn /data/docker/volumes/* ls -ldn /var/lib/docker/volumes/*两边的权限和属主对比一下,如果出现不一致,用chown和chmod修正。特别是MySQL这类容器,它对数据目录的权限非常敏感,权限稍有不对就直接拒绝启动。
6.2 Docker服务起不来:检查daemon.json语法
Docker服务启动失败,最常见的原因就是daemon.json写错了。JSON文件里多了一个逗号、少了一个引号,都会导致解析失败。这时候看日志:
journalctl -u docker --no-pager | tail -50日志里会明确提示daemon.json解析错误的位置。用python3 -m json.tool验证一遍格式,正常后再重启。另外要注意,daemon.json里的路径不要加尾斜杠,比如/data/docker/和/data/docker虽然看起来差不多,但不同版本Docker对尾斜杠的处理方式不一致,统一不加尾斜杠最稳。
6.3 同步数据时占用太高,导致线上业务卡顿
如果你的机器上还有其他服务在跑,rsync同步大量数据时会占用比较高的磁盘IO和CPU。我试过一次全速同步,直接把业务服务的IOPS拉满,数据库查询延迟飙升。后来我给rsync加了限速参数,把同步时间拉长,但保证了业务不受影响:
rsync -avxP --numeric-ids --bwlimit=50000 /var/lib/docker/ /data/docker/--bwlimit=50000表示限制带宽为50MB/s,实际效果取决于磁盘性能。如果你对IO特别敏感,甚至可以把限速设成10000(10MB/s),慢慢同步,但业务不断。
6.4 磁盘空间明明够,但rsync提示空间不足
这种情况我真遇到过。目标磁盘明明还有几十G空间,但rsync同步到一半提示No space left on device。查了半天发现是目标文件系统inode不够了。
检查命令:
df -i /datadf -i看的是inode使用率。如果inode使用率是100%,即使还有剩余空间也写不了任何新文件。解决办法是格式化目标盘时加大inode密度,或者在目标盘上换一种文件系统(比如xfs),或者把数据拆分同步。如果数据已经写了一半,需要先清理掉部分文件再继续。
6.5 迁移后旧目录怎么都删不掉
rm -rf /var/lib/docker理论上很快,但如果目录里文件特别多,删除也要花很久。我试过几万个文件删了十几分钟,期间IO还被打满。更稳妥的做法是直接在系统盘上等它慢慢删,或者用ionice降低删除操作的IO优先级,避免影响其他服务。
ionice -c 3 rm -rf /var/lib/docker.bak这个命令把删除进程的IO调度级别降为idle,磁盘繁忙时会自动让出资源给其他服务。如果你的业务对IO很敏感,这个技巧很实用。
7. 迁移后如何确认一切正常:验证清单
完整跑完迁移流程后,我会按下面这个清单逐项验证,全部通过才算迁移成功:
| 检查项 | 命令 | 通过标准 |
|---|---|---|
| Docker根目录 | docker info | grep "Docker Root Dir" | 显示为新路径 |
| 镜像完整性 | docker images | 迁移前存在的镜像全部在列 |
| 容器列表 | docker ps -a | 迁移前存在的容器全部在列 |
| 容器可启动 | docker start <容器> | 容器状态变为Up |
| 卷数据 | ls /data/docker/volumes/ | 卷目录齐全,大小合理 |
| 日志无报错 | journalctl -u docker --no-pager | tail -30 | 无致命错误 |
| 磁盘空间 | df -h | 系统盘空间明显释放 |
| 数据内容 | 业务侧验证 | 数据库能查、缓存能读、服务能访问 |
这个清单每次迁移我都会执行一遍,不是为了走流程,而是因为有一次我跳过了容器内容验证,直接删了旧目录,结果有个容器的卷数据其实没同步过来,最后只能从备份里恢复。那次的教训很深刻,所以现在每一步验证我都不会省。
8. 如果迁移后想回滚怎么办
虽然改daemon.json的方案很少需要回滚,但万一新目录有问题,回滚流程也很简单:
systemctl stop docker vim /etc/docker/daemon.json # 注释掉或删除>