Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions ops/backup/.env.backup.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# -- PostgreSQL Configuration --
PG_HOST=postgres-db
PG_PORT=5432
PG_USER=backup_user
PG_PASSWORD=supersecretpassword
# Optional: Specify databases to back up, comma-separated. If empty, all databases will be backed up.
PG_DATABASES=

# -- Redis Configuration --
REDIS_HOST=redis
REDIS_PORT=6379

# -- File Backup Configuration --
# Comma-separated list of absolute paths to back up
FILE_PATHS=/data/uploads,/data/config

# -- Backup Storage and Retention --
BACKUP_DIR=/backups
RETENTION_DAYS=7

# -- Scheduler Configuration --
# Cron schedule for daemon mode
CRON_SCHEDULE="0 2 * * *"

# -- Locking Configuration --
# A unique integer for Postgres advisory lock
ADVISORY_LOCK_KEY=123456789

# -- Optional: GPG Encryption --
GPG_ENABLED=false
GPG_RECIPIENT=

# -- Optional: Telegram Notification --
TELEGRAM_ENABLED=false
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
44 changes: 44 additions & 0 deletions ops/backup/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# /ops/backup/Dockerfile

# --- Stage 1: Build Environment ---
FROM python:3.11-slim as builder

WORKDIR /app

# Install system dependencies, including cron
RUN apt-get update && apt-get install -y --no-install-recommends \
postgresql-client \
redis-tools \
cron \
&& rm -rf /var/lib/apt/lists/*

# Copy requirements and install python packages
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy source code
COPY src/ ./src
COPY entrypoint.sh .
RUN chmod +x entrypoint.sh

# --- Stage 2: Final Image ---
FROM python:3.11-slim

WORKDIR /app

# Copy system utilities and cron from builder stage
COPY --from=builder /usr/bin/pg_dump /usr/bin/
COPY --from=builder /usr/bin/psql /usr/bin/
COPY --from=builder /usr/bin/redis-cli /usr/bin/
COPY --from=builder /usr/sbin/cron /usr/sbin/
COPY --from=builder /etc/cron.d /etc/cron.d

# Copy installed Python packages
COPY --from=builder /usr/local/lib/python3.11/site-packages/ /usr/local/lib/python3.11/site-packages/

# Copy source code and entrypoint
COPY src/ ./src
COPY entrypoint.sh .

# Set the entrypoint to our script
ENTRYPOINT ["./entrypoint.sh"]
95 changes: 95 additions & 0 deletions ops/backup/README-backup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# سرویس بکاپ داخلی MasirYar

این مستندات به تشریح سرویس بکاپ داخلی برای پلتفرم MasirYar می‌پردازد. این سرویس به صورت یک کانتینر داکر اجرا شده و به طور منظم از داده‌های حیاتی سیستم (PostgreSQL, Redis, و فایل‌ها) بکاپ تهیه می‌کند.

## 📜 نیازمندی‌ها

- Docker و Docker Compose
- دسترسی به یک رجیستری داکر (برای build و push ایمیج)
- ابزارهای `make` (اختیاری، برای سهولت در اجرای دستورات)

## ⚙️ پیکربندی

تمام پیکربندی‌ها از طریق متغیرهای محیطی انجام می‌شود. یک فایل نمونه به نام `.env.backup.template` در همین پوشه قرار دارد. برای اجرای سرویس، یک کپی از این فایل با نام `.env` ایجاد کرده و مقادیر آن را مطابق با محیط خود تنظیم کنید.

| متغیر | توضیحات | مقدار پیش‌فرض |
| ------------------- | ------------------------------------------------------------------------ | ------------------ |
| `PG_HOST` | آدرس سرویس PostgreSQL. | `postgres-db` |
| `PG_USER` | نام کاربری برای اتصال به دیتابیس. | `backup_user` |
| `PG_PASSWORD` | رمز عبور کاربر دیتابیس. | - |
| `REDIS_HOST` | آدرس سرویس Redis. | `redis` |
| `FILE_PATHS` | لیستی از مسیرهای مطلق فایل‌ها برای بکاپ، جدا شده با کاما. | - |
| `BACKUP_DIR` | مسیر داخل کانتینر که بکاپ‌ها در آن ذخیره می‌شوند. | `/backups` |
| `RETENTION_DAYS` | تعداد روزهایی که بکاپ‌ها باید نگهداری شوند. | `7` |
| `CRON_SCHEDULE` | زمان‌بندی اجرای بکاپ در فرمت Cron. | `0 2 * * *` (2 بامداد) |
| `ADVISORY_LOCK_KEY` | یک کلید عددی منحصر به فرد برای قفل در PostgreSQL. | `123456789` |

## 🚀 نحوه اجرا

### اجرای مستقل برای تست

یک فایل `docker-compose.backup.yml` برای اجرای سرویس به صورت ایزوله و برای تست فراهم شده است.

```bash
# Build کردن و اجرای سرویس بکاپ به صورت یکباره
docker-compose -f docker-compose.backup.yml run --build backup-service run-once

# برای مشاهده بکاپ‌های ایجاد شده
docker-compose -f docker-compose.backup.yml exec backup-service ls /backups
```

### ادغام با `docker-compose` اصلی

برای ادغام با پروژه اصلی، قطعه کد زیر را به فایل `docker-compose.yml` اصلی خود اضافه کنید و مقادیر را مطابق با نیاز خود تنظیم نمایید.

```yaml
services:
backup:
build:
context: ./ops/backup
container_name: masiryar-backup
environment:
- PG_HOST=postgres-db
- PG_USER=your_backup_user
- PG_PASSWORD=${YOUR_DB_PASSWORD}
- REDIS_HOST=redis
- FILE_PATHS=/path/to/your/files
- BACKUP_DIR=/backups
- RETENTION_DAYS=7
- CRON_SCHEDULE=0 2 * * *
volumes:
- masiryar_backup_data:/backups # Volume برای نگهداری دائمی بکاپ‌ها
restart: on-failure
```

## 🧪 تست

Unit testها با استفاده از `pytest` نوشته شده‌اند.

```bash
# نصب وابستگی‌ها
pip install -r ops/backup/requirements.txt

# اجرای تست‌ها
pytest ops/backup/tests/
```

## ⏪ بازیابی بکاپ (Restore)

یک اسکریپت کمکی به نام `restore.sh` برای بازیابی بکاپ‌های PostgreSQL فراهم شده است.

**نحوه استفاده:**

```bash
# دادن دسترسی اجرایی به اسکریپت
chmod +x ops/backup/restore.sh

# اجرای اسکریپت
PGPASSWORD=your_db_pass ./ops/backup/restore.sh <path_to_backup_dir> <original_db_name> [new_db_name]
```

- `<path_to_backup_dir>`: مسیر پوشه‌ای که بکاپ در آن قرار دارد (مثلاً `/var/lib/docker/volumes/masiryar_backup_data/_data/20231103T102030Z`).
- `<original_db_name>`: نام دیتابیسی که از آن بکاپ گرفته شده است.
- `[new_db_name]` (اختیاری): نام دیتابیس جدیدی که می‌خواهید بکاپ در آن بازیابی شود.

این اسکریپت به صورت تعاملی برای حذف دیتابیس مقصد (در صورت وجود) از شما تایید خواهد گرفت.
49 changes: 49 additions & 0 deletions ops/backup/docker-compose.backup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# /ops/backup/docker-compose.backup.yml
version: '3.8'

services:
postgres-test:
image: postgres:16
container_name: backup_postgres_test
environment:
- POSTGRES_USER=testuser
- POSTGRES_PASSWORD=testpass
- POSTGRES_DB=testdb
ports:
- "5435:5432" # Use a different port to avoid conflicts
volumes:
- postgres_test_data:/var/lib/postgresql/data
restart: always

redis-test:
image: redis:7
container_name: backup_redis_test
ports:
- "6381:6379" # Use a different port
restart: always

backup-service:
build:
context: .
dockerfile: Dockerfile
container_name: masiryar_backup_test
depends_on:
- postgres-test
- redis-test
environment:
- PG_HOST=postgres-test
- PG_PORT=5432
- PG_USER=testuser
- PG_PASSWORD=testpass
- REDIS_HOST=redis-test
- REDIS_PORT=6379
- FILE_PATHS=/testdata/logs,/testdata/config # Example paths inside the container
- BACKUP_DIR=/backups
- RETENTION_DAYS=3
volumes:
- backup_test_data:/backups
- ./testdata:/testdata # Mount a local testdata directory

volumes:
postgres_test_data:
backup_test_data:
28 changes: 28 additions & 0 deletions ops/backup/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/bin/bash
# /ops/backup/entrypoint.sh

set -e

# --- Environment Variable Setup for Cron ---
# Export all environment variables to a file that cron can use
printenv | grep -v "no_proxy" > /etc/environment

# --- Cron Job Setup ---
echo "Setting up cron schedule: ${CRON_SCHEDULE}"

# Create a crontab file
# The command runs the backup service once and logs the output
echo "${CRON_SCHEDULE} /usr/local/bin/python /app/src/backup_service.py run-once >> /var/log/cron.log 2>&1" > /etc/cron.d/backup-cron

# Give execution rights on the cron job
chmod 0644 /etc/cron.d/backup-cron

# Apply cron job
crontab /etc/cron.d/backup-cron

# Create the log file to be able to run tail
touch /var/log/cron.log

# --- Start Cron and Log Output ---
echo "Starting cron scheduler..."
cron && tail -f /var/log/cron.log
5 changes: 5 additions & 0 deletions ops/backup/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# ops/backup/requirements.txt
python-dotenv==1.0.0
psycopg2-binary==2.9.9
redis==5.0.4
pytest==8.2.0
76 changes: 76 additions & 0 deletions ops/backup/restore.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/bin/bash
# /ops/backup/restore.sh

set -e
set -o pipefail

# --- Defaults and Configuration ---
PG_HOST=${PG_HOST:-localhost}
PG_PORT=${PG_PORT:-5432}
PG_USER=${PG_USER:-admin}

# --- Helper Functions ---
function print_usage() {
echo "Usage: ./restore.sh <backup_dir> <db_name> [target_db_name]"
echo ""
echo "Restores a PostgreSQL database from a backup directory generated by the backup service."
echo ""
echo "Arguments:"
echo " <backup_dir> Path to the backup directory (e.g., /backups/20231103T102030Z)."
echo " <db_name> The original name of the database to restore (e.g., PersonalGrowthDb)."
echo " [target_db_name] Optional. The name of the new database to restore into. Defaults to <db_name>_restored."
echo ""
echo "Environment Variables:"
echo " PG_HOST, PG_PORT, PG_USER, PGPASSWORD can be set to configure the connection."
}

# --- Input Validation ---
if [[ -z "$1" || -z "$2" ]]; then
echo "Error: Backup directory and database name are required."
print_usage
exit 1
fi

BACKUP_DIR=$1
DB_NAME=$2
TARGET_DB_NAME=${3:-${DB_NAME}_restored}

# Find the dump file in the backup directory
DUMP_FILE=$(find "$BACKUP_DIR" -name "pg-${DB_NAME}.dump" | head -n 1)

if [[ ! -f "$DUMP_FILE" ]]; then
echo "Error: Dump file for database '$DB_NAME' not found in '$BACKUP_DIR'."
exit 1
fi

if [[ -z "$PGPASSWORD" ]]; then
echo -n "Enter PostgreSQL password for user '$PG_USER': "
read -s PGPASSWORD
export PGPASSWORD
echo
fi

# --- Restore Process ---
echo "Starting restore of '$DB_NAME' from '$DUMP_FILE' to database '$TARGET_DB_NAME'..."

# 1. Drop target database if it exists (with confirmation)
read -p "WARNING: This will drop the database '$TARGET_DB_NAME' if it exists. Continue? (y/N) " confirm
if [[ ! "$confirm" =~ ^[yY]$ ]]; then
echo "Restore cancelled."
exit 0
fi

echo "Dropping existing database '$TARGET_DB_NAME'..."
dropdb --if-exists -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" "$TARGET_DB_NAME"

# 2. Create a new, empty database
echo "Creating new database '$TARGET_DB_NAME'..."
createdb -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -O "$PG_USER" "$TARGET_DB_NAME"

# 3. Restore the dump into the new database
echo "Restoring data using pg_restore..."
pg_restore -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$TARGET_DB_NAME" -v --no-owner --no-privileges "$DUMP_FILE"

echo ""
echo "✅ Restore completed successfully."
echo "Database '$DB_NAME' has been restored to '$TARGET_DB_NAME' on host '$PG_HOST'."
Loading
Loading