Dokploy 自带 PostgreSQL 备份局限与扩展
一开始我用的是 Dokploy 自带的数据库备份。它很方便,点几下就能做定时备份,也能保留最近几份。但是当数据库逐渐变大之后,我发现了一些问题。
这不是说 Dokploy 自带备份没用。它作为小库的兜底、临时导出、人工恢复很方便。但如果数据库已经到几十 GB,那靠 dump 就并不是一个好选择。
最终我的方案
PostgreSQL -> archive_mode + WAL archive -> pgBackRest -> S3-compatible object storage, here using Cloudflare R2备份策略大概是:
每周一次 full backup每天一次 differential backup持续归档 WAL每天做 pgBackRest check保留多个 full backup问题是怎么发现的
最开始的信号很简单:
我的监控一直发现pg数据库一直处于高压状态,我寻思我的流量应该不至于能让数据库这么高负载,于是赶紧查看,由于之前也没怎么管过,一直用的dokploy原生的备份。看了眼并没有数据库本身的异常,我发现有个服务一直在压缩数据库,进而联想到了备份,最终终于找到了原因。

当前的数据库 dump 已经接近 10GB,生产 PostgreSQL 物理数据目录接近 50GB。
Dokploy 自带 dump 备份的问题
Dokploy 的自带数据库备份优点是简单:配置计划任务、保留份数。
但它作为生产 PostgreSQL 的主备份,有几个明显短板。
1. 它主要是逻辑备份
逻辑备份通常是 pg_dump 一类思路。它适合:
- 小数据库;
- 迁移表结构和数据;
- 临时导出;
- 做一份额外保险。
但库大了以后,逻辑备份的问题会越来越明显:
- dump 过程耗时变长;
- restore 更慢;
- 恢复时需要重建索引;
- 不适合频繁恢复演练;
- 不天然支持按时间点恢复。
如果业务写入比较频繁,单纯一天一次 dump 的恢复点也比较粗。比如凌晨 0 点备份,晚上 23 点出事故,那这中间的数据就很难恢复。你像如果我的库规模比较大,50G,我之前设置的每隔1H一次,保留最新的24份,导致每一个小时里40min都处于pg_dump状态,此时压力非常大,几乎只有20min正常。
2. 没有连续 WAL 归档
PostgreSQL 真正可靠的生产备份,一般不能只看全量备份,还要看 WAL。
WAL 可以理解成 PostgreSQL 的变更日志。只要你有一个基础备份,再配合之后的 WAL,就有机会把数据库恢复到某个时间点。
只做 dump 的话,通常就只能恢复到上一次备份的时间点
而做了 WAL 归档后,恢复目标就可以变成:
某个 full/diff backup + 后续 WAL这就是 RPO 的差别。
3. 恢复不可控
备份没验证过,基本等于没备份。
Dokploy 自带 dump 对小库还好。库到了几十 GB 后,恢复时长、索引重建、停机窗口都会变成问题。
解决方案:pgBackRest + 对象存储
我选择 pgBackRest。
原因很简单:数据库备份不是适合发明轮子的地方,这应该是认可度比较高的方案。
能提供:
- full backup;
- differential backup;
- incremental backup;
- WAL archive push;
- S3-compatible 存储;
- 加密;
- retention 策略;
check;info;- restore。
这正好覆盖我需要的核心能力。
部署思路
我的 PostgreSQL 是 Dokploy / Docker Swarm 管理的服务,所以整体思路是:
- 做一个带 pgBackRest 的 PostgreSQL 镜像;
- 保留原来的 PostgreSQL data volume;
- 用 Docker secret 挂载 pgBackRest 配置;
- 开启 PostgreSQL WAL archive;
- 用 systemd timer 调度 full/diff/check;
- 把备份写到 R2;
- 做独立恢复验证。
pgBackRest 配置示例
下面是结构示例,敏感信息全部用占位符。
[global]repo1-type=s3repo1-s3-endpoint=<s3-compatible-endpoint>repo1-s3-region=<region>repo1-s3-bucket=<bucket>repo1-s3-key=<access-key-id>repo1-s3-key-secret=<secret-access-key>repo1-path=/postgres-backuprepo1-cipher-type=aes-256-cbcrepo1-cipher-pass=<long-random-passphrase>
repo1-retention-full=4repo1-retention-full-type=countrepo1-retention-diff=14
process-max=2start-fast=ylog-level-console=infolog-level-file=info
[main]pg1-path=/var/lib/postgresql/dataPostgreSQL 开启 WAL archive
archive_mode = onarchive_command = 'pgbackrest --config=/run/secrets/pgbackrest.conf --stanza=main archive-push %p'archive_timeout = 300swal_compression = onarchive_timeout = 300s 的意思是即使 WAL 没写满,也最多 5 分钟切一次,降低极端情况下最后一段 WAL 长时间不归档的风险。
不是越小越好。太小会制造更多 WAL 文件和请求。要结合写入量、对象存储请求成本、RPO 要求来定。
定时任务 systemd timer
频率如下:
周日凌晨 full backup周一到周六凌晨 differential backup每天早上 pgBackRest check用 systemd timer 的好处是:
- 可以看 timer 状态;
- 可以看 service 日志;
- 可以配合 wrapper;
- 比散落的 crontab 更容易排查。
wrapper 脚本里我会做几件事:
- 动态找到当前 PostgreSQL container;
- 通过 Docker secret 里的配置运行 pgBackRest;
- 用
flock防止 full、diff、check 重叠; - 把日志写到固定文件;
- 不在命令行里暴露密钥。
示意:
#!/usr/bin/env bashset -euo pipefail
backup_type="${1:-diff}"lock_file="/var/lock/postgres-pgbackrest.lock"
exec flock -n "$lock_file" bash -c ' cid=$(docker ps --filter label=com.docker.swarm.service.name=<postgres-service-name> -q | head -n1) if [ -z "$cid" ]; then echo "postgres container not found" exit 1 fi
docker exec -u postgres "$cid" \ pgbackrest --config=/run/secrets/pgbackrest.conf \ --stanza=main \ backup --type='"$backup_type"''保留策略怎么估算
我当时的规模大概是:
PostgreSQL 物理库:49.6GBpgBackRest full repo size:11.5GB如果保留 4 份 full backup,那么只算 full:
11.5GB * 4 = 46GB然后再加:
- 每天 differential backup;
- 持续 WAL archive;
- 对象存储自身版本或生命周期策略;
- 未来写入增长。
所以实际预估不能只看 full。一个比较保守的估算方式是:
低写入:60-80GB中等写入:80-150GB高写入:150GB+真正准确的方式是等几天自动 diff 跑完,看 pgBackRest info 和对象存储增长。
pgbackrest --config=/run/secrets/pgbackrest.conf --stanza=main info顺手做的 PostgreSQL 调优
既然开启 WAL archive 和备份需要维护窗口,我也顺手调整了一些 PostgreSQL 参数。
示例:
shared_buffers = 8GBeffective_cache_size = 32GBmax_wal_size = 16GBmin_wal_size = 2GBcheckpoint_timeout = 15mincheckpoint_completion_target = 0.9wal_compression = ontrack_io_timing = onlog_checkpoints = onidle_in_transaction_session_timeout = 15min这些参数不是通用答案。它们要根据机器内存、业务写入、连接数、磁盘性能调整。(不要照抄,可以用ai帮调)
这里的目标是:
- 降低频繁 checkpoint 带来的写放大;
- 控制 WAL 体积;
- 打开 IO timing,方便后续分析慢查询;
- 记录 checkpoint;
- 避免长时间 idle transaction 阻塞 vacuum。
验证
这一步最重要。
恢复流程:
- 创建一个独立目录;
- 挂载同一个 pgBackRest 配置 secret;
pgbackrest restore到这个目录;- 启动一个临时 PostgreSQL;
- 只监听容器内部地址或本机地址;
- 执行只读查询;
- 删除临时服务。
验证时可以查:
select pg_is_in_recovery();select count(*) from pg_database;show server_version_num;我当时验证结果大概是:
restore command end: completed successfullydatabase system is ready to accept read-only connectionspg_is_in_recovery(): truedatabase count: 4server_version_num: 180003这一步完成以后,我才认为“这份备份可以恢复”。
日常检查
pgBackRest 信息
pgbackrest --config=/run/secrets/pgbackrest.conf --stanza=main info看:
- full/diff 是否按计划生成;
- repo size;
- WAL archive 范围;
- 是否有 error。
WAL 归档健康
select archived_count, failed_count, last_archived_wal, last_archived_time, last_failed_wal, last_failed_timefrom pg_stat_archiver;failed_count 不应该持续增加。
pg_wal 大小
select pg_size_pretty(sum(size)) as pg_wal_sizefrom pg_ls_waldir();如果 pg_wal 不断膨胀,要优先查 archive 是否失败、replication slot 是否卡住、checkpoint 是否异常。
定时器状态
systemctl list-timers --all "postgres-pgbackrest*" --no-pagersystemctl status postgres-pgbackrest-diff.timer postgres-pgbackrest-full.timer postgres-pgbackrest-check.timer --no-pagerPostgreSQL 是否有 pending restart
select name, setting, pending_restartfrom pg_settingswhere pending_restart = trueorder by name;最后怎么处理 Dokploy 自带备份
我的建议不是立刻关掉 Dokploy dump。
在过渡期,Dokploy dump 仍然有价值。它可以作为逻辑层面的第二保险。