Merge pull request #61: infra: add PostgreSQL daily backup via pg_dump with rotation
- Add db-backup service to compose.yaml (postgres:17-alpine + cron) - Add pgbackups volume for backup storage - Add scripts/restore.sh for manual restore from latest backup - Update .env.example with BACKUP_RETENTION_DAYS and BACKUP_VOLUME_NAME - Document backup/restore flow in README Bump version -> 1.15.0 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -15,3 +15,10 @@ POSTGRES_PASSWORD=StrongPasswordForDatabase
|
||||
|
||||
# Локальный порт веб-интерфейса GM-Relay
|
||||
GMRELAY_WEB_PORT=8080
|
||||
|
||||
# === Backup ===
|
||||
# Сколько дней хранить дампы PostgreSQL (default: 7)
|
||||
BACKUP_RETENTION_DAYS=7
|
||||
|
||||
# Имя Docker volume для резервных копий БД
|
||||
BACKUP_VOLUME_NAME=game_pgbackups
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
- main
|
||||
|
||||
env:
|
||||
VERSION: 1.14.0
|
||||
VERSION: 1.15.0
|
||||
|
||||
jobs:
|
||||
# ЧАСТЬ 1: Собираем образы и кладем в Gitea (чтобы делиться с ребятами)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.14.0</Version>
|
||||
<Version>1.15.0</Version>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Проект разработан с упором на производительность, архитектуру Vertical Slice, Native AOT (для бота) и удобство развертывания с использованием .NET Aspire.
|
||||
|
||||
**Текущая версия:** `v1.14.0`.
|
||||
**Текущая версия:** `v1.15.0`.
|
||||
|
||||
---
|
||||
|
||||
@@ -105,6 +105,44 @@ docker compose up -d
|
||||
2. Создайте группу через `/newgroup`.
|
||||
3. Откройте Mini App или Web Dashboard для расширенного управления.
|
||||
|
||||
## 💾 Backup и восстановление
|
||||
|
||||
Проект включает автоматический ежедневный backup PostgreSQL через сервис `db-backup` в Docker Compose.
|
||||
|
||||
### Как это работает
|
||||
- **Каждый день в 03:00** выполняется `pg_dump` базы `gmrelay_db`.
|
||||
- Дампы сжимаются (`gzip`) и сохраняются в volume `pgbackups` (`/backups`).
|
||||
- Формат имени: `gmrelay_db_YYYYMMDD_HHMMSS.sql.gz`.
|
||||
- Ротация: по умолчанию хранятся последние **7 дней** (настраивается через `BACKUP_RETENTION_DAYS`).
|
||||
|
||||
### Проверка бэкапов
|
||||
```bash
|
||||
docker compose exec db-backup ls -la /backups
|
||||
```
|
||||
|
||||
### Ручное создание дампа
|
||||
```bash
|
||||
docker compose exec db-backup sh -c "pg_dump -h db -U gmrelay -d gmrelay_db | gzip > /backups/gmrelay_db_manual.sql.gz"
|
||||
```
|
||||
|
||||
### Восстановление из бэкапа
|
||||
```bash
|
||||
# Использовать последний автоматический бэкап
|
||||
./scripts/restore.sh
|
||||
|
||||
# Или указать конкретный файл
|
||||
./scripts/restore.sh backups/gmrelay_db_20260512_030000.sql.gz
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> Восстановление **перезаписывает текущую базу данных**. Убедитесь, что вы понимаете последствия, прежде чем запускать `restore.sh`.
|
||||
|
||||
### Переменные окружения (опциональные)
|
||||
```env
|
||||
BACKUP_RETENTION_DAYS=7
|
||||
BACKUP_VOLUME_NAME=game_pgbackups
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗂 Структура репозитория
|
||||
|
||||
+36
-2
@@ -16,8 +16,40 @@ services:
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
db-backup:
|
||||
image: postgres:17-alpine
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
POSTGRES_USER: gmrelay
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
|
||||
POSTGRES_DB: gmrelay_db
|
||||
PGPASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
|
||||
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7}
|
||||
volumes:
|
||||
- pgbackups:/backups
|
||||
networks:
|
||||
- gmrelay
|
||||
entrypoint: ["sh", "-c"]
|
||||
command:
|
||||
- |
|
||||
cat > /usr/local/bin/backup.sh << 'EOF'
|
||||
#!/bin/sh
|
||||
set -e
|
||||
TMPFILE="/tmp/backup_$$.sql"
|
||||
pg_dump -h db -U gmrelay -d gmrelay_db > "$TMPFILE"
|
||||
gzip "$TMPFILE"
|
||||
mv "$TMPFILE.gz" "/backups/gmrelay_db_$(date +%Y%m%d_%H%M%S).sql.gz"
|
||||
find /backups -name 'gmrelay_db_*.sql.gz' -type f -mtime +${BACKUP_RETENTION_DAYS} -delete
|
||||
EOF
|
||||
chmod +x /usr/local/bin/backup.sh
|
||||
echo "0 3 * * * /usr/local/bin/backup.sh" | crontab -
|
||||
crond -f
|
||||
|
||||
bot:
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-bot:1.14.0
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-bot:1.15.0
|
||||
restart: always
|
||||
depends_on:
|
||||
db:
|
||||
@@ -30,7 +62,7 @@ services:
|
||||
- gmrelay
|
||||
|
||||
web:
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-web:1.14.0
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-web:1.15.0
|
||||
restart: always
|
||||
depends_on:
|
||||
db:
|
||||
@@ -52,6 +84,8 @@ volumes:
|
||||
name: ${POSTGRES_VOLUME_NAME:-game_pgdata}
|
||||
web_keys:
|
||||
name: ${WEB_KEYS_VOLUME_NAME:-gmrelay_web_keys}
|
||||
pgbackups:
|
||||
name: ${BACKUP_VOLUME_NAME:-game_pgbackups}
|
||||
|
||||
networks:
|
||||
gmrelay:
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
# GM-Relay PostgreSQL Backup Restore Script
|
||||
# Usage: ./scripts/restore.sh [backup_file]
|
||||
# If no file is provided, uses the most recent backup.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
# Check required env
|
||||
if [ -z "${POSTGRES_PASSWORD:-}" ]; then
|
||||
if [ -f "${PROJECT_ROOT}/.env" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
set -a
|
||||
. "${PROJECT_ROOT}/.env"
|
||||
set +a
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "${POSTGRES_PASSWORD:-}" ]; then
|
||||
echo "ERROR: POSTGRES_PASSWORD is not set. Please set it in your environment or .env file."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BACKUP_DIR="${PROJECT_ROOT}/backups"
|
||||
|
||||
# Determine backup file
|
||||
if [ $# -ge 1 ]; then
|
||||
BACKUP_FILE="$1"
|
||||
else
|
||||
BACKUP_FILE=$(find "${BACKUP_DIR}" -name 'gmrelay_db_*.sql.gz' -type f -printf '%T+ %p\n' 2>/dev/null | sort -r | head -n1 | cut -d' ' -f2-)
|
||||
if [ -z "${BACKUP_FILE}" ]; then
|
||||
echo "ERROR: No backup files found in ${BACKUP_DIR}."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -f "${BACKUP_FILE}" ]; then
|
||||
echo "ERROR: Backup file not found: ${BACKUP_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=================================================="
|
||||
echo " GM-Relay PostgreSQL Restore"
|
||||
echo "=================================================="
|
||||
echo ""
|
||||
echo "Backup file: ${BACKUP_FILE}"
|
||||
echo "Database: gmrelay_db"
|
||||
echo "User: gmrelay"
|
||||
echo ""
|
||||
read -p "This will OVERWRITE the current database. Are you sure? [y/N] " CONFIRM
|
||||
|
||||
if [[ ! "${CONFIRM}" =~ ^[Yy]$ ]]; then
|
||||
echo "Restore cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Restoring database from ${BACKUP_FILE}..."
|
||||
|
||||
# Restore using docker compose exec to leverage the running postgres container
|
||||
COMPOSE_ARGS="-f ${PROJECT_ROOT}/compose.yaml"
|
||||
|
||||
docker compose ${COMPOSE_ARGS} exec -T -e PGPASSWORD="${POSTGRES_PASSWORD}" db psql \
|
||||
-U gmrelay \
|
||||
-d gmrelay_db \
|
||||
-c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" 2>/dev/null || true
|
||||
|
||||
gunzip -c "${BACKUP_FILE}" | docker compose ${COMPOSE_ARGS} exec -T -e PGPASSWORD="${POSTGRES_PASSWORD}" db psql \
|
||||
-U gmrelay \
|
||||
-d gmrelay_db
|
||||
|
||||
echo ""
|
||||
echo "=================================================="
|
||||
echo " Restore completed successfully!"
|
||||
echo "=================================================="
|
||||
@@ -83,6 +83,12 @@
|
||||
"System.IO.Hashing": "10.0.3"
|
||||
}
|
||||
},
|
||||
"SecurityCodeScan.VS2019": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.6.7, )",
|
||||
"resolved": "5.6.7",
|
||||
"contentHash": "WIE9RJswdSc2j+rLz2gW6U+gMUjMHzY2j7C/CL8/R2olXNM/+twarfMnWqm+rZodDBvaYDApJyxM8mVYf9FGrQ=="
|
||||
},
|
||||
"Aspire.Hosting": {
|
||||
"type": "Transitive",
|
||||
"resolved": "13.2.1",
|
||||
|
||||
@@ -95,6 +95,12 @@
|
||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.0"
|
||||
}
|
||||
},
|
||||
"SecurityCodeScan.VS2019": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.6.7, )",
|
||||
"resolved": "5.6.7",
|
||||
"contentHash": "WIE9RJswdSc2j+rLz2gW6U+gMUjMHzY2j7C/CL8/R2olXNM/+twarfMnWqm+rZodDBvaYDApJyxM8mVYf9FGrQ=="
|
||||
},
|
||||
"Telegram.Bot": {
|
||||
"type": "Direct",
|
||||
"requested": "[22.9.5.3, )",
|
||||
|
||||
@@ -66,6 +66,12 @@
|
||||
"OpenTelemetry.Api": "[1.15.3, 2.0.0)"
|
||||
}
|
||||
},
|
||||
"SecurityCodeScan.VS2019": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.6.7, )",
|
||||
"resolved": "5.6.7",
|
||||
"contentHash": "WIE9RJswdSc2j+rLz2gW6U+gMUjMHzY2j7C/CL8/R2olXNM/+twarfMnWqm+rZodDBvaYDApJyxM8mVYf9FGrQ=="
|
||||
},
|
||||
"Microsoft.Extensions.AmbientMetadata.Application": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.2.0",
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net10.0": {}
|
||||
"net10.0": {
|
||||
"SecurityCodeScan.VS2019": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.6.7, )",
|
||||
"resolved": "5.6.7",
|
||||
"contentHash": "WIE9RJswdSc2j+rLz2gW6U+gMUjMHzY2j7C/CL8/R2olXNM/+twarfMnWqm+rZodDBvaYDApJyxM8mVYf9FGrQ=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="nav-version">v1.14.0</div>
|
||||
<div class="nav-version">v1.15.0</div>
|
||||
</div>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
|
||||
@@ -32,6 +32,12 @@
|
||||
"resolved": "10.0.2",
|
||||
"contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg=="
|
||||
},
|
||||
"SecurityCodeScan.VS2019": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.6.7, )",
|
||||
"resolved": "5.6.7",
|
||||
"contentHash": "WIE9RJswdSc2j+rLz2gW6U+gMUjMHzY2j7C/CL8/R2olXNM/+twarfMnWqm+rZodDBvaYDApJyxM8mVYf9FGrQ=="
|
||||
},
|
||||
"Telegram.Bot": {
|
||||
"type": "Direct",
|
||||
"requested": "[22.9.6.1, )",
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
"Microsoft.TestPlatform.TestHost": "17.14.1"
|
||||
}
|
||||
},
|
||||
"SecurityCodeScan.VS2019": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.6.7, )",
|
||||
"resolved": "5.6.7",
|
||||
"contentHash": "WIE9RJswdSc2j+rLz2gW6U+gMUjMHzY2j7C/CL8/R2olXNM/+twarfMnWqm+rZodDBvaYDApJyxM8mVYf9FGrQ=="
|
||||
},
|
||||
"xunit": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.9.3, )",
|
||||
@@ -726,8 +732,8 @@
|
||||
"Aspire.Npgsql": "[13.2.2, )",
|
||||
"Dapper": "[2.1.72, )",
|
||||
"Dapper.AOT": "[1.0.48, )",
|
||||
"GmRelay.ServiceDefaults": "[1.14.0, )",
|
||||
"GmRelay.Shared": "[1.14.0, )",
|
||||
"GmRelay.ServiceDefaults": "[1.15.0, )",
|
||||
"GmRelay.Shared": "[1.15.0, )",
|
||||
"Microsoft.Extensions.Hosting": "[10.0.5, )",
|
||||
"Npgsql": "[10.0.2, )",
|
||||
"Telegram.Bot": "[22.9.5.3, )",
|
||||
@@ -754,8 +760,8 @@
|
||||
"dependencies": {
|
||||
"Aspire.Npgsql": "[13.2.2, )",
|
||||
"Dapper": "[2.1.72, )",
|
||||
"GmRelay.ServiceDefaults": "[1.14.0, )",
|
||||
"GmRelay.Shared": "[1.14.0, )",
|
||||
"GmRelay.ServiceDefaults": "[1.15.0, )",
|
||||
"GmRelay.Shared": "[1.15.0, )",
|
||||
"Npgsql": "[10.0.2, )",
|
||||
"Telegram.Bot": "[22.9.6.1, )"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user