Skip to content

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 Host

  • Docker Host 已执行过 docker swarm init

  • family-ledger:Portainer 中的 Stack

  • family-ledgerDocker Swarm Stack

  • PostgreSQL 是 Stack 中的一个 Service

  • PostgreSQL Service 名称为:

    family-ledger_postgres
  • PostgreSQL 数据库:

    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天                          │
└───────────────────────────────────┘

蜀ICP备2025150039号