一次例行检查,我发现博客的数据根本不在我以为的地方。如果那天点了"升级镜像",整个站会安静地消失。
起因:一个"看起来很对"的 compose 文件
部署 Halo 博客时,我的 docker-compose.yml 是这样的:
services:
halo:
image: halohub/halo:2.21
container_name: halo
restart: unless-stopped
ports:
- "127.0.0.1:8090:8090"
environment:
HALO_INITIAL_ADMIN_EMAIL: admin@example.com
volumes:
- /opt/halo:/root/halo # 问题在这一行
看起来没问题:数据目录挂到了宿主机 /opt/halo,容器销毁数据还在。
但是它有 bug。
发现:宿主机目录是空的
一次例行检查:
$ du -sh /opt/halo/
8.0K /opt/halo/
8K——里面只有一个 compose 文件。
那数据在哪儿?
$ docker exec halo du -sh /root/.halo2
8.1M /root/.halo2
8.1M,包含 db/、themes/、plugins/。
数据在容器的可写层里,根本没落盘。
根因:挂载点写错了
Halo 2.x 的工作目录是 /root/.halo2,不是 /root/halo。
compose 里写的 - /opt/halo:/root/halo,把宿主机目录挂到了一个 Halo 压根不用的路径。而 Halo 真正读写的 /root/.halo2,没有被任何 volume 覆盖,于是落在了容器可写层。
怎么确认?看容器里的环境变量:
$ docker inspect halo --format '{{range .Config.Env}}{{println .}}{{end}}' | grep -i halo
HALO_WORK_DIR=/root/.halo2
SPRING_CONFIG_LOCATION=optional:classpath:/;optional:file:/root/.halo2/
HALO_WORK_DIR 才是真相。别信 compose 里写了什么,信容器里的环境变量。
为什么这很危险
容器可写层的生命周期等于容器的生命周期。以下操作都会静默删光数据:
docker compose down(删除容器)docker compose up -d --force-recreate- 镜像升级(本质是删旧容器、建新容器)
没有报错、没有警告,站点直接回到初始化状态:管理员账号没了、文章没了、主题设置没了。
我是因为"还没有正式内容"才逃过一劫。如果这发生在积累了三个月文章之后……
修复:四步安全迁移
第 1 步:备份(永远先备份)
docker exec halo tar czf /tmp/backup.tgz -C /root .halo2
docker cp halo:/tmp/backup.tgz /root/halo-backup-$(date +%F).tgz
第 2 步:停容器,把数据迁到宿主机
docker stop halo
docker cp halo:/root/.halo2 /opt/halo/.halo2
小技巧:
docker cp拷贝目录时,会把目录内容直接放进目标路径,不会多套一层。拷完检查ls /opt/halo/.halo2/确认结构对得上。
第 3 步:改挂载点
volumes:
- /opt/halo/.halo2:/root/.halo2 # 宿主机数据目录 → 容器工作目录
第 4 步:重建容器——注意不是 restart
docker compose down && docker compose up -d
关键点:修改 volume 挂载必须重建容器。docker restart 只是在现有容器里重启进程,挂载配置是创建时固化的,改不了。
验证:三个必须做的检查
1. 挂载点对不对
$ docker inspect halo --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}'
/opt/halo/.halo2 -> /root/.halo2
2. 数据是否真的落盘
$ ls -la /opt/halo/.halo2/db/
halo-next.mv.db
并且启动后这个文件的 mtime 应该会变——说明 Halo 正在读写挂载目录,而不是可写层。这一条最能说明问题。
3. 服务是否正常
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8090/login
返回 200,并且用原账号能登录成功——说明数据库被完整带过来了。
事后加固
在 compose 里显式声明工作目录,让"挂载点"和"应用实际使用的路径"不再可能对不上:
environment:
HALO_WORK_DIR: /root/.halo2
volumes:
- /opt/halo/.halo2:/root/.halo2
小结
这个 bug 的可怕之处在于它不报错。容器正常运行、博客访问正常,只有"数据不落盘"这一个后果——而这个后果只在容器被销毁时才显现。
排查口诀:部署任何有状态服务后,做一次"宿主机目录体检":
du -sh <宿主机挂载目录> # 应该是"像样的体积",不是几 KB
docker inspect <容器> --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}'
docker exec <容器> env | grep -i work # 应用认为自己该写哪里
三者对齐,才算真正持久化。