Appearance
PostgreSQL 自动备份方案汇总
一、当前环境
已经确认你的实际环境:
fnOS
│
│ SSH Key 免密
▼
192.168.10.20 / lsdlog.cn
│
└── Docker
│
└── Docker Swarm
│
└── Portainer Stack
│
└── family-ledger
│
└── family-ledger_postgres
│
└── PostgreSQL其中:
fnOS:作为备份任务的执行端
192.168.10.20:Docker HostDocker Host 已执行过
docker swarm initfamily-ledger:Portainer 中的 Stackfamily-ledger是 Docker Swarm StackPostgreSQL 是 Stack 中的一个 Service
PostgreSQL Service 名称为:
family-ledger_postgresPostgreSQL 数据库:
family_ledger
通过:
ssh root@lsdlog.cn "docker stack ls"确认得到:
NAME SERVICES
adminer 1
blog 1
famgrow 3
family-ledger 3
finance 6
portainer 2因此可以确定 family-ledger 是 Docker Swarm Stack。
二、为什么不把 PostgreSQL Container 名称写死
最开始考虑过:
CONTAINER="postgres"但这不适合当前环境。
因为 Swarm 中 PostgreSQL Container 可能因为:
- Stack 更新
- Service 重建
- Docker 重启
- 容器升级
导致 Container ID 发生变化。
所以采用:
shell
Stack
↓
Service
↓
动态寻找当前运行的 Container
↓
docker exec
↓
pg_dump当前使用的定位方式:
docker ps \
--filter "label=com.docker.swarm.service.name=family-ledger_postgres" \
--format "{{.ID}}" |
head -n1这会得到当前 PostgreSQL Container ID,例如:
48e62ef1dcc5所以即使以后 Container ID 变成:
a123456789脚本仍然可以正常工作。
三、SSH 不应该使用密码自动登录
自动备份不能每次要求输入:
root@192.168.10.20's password:所以采用 SSH Key 免密认证。
逻辑:
fnOS
│
└── ~/.ssh/id_ed25519
│
│ SSH Key
▼
192.168.10.20生成密钥:
ssh-keygen -t ed25519然后将:
~/.ssh/id_ed25519.pub加入 Docker Host 的:
~/.ssh/authorized_keys测试:
ssh root@192.168.10.20能够不输入密码直接登录即可。
当前实际环境已经验证 SSH 可以正常使用。
四、最终备份脚本
在 fnOS 上建立目录:
mkdir -p /vol1/backup/postgresql/family-ledger脚本路径:
/vol1/backup/postgresql/family-ledger/backupo.sh当前使用的脚本:
#!/bin/bash
set -u
# ============================================================
# PostgreSQL Backup
# fnOS -> SSH -> Docker Swarm -> PostgreSQL
# ============================================================
# ---------- Docker Host ----------
SERVER="root@192.168.10.20"
# ---------- Docker Swarm Stack ----------
STACK="family-ledger"
# ---------- PostgreSQL Service ----------
SERVICE="${STACK}_postgres"
# ---------- PostgreSQL Database ----------
DATABASE="family_ledger"
# ---------- PostgreSQL User ----------
DB_USER="postgres"
# ---------- fnOS Backup Directory ----------
BACKUP_DIR="/vol1/backup/postgresql/family-ledger"
# ---------- Retention ----------
RETENTION_DAYS=30
# ============================================================
# Variables
# ============================================================
DATE=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="${BACKUP_DIR}/${DATABASE}_${DATE}.dump"
mkdir -p "$BACKUP_DIR"
echo "============================================================"
echo "PostgreSQL Backup"
echo "============================================================"
echo "Time : $(date '+%Y-%m-%d %H:%M:%S')"
echo "Server : $SERVER"
echo "Stack : $STACK"
echo "Service : $SERVICE"
echo "Database : $DATABASE"
echo "Backup : $BACKUP_FILE"
echo "============================================================"
# ============================================================
# 1. Check SSH connection
# ============================================================
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Checking SSH connection..."
if ! ssh -o BatchMode=yes "$SERVER" "echo ok" >/dev/null 2>&1; then
echo "ERROR: SSH connection failed."
exit 1
fi
# ============================================================
# 2. Check Docker Service
# ============================================================
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Checking Docker service..."
SERVICE_EXISTS=$(ssh "$SERVER" \
"docker service ls --filter name=${SERVICE} --format '{{.Name}}'" \
| grep -Fx "$SERVICE" || true)
if [ -z "$SERVICE_EXISTS" ]; then
echo "ERROR: Docker service not found: $SERVICE"
exit 1
fi
# ============================================================
# 3. Find PostgreSQL Container
# ============================================================
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Finding PostgreSQL container..."
CONTAINER_ID=$(ssh "$SERVER" \
"docker ps \
--filter 'label=com.docker.swarm.service.name=${SERVICE}' \
--format '{{.ID}}' \
| head -n1")
if [ -z "$CONTAINER_ID" ]; then
echo "ERROR: PostgreSQL container not found."
echo "Service: $SERVICE"
exit 1
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Container: $CONTAINER_ID"
# ============================================================
# 4. Check PostgreSQL
# ============================================================
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Checking PostgreSQL..."
if ! ssh "$SERVER" \
"docker exec $CONTAINER_ID pg_isready -U '$DB_USER' -d '$DATABASE'" \
>/dev/null 2>&1; then
echo "ERROR: PostgreSQL is not ready."
exit 1
fi
# ============================================================
# 5. Execute pg_dump
# ============================================================
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting pg_dump..."
ssh "$SERVER" \
"docker exec $CONTAINER_ID \
pg_dump \
-U '$DB_USER' \
-d '$DATABASE' \
-Fc" \
> "$BACKUP_FILE"
RESULT=$?
# ============================================================
# 6. Check pg_dump result
# ============================================================
if [ $RESULT -ne 0 ]; then
echo "ERROR: pg_dump failed."
rm -f "$BACKUP_FILE"
exit 1
fi
# ============================================================
# 7. Check backup file
# ============================================================
if [ ! -s "$BACKUP_FILE" ]; then
echo "ERROR: Backup file is empty."
rm -f "$BACKUP_FILE"
exit 1
fi
FILE_SIZE=$(du -h "$BACKUP_FILE" | awk '{print $1}')
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup success."
echo "File : $BACKUP_FILE"
echo "Size : $FILE_SIZE"
# ============================================================
# 8. Delete old backups
# ============================================================
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Cleaning old backups..."
find "$BACKUP_DIR" \
-type f \
-name "${DATABASE}_*.dump" \
-mtime +${RETENTION_DAYS} \
-delete
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup completed."
exit 0五、为什么使用 pg_dump -Fc
脚本使用:
pg_dump -U postgres -d family_ledger -Fc其中:
-Fc表示 PostgreSQL Custom Archive 格式。
生成:
family_ledger_20260825_223032.dump这种格式后续可以使用:
pg_restore进行恢复。
相比直接备份 PostgreSQL Docker 的:
/var/lib/postgresql/data目录,当前这种方式更适合作为数据库逻辑备份。
六、实际执行结果
你已经成功手工执行了:
./backupo.sh输出:
============================================================
PostgreSQL Backup
============================================================
Time : 2026-08-25 22:30:32
Server : root@lsdlog.cn
Stack : family-ledger
Service : family-ledger_postgres
Database : family_ledger
Backup : /vol1/backup/postgresql/family-ledger/family_ledger_20260825_223032.dump
============================================================
[2026-08-25 22:30:32] Checking SSH connection...
[2026-08-25 22:30:33] Checking Docker service...
[2026-08-25 22:30:34] Finding PostgreSQL container...
[2026-08-25 22:30:34] Container: 48e62ef1dcc5
[2026-08-25 22:30:34] Checking PostgreSQL...
[2026-08-25 22:30:35] Starting pg_dump...
[2026-08-25 22:30:36] Backup success.
File : /vol1/backup/postgresql/family-ledger/family_ledger_20260825_223032.dump
Size : 84K
[2026-08-25 22:30:36] Cleaning old backups...
[2026-08-25 22:30:36] Backup completed.并确认:
ls得到:
backupo.sh
family_ledger_20260825_223032.dump因此以下链路已经实际验证成功:
fnOS
↓
SSH
↓
192.168.10.20
↓
Docker Swarm
↓
family-ledger_postgres Service
↓
动态获取 Container ID
↓
pg_dump
↓
备份文件写入 fnOS七、备份文件保留策略
脚本配置:
RETENTION_DAYS=30因此:
/vol1/backup/postgresql/family-ledger/
├── family_ledger_20260825_223032.dump
├── family_ledger_20260826_030000.dump
├── family_ledger_20260827_030000.dump
├── ...超过 30 天的:
family_ledger_*.dump会被:
find ... -mtime +30 -delete自动删除。
八、设置 fnOS 定时任务
你执行:
cron时出现:
cron: can't lock /var/run/crond.pid, otherpid may be 809: Resource temporarily unavailable这不是错误的备份脚本导致的。
它表示系统已经有一个 crond 在运行,PID 大概率是:
809所以不要再次执行 cron 来启动服务,也不要删除:
/var/run/crond.pid如果使用 fnOS 图形界面的定时任务功能,直接在那里创建即可。
建议:
执行频率:每天
执行时间:03:00
执行命令:
/vol1/backup/postgresql/family-ledger/backupo.sh >> /vol1/backup/postgresql/family-ledger/backup.log 2>&1也可以使用系统 crontab:
crontab -e加入:
0 3 * * * /vol1/backup/postgresql/family-ledger/backupo.sh >> /vol1/backup/postgresql/family-ledger/backup.log 2>&1检查:
crontab -l应该看到:
0 3 * * * /vol1/backup/postgresql/family-ledger/backupo.sh >> /vol1/backup/postgresql/family-ledger/backup.log 2>&1九、建议最后再做一次恢复测试
现在已经证明:
备份能够成功生成。
但生产环境还应该验证:
备份文件能够成功恢复。
因为“pg_dump 成功”并不等于“恢复一定成功”。
当前备份文件:
family_ledger_20260825_223032.dump是 -Fc 格式,因此恢复使用:
pg_restore建议第一次创建一个测试数据库:
family_ledger_test然后将 .dump 恢复进去,确认表、数据、索引等都正常。
验证通过以后,这套备份方案基本就完整了。
十、最终方案
最终可以概括为:
每天 03:00
│
▼
┌───────────────────────────────────┐
│ fnOS │
│ │
│ /vol1/backup/postgresql/ │
│ family-ledger/ │
│ │
│ backupo.sh │
│ │ │
│ │ SSH Key │
└──────────┼────────────────────────┘
│
▼
┌───────────────────────────────────┐
│ 192.168.10.20 │
│ │
│ Docker Swarm │
│ │ │
│ ▼ │
│ Stack: family-ledger │
│ │ │
│ ▼ │
│ Service: │
│ family-ledger_postgres │
│ │ │
│ ▼ │
│ 当前运行 Container │
│ │ │
│ ▼ │
│ pg_dump -Fc │
└───────────────────────────────────┘
│
│ SSH stdout
▼
┌───────────────────────────────────┐
│ fnOS │
│ │
│ family_ledger_20260825_223032.dump│
│ │
│ 保留30天 │
└───────────────────────────────────┘