diff --git a/.dockerignore b/.dockerignore index 53ca06d..a92a329 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,6 +3,7 @@ .env.* node_modules dist +backups apps/*/dist services/*/dist packages/*/dist diff --git a/.gitignore b/.gitignore index bbd338a..e4922c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,9 @@ legacy-cent-app/ node_modules/ dist/ +backups/ .env .env.* !.env.example *.log .DS_Store - diff --git a/README.md b/README.md index 6ce87a8..efdd96d 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,30 @@ docker compose --env-file .env -f compose.pro.yaml down ```bash docker compose --env-file .env -f compose.pro.yaml exec api \ - npm run user:add --workspace @cents/api -- '用户名' '至少8位的密码' + npm run user:add -- '用户名' '至少8位的密码' ``` 用户与唯一的“个人账本”会在同一个数据库事务内创建。 + +## 生产数据库备份 + +`backup` 容器每天北京时间 `03:00` 将 PostgreSQL 备份到生产 clone 目录下的 `./backups`。备份采用 PostgreSQL custom format,并在写入后校验;超过 30 天的备份会自动删除。 + +查看调度和备份日志: + +```bash +docker compose --env-file .env -f compose.pro.yaml logs -f backup +``` + +立即手动备份一次: + +```bash +docker compose --env-file .env -f compose.pro.yaml exec backup \ + /usr/local/bin/cents-backup run +``` + +备份文件位于宿主机: + +```text +./backups/cents-YYYYMMDD-HHMMSS.dump +``` diff --git a/compose.pro.yaml b/compose.pro.yaml index 413be35..64f0a2b 100644 --- a/compose.pro.yaml +++ b/compose.pro.yaml @@ -32,6 +32,30 @@ services: networks: - app + backup: + image: postgres:17-alpine + restart: unless-stopped + environment: + TZ: Asia/Shanghai + PGHOST: db + PGDATABASE: ${POSTGRES_DB:-cents} + PGUSER: ${POSTGRES_USER:-cents} + PGPASSWORD: ${POSTGRES_PASSWORD} + entrypoint: ["/usr/local/bin/cents-backup"] + volumes: + - ./backups:/backups + - ./deploy/postgres-backup.sh:/usr/local/bin/cents-backup:ro + depends_on: + db: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "pgrep crond >/dev/null"] + interval: 30s + timeout: 3s + retries: 3 + networks: + - app + web: build: context: . diff --git a/deploy/postgres-backup.sh b/deploy/postgres-backup.sh new file mode 100755 index 0000000..ace5b92 --- /dev/null +++ b/deploy/postgres-backup.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +set -eu + +backup_dir=${BACKUP_DIR:-/backups} + +run_backup() { + umask 077 + mkdir -p "$backup_dir" + + timestamp=$(date +%Y%m%d-%H%M%S) + target="$backup_dir/cents-$timestamp.dump" + temporary="$target.tmp" + trap 'rm -f "$temporary"' EXIT INT TERM + + pg_dump --format=custom --file="$temporary" + pg_restore --list "$temporary" >/dev/null + mv "$temporary" "$target" + trap - EXIT INT TERM + + find "$backup_dir" -maxdepth 1 -type f -name 'cents-*.dump' -mtime +29 -delete + printf 'Backup created: %s\n' "$target" +} + +case "${1:-cron}" in + run) + run_backup + ;; + cron) + printf '%s\n' \ + '0 3 * * * /usr/local/bin/cents-backup run >> /proc/1/fd/1 2>> /proc/1/fd/2' \ + > /etc/crontabs/root + printf 'Backup scheduler started: daily at 03:00 %s, retaining 30 days\n' "${TZ:-local time}" + exec crond -f -l 2 + ;; + *) + printf 'Unknown command: %s\n' "$1" >&2 + exit 2 + ;; +esac diff --git a/docs/账号与权限.md b/docs/账号与权限.md index e74c8e9..b445d6f 100644 --- a/docs/账号与权限.md +++ b/docs/账号与权限.md @@ -20,7 +20,7 @@ npm run user:add -- '用户名' '至少8位的密码' ```bash docker compose -f compose.pro.yaml exec api \ - npm run user:add --workspace @cents/api -- '用户名' '至少8位的密码' + npm run user:add -- '用户名' '至少8位的密码' ``` 用户与个人账本在同一数据库事务内创建;用户名已存在或任一步失败时都不会留下半成品数据。