Reading
SQLite 正在服务线上,怎么备份才不丢数据
直接 cp 一个正在写入的 SQLite 数据库,等于赌备份时刻恰好没有事务。用 .backup 或 VACUUM INTO 做一致性快照,再配上完整性校验和异地同步。

适用场景:线上跑着一个 SQLite 的小服务(比如这个博客本身),想要可靠的定时备份。直接
cp数据库文件看起来能用,但在有写入的瞬间拷贝可能得到一份损坏的副本——这篇讲怎么备得对、怎么验证备份真的能恢复。
背景和思路
这个博客的数据层就是一个 qingsong-notes.db 文件,跑在服务器的 Docker volume 里。文件小、依赖少,是 SQLite 的典型舒适区,但备份策略我一开始就想错了:cron 里写一行 cp。
问题出在 SQLite 的写入机制上。开启 WAL 模式后,数据同时存在于 .db、.db-wal、.db-shm 三个文件里;即使是回滚日志模式,写事务进行到一半时拷走的文件也可能是不一致的。cp 一个正在被写入的数据库,等于赌备份时刻恰好没有事务。
SQLite 官方给了两条正路:.backup 命令和 VACUUM INTO,两者都会在数据库层面做一致性快照,不怕并发写入。
方法一:sqlite3 .backup
sqlite3 /app/data/qingsong-notes.db ".backup '/backup/qingsong-notes-$(date +%F).db'"
.backup 使用 SQLite 的在线备份 API,逐页复制并自动处理期间的写入,产出的是一个完整独立的数据库文件。这是最通用的方式。
方法二:VACUUM INTO(顺便瘦身)
VACUUM INTO '/backup/qingsong-notes-2026-07-10.db';
sqlite3 /app/data/qingsong-notes.db "VACUUM INTO '/backup/qingsong-notes-$(date +%F).db'"
VACUUM INTO 在导出的同时做碎片整理,备份文件通常比原库更小。代价是比 .backup 更吃 IO,库大的时候注意错峰。我的库只有几 MB,直接选它。
注意:目标文件必须不存在,否则会报错,脚本里日期戳正好规避了这一点。
放进容器环境里
博客跑在 Docker 里,宿主机 cron 通过 docker compose exec 进容器执行:
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR=/opt/qingsong-notes/backups
STAMP=$(date +%F-%H%M)
mkdir -p "$BACKUP_DIR"
docker compose -f /opt/qingsong-notes/docker-compose.yml exec -T qingsong-notes \
sh -lc "apk add --no-cache sqlite >/dev/null 2>&1 || true; \
sqlite3 /app/data/qingsong-notes.db \"VACUUM INTO '/app/data/backup-tmp.db'\""
docker compose -f /opt/qingsong-notes/docker-compose.yml cp \
qingsong-notes:/app/data/backup-tmp.db "$BACKUP_DIR/qingsong-notes-$STAMP.db"
docker compose -f /opt/qingsong-notes/docker-compose.yml exec -T qingsong-notes \
rm -f /app/data/backup-tmp.db
# 只保留最近 30 份
ls -t "$BACKUP_DIR"/qingsong-notes-*.db | tail -n +31 | xargs -r rm -f
配上 crontab,每天凌晨四点执行:
0 4 * * * /opt/qingsong-notes/backup.sh >> /var/log/qingsong-backup.log 2>&1
如果容器镜像里没有 sqlite3 命令,更干净的做法是在 Dockerfile 里装好,而不是像上面那样运行时补装。
备份必须验证,否则等于没备
备份最大的谎言是「文件存在」。每次备份后至少做完整性检查:
sqlite3 "$BACKUP_DIR/qingsong-notes-$STAMP.db" "PRAGMA integrity_check;"
输出必须是一行 ok。可以把这一步加进脚本,非 ok 就报警(发邮件或者打一条日志再 exit 1 让 cron 通知你)。
更进一步,每季度做一次恢复演练:把备份文件放到本地项目里跑起来,确认文章、标签、媒体记录都在。媒体文件(图片)不在数据库里,要和 media/ 目录一起备份,只备数据库会出现文章在、图挂了的情况——这个坑我在部署脚本里就踩过一次。
异地:别把备份和数据放同一块盘
服务器磁盘挂了,本机备份一起陪葬。最简单的异地方案是 rsync 到另一台机器或本地 NAS:
rsync -az --delete root@101.37.21.147:/opt/qingsong-notes/backups/ ~/backups/qingsong-notes/
想要更实时的连续备份,可以看一眼 Litestream(https://litestream.io/),它以 sidecar 方式把 WAL 持续同步到对象存储,恢复点可以精确到秒级。对个人博客来说每日快照够用了,但知道这个工具的存在很值得。
收尾检查
- 备份用
.backup或VACUUM INTO,绝不直接cp在线数据库。 - 每份备份跑过
PRAGMA integrity_check且结果为 ok。 - 媒体目录和数据库一起备份。
- 至少一份备份在另一台物理机器上。
- 真正做过一次恢复演练,而不是相信备份「应该没问题」。


