From d6c38c43e6a913bdb54ae337a66137670bc07044 Mon Sep 17 00:00:00 2001 From: NathanJ60 Date: Mon, 15 Dec 2025 04:31:24 +0100 Subject: [PATCH 1/8] feat: improved backup system with encryption and scheduling - Add GPG encryption (AES256) for backup archives - Add configurable destination (local or remote via rsync/scp) - Backup all .env files recursively - Backup frontend/public/home/ assets - Add backup-cron.sh wrapper for scheduled backups - Add backup.conf.example configuration template - Add optional Docker backup service (compose profile) - Update documentation with full usage guide Closes #416 --- backup-cron.sh | 135 +++++++++++ backup.conf.example | 83 +++++++ backup.sh | 534 ++++++++++++++++++++++++++++++++------------ compose.yml | 25 +++ docs/src/backup.md | 286 ++++++++++++++++++------ 5 files changed, 853 insertions(+), 210 deletions(-) create mode 100644 backup-cron.sh create mode 100644 backup.conf.example diff --git a/backup-cron.sh b/backup-cron.sh new file mode 100644 index 000000000..d83aa95a4 --- /dev/null +++ b/backup-cron.sh @@ -0,0 +1,135 @@ +#!/bin/bash + +# Arcadia Backup Cron Wrapper +# This script is designed to be run by cron for scheduled backups +# It loads configuration from backup.conf and handles logging/locking +# +# Installation: +# 1. Copy backup.conf.example to backup.conf and customize +# 2. chmod +x backup-cron.sh +# 3. Add to crontab: 0 3 * * * /path/to/backup-cron.sh +# +# Logs are written to /var/log/arcadia-backup.log by default + +set -e + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Default configuration +LOG_FILE="${BACKUP_LOG_FILE:-/var/log/arcadia-backup.log}" +LOCK_FILE="/tmp/arcadia-backup.lock" + +# ============================================================================ +# LOGGING +# ============================================================================ + +log() { + echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE" +} + +log_separator() { + echo "========================================" >> "$LOG_FILE" +} + +# ============================================================================ +# LOCK HANDLING +# ============================================================================ + +# Prevent concurrent runs +if [ -f "$LOCK_FILE" ]; then + # Check if the process is still running + if [ -f "$LOCK_FILE" ]; then + OLD_PID=$(cat "$LOCK_FILE" 2>/dev/null) + if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then + log "ERROR: Backup already running (PID: $OLD_PID), skipping" + exit 0 + else + log "WARNING: Stale lock file found, removing" + rm -f "$LOCK_FILE" + fi + fi +fi + +# Create lock file with our PID +echo $$ > "$LOCK_FILE" +trap "rm -f $LOCK_FILE" EXIT + +# ============================================================================ +# LOAD CONFIGURATION +# ============================================================================ + +if [ -f "$SCRIPT_DIR/backup.conf" ]; then + source "$SCRIPT_DIR/backup.conf" + log "Configuration loaded from backup.conf" +else + log "WARNING: No backup.conf found, using defaults" +fi + +# ============================================================================ +# RUN BACKUP +# ============================================================================ + +log_separator +log "Starting scheduled backup" + +cd "$SCRIPT_DIR" + +# Build command arguments +BACKUP_ARGS="--db-docker" + +[ "$BACKUP_ENCRYPT" = "true" ] && BACKUP_ARGS="$BACKUP_ARGS --encrypt" +[ -n "$BACKUP_PASSWORD" ] && BACKUP_ARGS="$BACKUP_ARGS --password \"$BACKUP_PASSWORD\"" +[ -n "$BACKUP_PASSWORD_FILE" ] && BACKUP_ARGS="$BACKUP_ARGS --password-file \"$BACKUP_PASSWORD_FILE\"" +[ -n "$BACKUP_DESTINATION" ] && BACKUP_ARGS="$BACKUP_ARGS --destination \"$BACKUP_DESTINATION\"" +[ -n "$BACKUP_REMOTE_TYPE" ] && BACKUP_ARGS="$BACKUP_ARGS --remote-type \"$BACKUP_REMOTE_TYPE\"" +[ -n "$BACKUP_REMOTE_HOST" ] && BACKUP_ARGS="$BACKUP_ARGS --remote-host \"$BACKUP_REMOTE_HOST\"" +[ -n "$BACKUP_REMOTE_USER" ] && BACKUP_ARGS="$BACKUP_ARGS --remote-user \"$BACKUP_REMOTE_USER\"" +[ -n "$BACKUP_REMOTE_PATH" ] && BACKUP_ARGS="$BACKUP_ARGS --remote-path \"$BACKUP_REMOTE_PATH\"" +[ -n "$BACKUP_REMOTE_KEY" ] && BACKUP_ARGS="$BACKUP_ARGS --remote-key \"$BACKUP_REMOTE_KEY\"" +[ -n "$BACKUP_REMOTE_PORT" ] && BACKUP_ARGS="$BACKUP_ARGS --remote-port \"$BACKUP_REMOTE_PORT\"" +[ "$BACKUP_DELETE_AFTER_UPLOAD" = "true" ] && BACKUP_ARGS="$BACKUP_ARGS --delete-after-upload" + +# Run backup +log "Executing: ./backup.sh $BACKUP_ARGS" +eval "./backup.sh $BACKUP_ARGS" >> "$LOG_FILE" 2>&1 +STATUS=$? + +if [ $STATUS -eq 0 ]; then + log "Backup completed successfully" +else + log "ERROR: Backup failed with exit code $STATUS" +fi + +# ============================================================================ +# RETENTION POLICY +# ============================================================================ + +if [ -n "$BACKUP_RETENTION_DAYS" ] && [ -n "$BACKUP_DESTINATION" ]; then + log "Applying retention policy: deleting backups older than $BACKUP_RETENTION_DAYS days" + + # Count files before deletion + OLD_COUNT=$(find "$BACKUP_DESTINATION" -name "arcadia_backup_*" -mtime +$BACKUP_RETENTION_DAYS 2>/dev/null | wc -l) + + if [ "$OLD_COUNT" -gt 0 ]; then + find "$BACKUP_DESTINATION" -name "arcadia_backup_*" -mtime +$BACKUP_RETENTION_DAYS -delete + log "Deleted $OLD_COUNT old backup(s)" + else + log "No old backups to delete" + fi +fi + +# ============================================================================ +# SUMMARY +# ============================================================================ + +if [ -n "$BACKUP_DESTINATION" ] && [ -d "$BACKUP_DESTINATION" ]; then + BACKUP_COUNT=$(find "$BACKUP_DESTINATION" -name "arcadia_backup_*" 2>/dev/null | wc -l) + BACKUP_SIZE=$(du -sh "$BACKUP_DESTINATION" 2>/dev/null | cut -f1) + log "Current backups: $BACKUP_COUNT files, total size: $BACKUP_SIZE" +fi + +log "Scheduled backup job finished" +log_separator + +exit $STATUS diff --git a/backup.conf.example b/backup.conf.example new file mode 100644 index 000000000..189ddb77c --- /dev/null +++ b/backup.conf.example @@ -0,0 +1,83 @@ +# Arcadia Backup Configuration +# ============================= +# Copy this file to backup.conf and customize for your environment +# +# This configuration is used by backup-cron.sh for scheduled backups +# You can also set these as environment variables + +# ============================================================================ +# ENCRYPTION +# ============================================================================ + +# Enable encryption (AES256 via GPG) +BACKUP_ENCRYPT=true + +# Encryption password (REQUIRED if BACKUP_ENCRYPT=true) +# Choose ONE of the following methods: +BACKUP_PASSWORD=your_secure_passphrase_here + +# OR use a password file (more secure for automated systems) +# BACKUP_PASSWORD_FILE=/path/to/secure/password/file + +# ============================================================================ +# LOCAL STORAGE +# ============================================================================ + +# Directory where backups will be stored +BACKUP_DESTINATION=/home/ubuntu/arcadia-backups + +# ============================================================================ +# REMOTE STORAGE (Optional) +# ============================================================================ + +# Upload method: rsync or scp +# BACKUP_REMOTE_TYPE=rsync + +# Remote server hostname +# BACKUP_REMOTE_HOST=backup.example.com + +# Remote server username +# BACKUP_REMOTE_USER=backup + +# Remote destination path +# BACKUP_REMOTE_PATH=/backups/arcadia + +# SSH private key file (optional, uses default SSH key if not set) +# BACKUP_REMOTE_KEY=/home/ubuntu/.ssh/backup_key + +# SSH port (default: 22) +# BACKUP_REMOTE_PORT=22 + +# Delete local backup after successful remote upload +# BACKUP_DELETE_AFTER_UPLOAD=false + +# ============================================================================ +# RETENTION POLICY +# ============================================================================ + +# Number of days to keep backups (older backups are automatically deleted) +BACKUP_RETENTION_DAYS=7 + +# ============================================================================ +# LOGGING +# ============================================================================ + +# Log file path +BACKUP_LOG_FILE=/var/log/arcadia-backup.log + +# ============================================================================ +# CRON SCHEDULE EXAMPLES +# ============================================================================ +# Add one of these to your crontab (crontab -e): +# +# Daily at 3:00 AM: +# 0 3 * * * /home/ubuntu/arcadia-services/backup-cron.sh +# +# Every 6 hours: +# 0 */6 * * * /home/ubuntu/arcadia-services/backup-cron.sh +# +# Weekly on Sunday at 2:00 AM: +# 0 2 * * 0 /home/ubuntu/arcadia-services/backup-cron.sh +# +# Monthly on the 1st at 4:00 AM: +# 0 4 1 * * /home/ubuntu/arcadia-services/backup-cron.sh diff --git a/backup.sh b/backup.sh index 277097acc..1b398bc3e 100644 --- a/backup.sh +++ b/backup.sh @@ -1,24 +1,48 @@ #!/bin/bash # Arcadia Backup Script -# This script backs up the database data and environment files -# Supports both Docker and non-Docker setups +# This script backs up the database, environment files, and frontend assets +# Supports Docker and non-Docker setups, encryption, and remote upload # -# Usage: -# ./backup.sh [--db-docker] [--db-name NAME] [--db-user USER] [--db-container CONTAINER] -# --db-docker: Use Docker setup (default: local PostgreSQL) -# --db-name: Database name (default: arcadia) -# --db-user: Database user (default: arcadia) -# --db-container: Docker container name (default: arcadia_db) +# Usage: ./backup.sh [OPTIONS] +# Run ./backup.sh --help for full options set -e # Exit on any error -# Default to local setup +# ============================================================================ +# DEFAULT VALUES +# ============================================================================ + USE_DOCKER=false +ENCRYPT=false +BACKUP_PASSWORD="" +BACKUP_PASSWORD_FILE="" +BACKUP_DESTINATION="." +KEEP_TEMP=false +REMOTE_ENABLED=false +REMOTE_TYPE="" +REMOTE_HOST="" +REMOTE_USER="" +REMOTE_PATH="" +REMOTE_KEY="" +REMOTE_PORT="22" +DELETE_AFTER_UPLOAD=false + +# Database defaults +DB_NAME="" +DB_USER="" +DB_PASSWORD="" +DB_HOST="" +DB_PORT="" +DB_CONTAINER="" + +# ============================================================================ +# PARSE COMMAND LINE ARGUMENTS +# ============================================================================ -# Parse command line arguments first (to handle --help before loading config) while [[ $# -gt 0 ]]; do case $1 in + # Database options --db-docker) USE_DOCKER=true shift @@ -47,35 +71,115 @@ while [[ $# -gt 0 ]]; do DB_PORT="$2" shift 2 ;; + # Encryption options + --encrypt) + ENCRYPT=true + shift + ;; + --password) + BACKUP_PASSWORD="$2" + shift 2 + ;; + --password-file) + BACKUP_PASSWORD_FILE="$2" + shift 2 + ;; + # Destination options + --destination) + BACKUP_DESTINATION="$2" + shift 2 + ;; + --keep-temp) + KEEP_TEMP=true + shift + ;; + # Remote upload options + --remote-type) + REMOTE_ENABLED=true + REMOTE_TYPE="$2" + shift 2 + ;; + --remote-host) + REMOTE_HOST="$2" + shift 2 + ;; + --remote-user) + REMOTE_USER="$2" + shift 2 + ;; + --remote-path) + REMOTE_PATH="$2" + shift 2 + ;; + --remote-key) + REMOTE_KEY="$2" + shift 2 + ;; + --remote-port) + REMOTE_PORT="$2" + shift 2 + ;; + --delete-after-upload) + DELETE_AFTER_UPLOAD=true + shift + ;; + # Help -h|--help) - echo "Arcadia Backup Script - Creates a complete backup of the application" - echo "" - echo "Usage: $0 [OPTIONS]" - echo "" - echo "BACKUP MODES:" - echo " --db-docker Use Docker setup (connects to database container)" - echo " (default) Use local setup (connects to local PostgreSQL)" - echo "" - echo "DOCKER MODE OPTIONS:" - echo " --db-container Docker container name (default: arcadia_db)" - echo " Override if your container has a different name" - echo "" - echo "LOCAL MODE OPTIONS:" - echo " --db-host Database host (default: localhost)" - echo " --db-port Database port (default: 5432)" - echo " --db-name Database name (default: arcadia)" - echo " --db-user Database user (default: arcadia)" - echo " --db-password Database password (optional)" - echo " If not provided, uses .pgpass, environment vars, or peer auth" - echo "" - echo "COMMON OPTIONS:" - echo " -h, --help Show this help message" - echo "" - echo "EXAMPLES:" - echo " $0 # Local backup with default settings" - echo " $0 --db-docker # Docker backup with default container" - echo " $0 --db-host db.example.com # Local backup to remote database" - echo " $0 --db-docker --db-container my_db # Docker backup with custom container" + cat << 'EOF' +Arcadia Backup Script - Creates a complete backup of the application + +Usage: ./backup.sh [OPTIONS] + +DATABASE OPTIONS: + --db-docker Use Docker setup (connects to database container) + --db-container NAME Docker container name (default: arcadia_db) + --db-host HOST Database host for local setup (default: localhost) + --db-port PORT Database port (default: 5432) + --db-name NAME Database name (default: arcadia) + --db-user USER Database user (default: arcadia) + --db-password PASS Database password + +ENCRYPTION OPTIONS: + --encrypt Enable GPG encryption (AES256) + --password PASS Encryption password + --password-file FILE Read encryption password from file + +DESTINATION OPTIONS: + --destination DIR Output directory for backup (default: current dir) + --keep-temp Keep temporary backup directory + +REMOTE UPLOAD OPTIONS: + --remote-type TYPE Upload method: rsync or scp + --remote-host HOST Remote server hostname + --remote-user USER Remote server username + --remote-path PATH Remote destination path + --remote-key FILE SSH private key file (optional) + --remote-port PORT SSH port (default: 22) + --delete-after-upload Delete local backup after successful upload + +GENERAL OPTIONS: + -h, --help Show this help message + +EXAMPLES: + ./backup.sh --db-docker + Basic Docker backup + + ./backup.sh --db-docker --encrypt --password "secret123" + Encrypted Docker backup + + ./backup.sh --db-docker --destination /backups --encrypt --password "secret" + Encrypted backup to custom directory + + ./backup.sh --db-docker --encrypt --password "secret" \ + --remote-type rsync --remote-host backup.example.com \ + --remote-user backup --remote-path /backups/arcadia + Encrypted backup with remote upload + +ENVIRONMENT VARIABLES: + BACKUP_PASSWORD Default encryption password + BACKUP_ENCRYPT Set to 'true' to enable encryption by default + +EOF exit 0 ;; *) @@ -86,118 +190,106 @@ while [[ $# -gt 0 ]]; do esac done -# Load configuration after argument parsing -# Default configuration -# Default values will be set later after processing command line arguments and environment variables +# ============================================================================ +# LOAD CONFIGURATION +# ============================================================================ + BACKUP_DIR="backup_$(date +%Y%m%d_%H%M%S)" -ZIP_FILE="arcadia_backup_$(date +%Y%m%d_%H%M%S).zip" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) -# Source configuration from backend .env files based on mode and priority +# Source configuration from backend .env files if [ "$USE_DOCKER" = true ]; then - # Docker mode: prioritize .env.docker, fallback to .env if [ -f "backend/.env.docker" ]; then echo "Loading configuration from backend/.env.docker..." - export $(grep -v '^#' backend/.env.docker | grep -E '^(POSTGRES_|DB_|BACKUP_)' | tr -d '\r' | xargs) + export $(grep -v '^#' backend/.env.docker | grep -E '^(POSTGRES_|DB_|BACKUP_)' | tr -d '\r' | xargs 2>/dev/null) || true elif [ -f "backend/.env" ]; then echo "Loading configuration from backend/.env..." - export $(grep -v '^#' backend/.env | grep -E '^(POSTGRES_|DB_|BACKUP_)' | tr -d '\r' | xargs) + export $(grep -v '^#' backend/.env | grep -E '^(POSTGRES_|DB_|BACKUP_)' | tr -d '\r' | xargs 2>/dev/null) || true fi else - # Local mode: use .env only if [ -f "backend/.env" ]; then echo "Loading configuration from backend/.env..." - export $(grep -v '^#' backend/.env | grep -E '^(POSTGRES_|DB_|BACKUP_)' | tr -d '\r' | xargs) + export $(grep -v '^#' backend/.env | grep -E '^(POSTGRES_|DB_|BACKUP_)' | tr -d '\r' | xargs 2>/dev/null) || true fi fi -# Function to strip carriage returns from variables +# Function to strip carriage returns strip_cr() { echo "$1" | tr -d '\r' } -# Map POSTGRES_ variables from backend .env to DB_ variables (only if not set by command line) -# Command line arguments take highest priority -if [ -z "$DB_NAME" ]; then - DB_NAME=$(strip_cr "${POSTGRES_DATABASE:-"arcadia"}") -fi -if [ -z "$DB_USER" ]; then - DB_USER=$(strip_cr "${POSTGRES_USER:-"arcadia"}") -fi -if [ -z "$DB_PASSWORD" ]; then - DB_PASSWORD=$(strip_cr "${POSTGRES_PASSWORD}") -fi -if [ -z "$DB_HOST" ]; then - DB_HOST=$(strip_cr "${POSTGRES_HOST:-"localhost"}") -fi -if [ -z "$DB_PORT" ]; then - DB_PORT=$(strip_cr "${POSTGRES_PORT:-"5432"}") -fi -if [ -z "$DB_CONTAINER" ]; then - DB_CONTAINER=$(strip_cr "${DB_CONTAINER:-"arcadia_db"}") -fi -BACKUP_DIR=${BACKUP_DIR:-"backup_$(date +%Y%m%d_%H%M%S)"} -ZIP_FILE=${ZIP_FILE:-"arcadia_backup_$(date +%Y%m%d_%H%M%S).zip"} +# Apply defaults (command line > env vars > defaults) +[ -z "$DB_NAME" ] && DB_NAME=$(strip_cr "${POSTGRES_DATABASE:-arcadia}") +[ -z "$DB_USER" ] && DB_USER=$(strip_cr "${POSTGRES_USER:-arcadia}") +[ -z "$DB_PASSWORD" ] && DB_PASSWORD=$(strip_cr "${POSTGRES_PASSWORD}") +[ -z "$DB_HOST" ] && DB_HOST=$(strip_cr "${POSTGRES_HOST:-localhost}") +[ -z "$DB_PORT" ] && DB_PORT=$(strip_cr "${POSTGRES_PORT:-5432}") +[ -z "$DB_CONTAINER" ] && DB_CONTAINER=$(strip_cr "${DB_CONTAINER:-arcadia_db}") + +# Check encryption env vars +[ "$BACKUP_ENCRYPT" = "true" ] && ENCRYPT=true +[ -z "$BACKUP_PASSWORD" ] && [ -n "${BACKUP_PASSWORD:-}" ] && BACKUP_PASSWORD="$BACKUP_PASSWORD" -echo "Starting Arcadia backup..." +# ============================================================================ +# SETUP +# ============================================================================ + +echo "============================================" +echo "Arcadia Backup Script" +echo "============================================" +echo "Starting backup at $(date)" +echo "" # Create temporary backup directory mkdir -p "$BACKUP_DIR" -# Function to cleanup on exit +# Cleanup function cleanup() { - echo "Cleaning up temporary files..." - rm -rf "$BACKUP_DIR" + if [ "$KEEP_TEMP" = false ]; then + echo "Cleaning up temporary files..." + rm -rf "$BACKUP_DIR" + else + echo "Temporary files kept at: $BACKUP_DIR" + fi } trap cleanup EXIT -# Use Docker or local setup based on USE_DOCKER flag +# ============================================================================ +# VALIDATE SETUP +# ============================================================================ + if [ "$USE_DOCKER" = true ]; then - echo "Using Docker setup - containerized database" - # Check if Docker is running + echo "Mode: Docker" if ! docker info >/dev/null 2>&1; then echo "Error: Docker is not running or not accessible" exit 1 fi - # Check if database container is running if ! docker ps --format "table {{.Names}}" | grep -q "^$DB_CONTAINER$"; then echo "Error: Database container '$DB_CONTAINER' is not running" echo "Please start the database with: docker compose up db -d" exit 1 fi + echo "Database container: $DB_CONTAINER" else - echo "Using local setup - local PostgreSQL installation" - # Check if .env file exists - if [ ! -f "backend/.env" ]; then - echo "Error: backend/.env file not found" - echo "Please create backend/.env with DATABASE_URL configured" - exit 1 - fi - # Check if DATABASE_URL is in .env - if ! grep -q "DATABASE_URL" "backend/.env"; then - echo "Error: DATABASE_URL not found in backend/.env" + echo "Mode: Local PostgreSQL" + if ! command -v pg_dump >/dev/null 2>&1; then + echo "Error: pg_dump command not found. Please install PostgreSQL client tools" exit 1 fi + echo "Database: $DB_HOST:$DB_PORT/$DB_NAME" fi +echo "" + +# ============================================================================ +# BACKUP DATABASE +# ============================================================================ echo "Backing up database..." if [ "$USE_DOCKER" = true ]; then - # Docker setup - use docker exec - echo "Using Docker container for database backup..." docker exec "$DB_CONTAINER" pg_dump -U "$DB_USER" -d "$DB_NAME" --no-owner --no-privileges > "$BACKUP_DIR/database_full.sql" BACKUP_EXIT_CODE=$? else - # Local setup - use local pg_dump with DATABASE_URL - echo "Using local PostgreSQL installation for database backup..." - - # Check if pg_dump is available - if ! command -v pg_dump >/dev/null 2>&1; then - echo "Error: pg_dump command not found. Please install PostgreSQL client tools" - exit 1 - fi - - # Use pg_dump with individual connection parameters - echo "Connecting to database: $DB_HOST:$DB_PORT/$DB_NAME as $DB_USER" if [ -n "$DB_PASSWORD" ]; then PGPASSWORD="$DB_PASSWORD" pg_dump \ -h "$DB_HOST" \ @@ -206,7 +298,6 @@ else -d "$DB_NAME" \ --no-owner --no-privileges > "$BACKUP_DIR/database_full.sql" else - # No password provided, rely on .pgpass, environment, or peer authentication pg_dump \ -h "$DB_HOST" \ -p "$DB_PORT" \ @@ -218,57 +309,218 @@ else fi if [ $BACKUP_EXIT_CODE -eq 0 ]; then - echo "Database backup completed successfully" + DB_SIZE=$(du -h "$BACKUP_DIR/database_full.sql" | cut -f1) + echo "Database backup completed ($DB_SIZE)" else echo "Error: Database backup failed" exit 1 fi -echo "Copying environment files..." -# Copy backend .env file if it exists -if [ -f "backend/.env" ]; then - cp "backend/.env" "$BACKUP_DIR/backend.env" - echo "Backend .env file copied" -else - echo "Warning: backend/.env file not found, skipping..." -fi +# ============================================================================ +# BACKUP ALL .ENV FILES +# ============================================================================ + +echo "" +echo "Backing up environment files..." +mkdir -p "$BACKUP_DIR/env_files" + +env_count=0 +while IFS= read -r env_file; do + if [ -n "$env_file" ]; then + rel_path="${env_file#./}" + target_dir="$BACKUP_DIR/env_files/$(dirname "$rel_path")" + mkdir -p "$target_dir" + cp "$env_file" "$target_dir/" + echo " Backed up: $rel_path" + env_count=$((env_count + 1)) + fi +done < <(find . -name ".env*" -type f \ + ! -path "./node_modules/*" \ + ! -path "./.git/*" \ + ! -path "./target/*" \ + ! -path "./.sqlx/*" \ + ! -path "./backup_*/*" \ + 2>/dev/null) + +echo "Total .env files backed up: $env_count" + +# ============================================================================ +# BACKUP FRONTEND PUBLIC ASSETS +# ============================================================================ -# Copy frontend .env file if it exists -if [ -f "frontend/.env" ]; then - cp "frontend/.env" "$BACKUP_DIR/frontend.env" - echo "Frontend .env file copied" +echo "" +echo "Backing up frontend public assets..." + +if [ -d "frontend/public/home" ]; then + mkdir -p "$BACKUP_DIR/frontend_public" + cp -r "frontend/public/home" "$BACKUP_DIR/frontend_public/home" + ASSETS_SIZE=$(du -sh "frontend/public/home" 2>/dev/null | cut -f1) + echo "Frontend public/home backed up ($ASSETS_SIZE)" else - echo "Warning: frontend/.env file not found, skipping..." + echo "Warning: frontend/public/home not found, skipping..." fi -# Create backup info file -echo "Backup created on: $(date)" > "$BACKUP_DIR/backup_info.txt" -echo "Database: $DB_NAME" >> "$BACKUP_DIR/backup_info.txt" -if [ "$USE_DOCKER" = true ]; then - echo "Setup type: Docker" >> "$BACKUP_DIR/backup_info.txt" - echo "Container: $DB_CONTAINER" >> "$BACKUP_DIR/backup_info.txt" +# ============================================================================ +# CREATE BACKUP INFO FILE +# ============================================================================ + +cat > "$BACKUP_DIR/backup_info.txt" << EOF +Arcadia Backup +============== +Created on: $(date) +Timestamp: $TIMESTAMP + +Database: + Name: $DB_NAME + Setup: $([ "$USE_DOCKER" = true ] && echo "Docker (container: $DB_CONTAINER)" || echo "Local PostgreSQL ($DB_HOST:$DB_PORT)") + +Encryption: $([ "$ENCRYPT" = true ] && echo "Enabled (AES256)" || echo "Disabled") + +Contents: + - database_full.sql: Full database dump + - env_files/: All environment configuration files + - frontend_public/: Frontend public assets (if present) + - backup_info.txt: This file +EOF + +# ============================================================================ +# CREATE ARCHIVE +# ============================================================================ + +echo "" +echo "Creating archive..." + +if [ "$ENCRYPT" = true ]; then + # Get password + if [ -n "$BACKUP_PASSWORD_FILE" ] && [ -f "$BACKUP_PASSWORD_FILE" ]; then + BACKUP_PASSWORD=$(cat "$BACKUP_PASSWORD_FILE") + fi + + if [ -z "$BACKUP_PASSWORD" ]; then + echo "Error: Encryption enabled but no password provided" + echo "Use --password, --password-file, or set BACKUP_PASSWORD env var" + exit 1 + fi + + # Check GPG is available + if ! command -v gpg >/dev/null 2>&1; then + echo "Error: gpg command not found. Please install GPG" + exit 1 + fi + + ARCHIVE_FILE="arcadia_backup_${TIMESTAMP}.tar.gz.gpg" + + # Create encrypted archive + tar -czf - -C "$(dirname "$BACKUP_DIR")" "$(basename "$BACKUP_DIR")" | \ + gpg --symmetric --cipher-algo AES256 \ + --passphrase "$BACKUP_PASSWORD" \ + --batch --yes \ + -o "$ARCHIVE_FILE" + + if [ $? -eq 0 ]; then + ARCHIVE_SIZE=$(du -h "$ARCHIVE_FILE" | cut -f1) + echo "Encrypted backup created: $ARCHIVE_FILE ($ARCHIVE_SIZE)" + echo "Encryption: AES256" + else + echo "Error: Failed to create encrypted archive" + exit 1 + fi else - echo "Setup type: Local PostgreSQL" >> "$BACKUP_DIR/backup_info.txt" - echo "Database URL: $(echo $DATABASE_URL | sed 's/:.*@/:***@/')" >> "$BACKUP_DIR/backup_info.txt" + ARCHIVE_FILE="arcadia_backup_${TIMESTAMP}.zip" + + if command -v zip >/dev/null 2>&1; then + zip -rq "$ARCHIVE_FILE" "$BACKUP_DIR" + else + echo "Error: zip command not found. Please install zip utility" + exit 1 + fi + + if [ $? -eq 0 ]; then + ARCHIVE_SIZE=$(du -h "$ARCHIVE_FILE" | cut -f1) + echo "Backup created: $ARCHIVE_FILE ($ARCHIVE_SIZE)" + else + echo "Error: Failed to create zip archive" + exit 1 + fi fi -echo "Backup type: Full dump (schema + data)" >> "$BACKUP_DIR/backup_info.txt" -echo "Creating zip archive..." -# Create zip file -if command -v zip >/dev/null 2>&1; then - zip -r "$ZIP_FILE" "$BACKUP_DIR" -else - echo "Error: zip command not found. Please install zip utility" - exit 1 +# ============================================================================ +# MOVE TO DESTINATION +# ============================================================================ + +if [ "$BACKUP_DESTINATION" != "." ]; then + mkdir -p "$BACKUP_DESTINATION" + mv "$ARCHIVE_FILE" "$BACKUP_DESTINATION/" + ARCHIVE_FILE="$BACKUP_DESTINATION/$(basename "$ARCHIVE_FILE")" + echo "Backup moved to: $ARCHIVE_FILE" fi -if [ $? -eq 0 ]; then - echo "Backup completed successfully!" - echo "Backup file: $ZIP_FILE" - echo "Backup size: $(du -h "$ZIP_FILE" | cut -f1)" -else - echo "Error: Failed to create zip archive" - exit 1 +# ============================================================================ +# REMOTE UPLOAD +# ============================================================================ + +if [ "$REMOTE_ENABLED" = true ]; then + echo "" + echo "Uploading backup to remote server..." + + # Validate required parameters + if [ -z "$REMOTE_HOST" ] || [ -z "$REMOTE_USER" ] || [ -z "$REMOTE_PATH" ]; then + echo "Error: Remote upload requires --remote-host, --remote-user, and --remote-path" + exit 1 + fi + + # Build SSH options + SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=30" + [ -n "$REMOTE_KEY" ] && SSH_OPTS="$SSH_OPTS -i $REMOTE_KEY" + + REMOTE_DEST="$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/" + + if [ "$REMOTE_TYPE" = "rsync" ]; then + echo "Using rsync..." + if [ "$REMOTE_PORT" != "22" ]; then + rsync -avz --progress -e "ssh $SSH_OPTS -p $REMOTE_PORT" "$ARCHIVE_FILE" "$REMOTE_DEST" + else + rsync -avz --progress -e "ssh $SSH_OPTS" "$ARCHIVE_FILE" "$REMOTE_DEST" + fi + UPLOAD_STATUS=$? + elif [ "$REMOTE_TYPE" = "scp" ]; then + echo "Using scp..." + if [ "$REMOTE_PORT" != "22" ]; then + scp -P "$REMOTE_PORT" $SSH_OPTS "$ARCHIVE_FILE" "$REMOTE_DEST" + else + scp $SSH_OPTS "$ARCHIVE_FILE" "$REMOTE_DEST" + fi + UPLOAD_STATUS=$? + else + echo "Error: Unknown remote type '$REMOTE_TYPE'. Use 'rsync' or 'scp'" + exit 1 + fi + + if [ $UPLOAD_STATUS -eq 0 ]; then + echo "Remote upload successful!" + echo "Remote location: $REMOTE_DEST$(basename "$ARCHIVE_FILE")" + + if [ "$DELETE_AFTER_UPLOAD" = true ]; then + rm "$ARCHIVE_FILE" + echo "Local backup deleted after successful upload" + fi + else + echo "Error: Remote upload failed (exit code: $UPLOAD_STATUS)" + exit 1 + fi fi -echo "Backup process finished." \ No newline at end of file +# ============================================================================ +# SUMMARY +# ============================================================================ + +echo "" +echo "============================================" +echo "Backup completed successfully!" +echo "============================================" +echo "Archive: $(basename "$ARCHIVE_FILE")" +echo "Size: $ARCHIVE_SIZE" +[ "$ENCRYPT" = true ] && echo "Encrypted: Yes (AES256)" +[ "$REMOTE_ENABLED" = true ] && echo "Uploaded to: $REMOTE_HOST:$REMOTE_PATH" +echo "Timestamp: $TIMESTAMP" +echo "" diff --git a/compose.yml b/compose.yml index 07adfae16..05a8ae566 100644 --- a/compose.yml +++ b/compose.yml @@ -114,6 +114,31 @@ services: action: sync+restart target: /etc/nginx/conf.d/default.conf + # Backup service (run with: docker compose --profile backup run --rm backup) + backup: + container_name: arcadia_backup + image: alpine:3.19 + restart: "no" + profiles: + - backup + volumes: + - ./backup.sh:/app/backup.sh:ro + - ./backup.conf:/app/backup.conf:ro + - ./backups:/backups + - /var/run/docker.sock:/var/run/docker.sock:ro + working_dir: /app + environment: + - BACKUP_DESTINATION=/backups + - TZ=Europe/Paris + command: > + sh -c " + apk add --no-cache bash postgresql16-client gnupg docker-cli zip && + chmod +x /app/backup.sh && + /app/backup.sh --db-docker + " + depends_on: + - db + volumes: db_data: redis_data: diff --git a/docs/src/backup.md b/docs/src/backup.md index a84de4118..b15f9b689 100644 --- a/docs/src/backup.md +++ b/docs/src/backup.md @@ -1,115 +1,263 @@ # Backup -This page explains how to create backups of your Arcadia installation, including the database and configuration files. +This page explains how to create backups of your Arcadia installation, including the database, configuration files, and frontend assets. -For all the possible flags and operations, check the `--help` flag: +## Quick Start + +### Basic Backup (Docker) +```bash +./backup.sh --db-docker +``` + +### Encrypted Backup (Recommended) +```bash +./backup.sh --db-docker --encrypt --password "your_secure_password" ``` + +### Full Help +```bash ./backup.sh --help ``` -## Overview +## What Gets Backed Up -The backup process includes: -- Complete database dump (schema + data) -- Environment configuration files (`.env` files) -- Backup metadata and timestamps +| Content | Location in Backup | Description | +|---------|-------------------|-------------| +| Database | `database_full.sql` | Complete PostgreSQL dump (schema + data) | +| Environment Files | `env_files/` | All `.env` files from the project | +| Frontend Assets | `frontend_public/` | Contents of `frontend/public/home/` | +| Backup Info | `backup_info.txt` | Metadata and timestamps | -## Prerequisites +## Encryption -- `zip` utility installed on your system -- For local setup: PostgreSQL client tools (`pg_dump`) +Backups can be encrypted using GPG with AES256 symmetric encryption. -## Quick Backup +### Enable Encryption +```bash +./backup.sh --db-docker --encrypt --password "your_secure_password" +``` -For Docker setup: +### Using a Password File (More Secure) ```bash -./backup.sh --db-docker +echo "your_secure_password" > /path/to/password_file +chmod 600 /path/to/password_file +./backup.sh --db-docker --encrypt --password-file /path/to/password_file ``` -For local/standard setup: +### Output Format +- **Without encryption:** `arcadia_backup_YYYYMMDD_HHMMSS.zip` +- **With encryption:** `arcadia_backup_YYYYMMDD_HHMMSS.tar.gz.gpg` + +### Decrypting a Backup ```bash -./backup.sh +gpg --decrypt --passphrase "your_password" --batch backup.tar.gz.gpg | tar -xzf - ``` -## Backup Script Options +## Custom Destination -The backup script supports both Docker and local PostgreSQL setups with various configuration options. +Save backups to a specific directory: +```bash +./backup.sh --db-docker --destination /path/to/backups +``` -### Docker Mode +## Remote Upload -Use `--db-docker` flag to backup from a containerized database: +Upload backups to a remote server automatically. +### Using rsync ```bash -# Default Docker backup -./backup.sh --db-docker +./backup.sh --db-docker --encrypt --password "secret" \ + --remote-type rsync \ + --remote-host backup.example.com \ + --remote-user backup \ + --remote-path /backups/arcadia +``` -# Custom container name -./backup.sh --db-docker --db-container my_custom_db +### Using scp +```bash +./backup.sh --db-docker --encrypt --password "secret" \ + --remote-type scp \ + --remote-host backup.example.com \ + --remote-user backup \ + --remote-path /backups/arcadia ``` -**Docker mode options:** -- `--db-container`: Docker container name (default: `arcadia_db`) +### Additional Options +- `--remote-key /path/to/key` - Use a specific SSH key +- `--remote-port 2222` - Use a non-standard SSH port +- `--delete-after-upload` - Delete local backup after successful upload + +## Scheduled Backups + +### Option 1: System Cron (Recommended) -### Local Mode +1. **Create configuration file:** + ```bash + cp backup.conf.example backup.conf + nano backup.conf # Edit with your settings + ``` -For standard installations with local PostgreSQL: +2. **Make scripts executable:** + ```bash + chmod +x backup.sh backup-cron.sh + ``` +3. **Add to crontab:** + ```bash + # Daily at 3:00 AM + crontab -e + # Add: 0 3 * * * /path/to/arcadia/backup-cron.sh + ``` + +### Option 2: Docker Container + +Run backup via Docker: ```bash -# Default local backup -./backup.sh +docker compose --profile backup run --rm backup +``` -# Remote database -./backup.sh --db-host db.example.com --db-user myuser +Add to crontab for scheduled Docker backups: +```bash +0 3 * * * cd /path/to/arcadia && docker compose --profile backup run --rm backup +``` + +## Configuration File -# With password -./backup.sh --db-password mypassword +The `backup.conf` file configures scheduled backups. Copy from example: + +```bash +cp backup.conf.example backup.conf ``` -**Local mode options:** -- `--db-host`: Database host (default: `localhost`) -- `--db-port`: Database port (default: `5432`) -- `--db-name`: Database name (default: `arcadia`) -- `--db-user`: Database user (default: `arcadia`) -- `--db-password`: Database password (optional) +### Configuration Options -## Configuration Priority +```bash +# Encryption +BACKUP_ENCRYPT=true +BACKUP_PASSWORD=your_secure_passphrase -The script loads configuration in this order (highest to lowest priority): +# Local storage +BACKUP_DESTINATION=/home/ubuntu/arcadia-backups -1. **Command line arguments** - Override everything -2. **Environment variables** from `.env` files: - - Docker mode: `backend/api/.env.docker` → `backend/api/.env` - - Local mode: `backend/api/.env` -3. **Built-in defaults** +# Remote storage (optional) +BACKUP_REMOTE_TYPE=rsync +BACKUP_REMOTE_HOST=backup.example.com +BACKUP_REMOTE_USER=backup +BACKUP_REMOTE_PATH=/backups/arcadia -## What Gets Backed Up +# Retention policy +BACKUP_RETENTION_DAYS=7 + +# Logging +BACKUP_LOG_FILE=/var/log/arcadia-backup.log +``` + +## Retention Policy + +Old backups are automatically deleted based on `BACKUP_RETENTION_DAYS`: + +```bash +# In backup.conf +BACKUP_RETENTION_DAYS=7 # Keep backups for 7 days +``` -### Database -- Complete PostgreSQL dump using `pg_dump` -- Includes all schema and data -- Uses `--no-owner --no-privileges` for portability +The cron wrapper (`backup-cron.sh`) handles this automatically. -### Configuration Files -- `backend/api/.env` → `backend.env` -- `frontend/.env` → `frontend.env` +## Restore Procedure + +### 1. Decrypt the Backup (if encrypted) +```bash +gpg --decrypt --passphrase "your_password" --batch \ + arcadia_backup_YYYYMMDD_HHMMSS.tar.gz.gpg | tar -xzf - +``` -### Metadata -- Backup timestamp -- Database information -- Setup type (Docker/Local) -- Backup configuration details +### 2. Restore Database +```bash +# Docker setup +docker exec -i arcadia_db psql -U arcadia -d arcadia < backup_*/database_full.sql -## Backup Output +# Local PostgreSQL +psql -h localhost -U arcadia -d arcadia < backup_*/database_full.sql +``` -The script creates: -- Temporary backup directory: `backup_YYYYMMDD_HHMMSS/` -- Final zip archive: `arcadia_backup_YYYYMMDD_HHMMSS.zip` +### 3. Restore Environment Files +```bash +# Copy .env files back to their locations +cp backup_*/env_files/backend/api/.env backend/api/ +cp backup_*/env_files/frontend/.env frontend/ +cp backup_*/env_files/tracker/arcadia_tracker/.env tracker/arcadia_tracker/ +``` -Example backup contents: +### 4. Restore Frontend Assets +```bash +cp -r backup_*/frontend_public/home/* frontend/public/home/ ``` -arcadia_backup_20241201_143022.zip -├── database_full.sql -├── backend.env -├── frontend.env -└── backup_info.txt + +## Command Reference + +``` +Usage: ./backup.sh [OPTIONS] + +DATABASE OPTIONS: + --db-docker Use Docker setup + --db-container NAME Docker container name (default: arcadia_db) + --db-host HOST Database host (default: localhost) + --db-port PORT Database port (default: 5432) + --db-name NAME Database name (default: arcadia) + --db-user USER Database user (default: arcadia) + --db-password PASS Database password + +ENCRYPTION OPTIONS: + --encrypt Enable GPG encryption (AES256) + --password PASS Encryption password + --password-file FILE Read password from file + +DESTINATION OPTIONS: + --destination DIR Output directory + --keep-temp Keep temporary backup directory + +REMOTE UPLOAD OPTIONS: + --remote-type TYPE Upload method: rsync or scp + --remote-host HOST Remote server hostname + --remote-user USER Remote server username + --remote-path PATH Remote destination path + --remote-key FILE SSH private key file + --remote-port PORT SSH port (default: 22) + --delete-after-upload Delete local backup after upload + +GENERAL OPTIONS: + -h, --help Show help message +``` + +## Troubleshooting + +### "Database container not running" +Start the database container: +```bash +docker compose up -d db +``` + +### "gpg: decryption failed" +- Verify you're using the correct password +- Check the file isn't corrupted: `file backup.tar.gz.gpg` + +### "Permission denied" on cron +- Ensure scripts are executable: `chmod +x backup.sh backup-cron.sh` +- Check log file permissions: `touch /var/log/arcadia-backup.log && chmod 666 /var/log/arcadia-backup.log` + +### Remote upload fails +- Test SSH connection: `ssh user@host` +- Check SSH key permissions: `chmod 600 /path/to/key` +- Verify remote path exists: `ssh user@host "mkdir -p /backups/arcadia"` + +## Logs + +When using `backup-cron.sh`, logs are written to: +``` +/var/log/arcadia-backup.log +``` + +View recent logs: +```bash +tail -50 /var/log/arcadia-backup.log ``` From d609c131504763f8882c65f2618f59a25b5043c6 Mon Sep 17 00:00:00 2001 From: NathanJ60 Date: Mon, 15 Dec 2025 21:45:07 +0100 Subject: [PATCH 2/8] feat: add daemon mode for backup container - Add backup-entrypoint.sh to handle daemon/oneshot modes - Add backup-daemon service in compose.yml with internal cron - Update backup.conf.example with daemon mode documentation - Container runs crond in foreground for scheduled backups --- backup-entrypoint.sh | 54 ++++++++++++++++++++++++++++++++++++++++++++ backup.conf.example | 32 +++++++++++++++++--------- compose.yml | 33 ++++++++++++++++++++++++++- 3 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 backup-entrypoint.sh diff --git a/backup-entrypoint.sh b/backup-entrypoint.sh new file mode 100644 index 000000000..053aff9f0 --- /dev/null +++ b/backup-entrypoint.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# Arcadia Backup Entrypoint +# Supports two modes: +# - oneshot: Run backup once and exit (default) +# - daemon: Run crond in foreground with scheduled backups + +set -e + +BACKUP_MODE="${BACKUP_MODE:-oneshot}" +BACKUP_CRON_SCHEDULE="${BACKUP_CRON_SCHEDULE:-0 3 * * *}" + +# ============================================================================ +# DAEMON MODE +# ============================================================================ + +if [ "$BACKUP_MODE" = "daemon" ]; then + echo "============================================" + echo "Arcadia Backup Daemon" + echo "============================================" + echo "Mode: daemon" + echo "Schedule: $BACKUP_CRON_SCHEDULE" + echo "Timezone: ${TZ:-UTC}" + echo "" + + # Create log file + touch /var/log/arcadia-backup.log + + # Create crontab entry + # The cron job runs backup-cron.sh which handles locking, logging, and retention + echo "$BACKUP_CRON_SCHEDULE /app/backup-cron.sh >> /var/log/arcadia-backup.log 2>&1" > /etc/crontabs/root + + echo "Crontab configured:" + cat /etc/crontabs/root + echo "" + echo "Backup daemon started. Logs: /var/log/arcadia-backup.log" + echo "To view logs: docker logs -f arcadia_backup_daemon" + echo "============================================" + echo "" + + # Tail the log file in background so docker logs shows backup output + tail -F /var/log/arcadia-backup.log 2>/dev/null & + + # Run crond in foreground + exec crond -f -l 2 + +# ============================================================================ +# ONESHOT MODE (default) +# ============================================================================ + +else + echo "Mode: oneshot" + exec /app/backup.sh "$@" +fi diff --git a/backup.conf.example b/backup.conf.example index 189ddb77c..3f6556d23 100644 --- a/backup.conf.example +++ b/backup.conf.example @@ -66,18 +66,28 @@ BACKUP_RETENTION_DAYS=7 BACKUP_LOG_FILE=/var/log/arcadia-backup.log # ============================================================================ -# CRON SCHEDULE EXAMPLES +# DAEMON MODE (Docker only) # ============================================================================ -# Add one of these to your crontab (crontab -e): +# When using backup-daemon service, cron runs inside the container. +# Set the schedule here (default: daily at 3:00 AM) + +# BACKUP_CRON_SCHEDULE="0 3 * * *" + +# Schedule examples: +# "0 3 * * *" - Daily at 3:00 AM +# "0 */6 * * *" - Every 6 hours +# "0 2 * * 0" - Weekly on Sunday at 2:00 AM +# "0 4 1 * *" - Monthly on the 1st at 4:00 AM +# "*/5 * * * *" - Every 5 minutes (for testing) + +# ============================================================================ +# MANUAL CRON SETUP (Alternative to daemon mode) +# ============================================================================ +# If you prefer to use the host OS cron instead of the daemon container, +# add one of these to your crontab (crontab -e): # -# Daily at 3:00 AM: +# Using backup-cron.sh directly: # 0 3 * * * /home/ubuntu/arcadia-services/backup-cron.sh # -# Every 6 hours: -# 0 */6 * * * /home/ubuntu/arcadia-services/backup-cron.sh -# -# Weekly on Sunday at 2:00 AM: -# 0 2 * * 0 /home/ubuntu/arcadia-services/backup-cron.sh -# -# Monthly on the 1st at 4:00 AM: -# 0 4 1 * * /home/ubuntu/arcadia-services/backup-cron.sh +# Using Docker oneshot mode: +# 0 3 * * * cd /home/ubuntu/arcadia-services && docker compose --profile backup run --rm backup diff --git a/compose.yml b/compose.yml index 05a8ae566..b6b328c7c 100644 --- a/compose.yml +++ b/compose.yml @@ -114,7 +114,7 @@ services: action: sync+restart target: /etc/nginx/conf.d/default.conf - # Backup service (run with: docker compose --profile backup run --rm backup) + # Backup service - oneshot mode (run with: docker compose --profile backup run --rm backup) backup: container_name: arcadia_backup image: alpine:3.19 @@ -139,6 +139,37 @@ services: depends_on: - db + # Backup daemon - runs cron internally (start with: docker compose --profile backup-daemon up -d backup-daemon) + backup-daemon: + container_name: arcadia_backup_daemon + image: alpine:3.19 + restart: unless-stopped + profiles: + - backup-daemon + volumes: + - ./backup.sh:/app/backup.sh:ro + - ./backup-cron.sh:/app/backup-cron.sh:ro + - ./backup-entrypoint.sh:/app/entrypoint.sh:ro + - ./backup.conf:/app/backup.conf:ro + - ./backups:/backups + - ./backend:/app/backend:ro + - ./frontend:/app/frontend:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + working_dir: /app + environment: + - BACKUP_MODE=daemon + - BACKUP_CRON_SCHEDULE=0 3 * * * + - BACKUP_DESTINATION=/backups + - TZ=Europe/Paris + command: > + sh -c " + apk add --no-cache bash postgresql16-client gnupg docker-cli zip && + chmod +x /app/*.sh && + exec /app/entrypoint.sh + " + depends_on: + - db + volumes: db_data: redis_data: From ce5bf145f50befef7c1fce20fbd436ce306f1984 Mon Sep 17 00:00:00 2001 From: NathanJ60 Date: Mon, 15 Dec 2025 21:50:39 +0100 Subject: [PATCH 3/8] fix: remove :ro from scripts to allow chmod in container --- compose.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compose.yml b/compose.yml index b6b328c7c..e8fb839d7 100644 --- a/compose.yml +++ b/compose.yml @@ -147,9 +147,9 @@ services: profiles: - backup-daemon volumes: - - ./backup.sh:/app/backup.sh:ro - - ./backup-cron.sh:/app/backup-cron.sh:ro - - ./backup-entrypoint.sh:/app/entrypoint.sh:ro + - ./backup.sh:/app/backup.sh + - ./backup-cron.sh:/app/backup-cron.sh + - ./backup-entrypoint.sh:/app/entrypoint.sh - ./backup.conf:/app/backup.conf:ro - ./backups:/backups - ./backend:/app/backend:ro From 604f5069f492d2d9cfc7a605c454d89ec0731a76 Mon Sep 17 00:00:00 2001 From: NathanJ60 Date: Mon, 15 Dec 2025 21:51:55 +0100 Subject: [PATCH 4/8] docs: add daemon mode documentation for backup --- docs/src/backup.md | 52 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/docs/src/backup.md b/docs/src/backup.md index b15f9b689..1442e7718 100644 --- a/docs/src/backup.md +++ b/docs/src/backup.md @@ -89,7 +89,46 @@ Upload backups to a remote server automatically. ## Scheduled Backups -### Option 1: System Cron (Recommended) +### Option 1: Docker Daemon (Recommended) + +The backup container can run in daemon mode with its own internal cron scheduler. This is the easiest setup - no host cron configuration needed. + +1. **Create configuration file:** + ```bash + cp backup.conf.example backup.conf + nano backup.conf # Edit with your settings + ``` + +2. **Start the backup daemon:** + ```bash + docker compose --profile backup-daemon up -d backup-daemon + ``` + +3. **Configure the schedule (optional):** + + Edit the `BACKUP_CRON_SCHEDULE` environment variable in `compose.yml` or set it when starting: + ```bash + BACKUP_CRON_SCHEDULE="0 */6 * * *" docker compose --profile backup-daemon up -d backup-daemon + ``` + + Schedule examples: + - `0 3 * * *` - Daily at 3:00 AM (default) + - `0 */6 * * *` - Every 6 hours + - `0 2 * * 0` - Weekly on Sunday at 2:00 AM + +4. **View logs:** + ```bash + docker logs -f arcadia_backup_daemon + ``` + +5. **Stop the daemon:** + ```bash + docker compose --profile backup-daemon down + ``` + +### Option 2: System Cron + +If you prefer to use your host system's cron instead of the Docker daemon: 1. **Create configuration file:** ```bash @@ -109,9 +148,9 @@ Upload backups to a remote server automatically. # Add: 0 3 * * * /path/to/arcadia/backup-cron.sh ``` -### Option 2: Docker Container +### Option 3: Docker Oneshot via Cron -Run backup via Docker: +Run backup via Docker triggered by host cron: ```bash docker compose --profile backup run --rm backup ``` @@ -252,6 +291,13 @@ docker compose up -d db ## Logs +### Docker Daemon Mode +When using the backup daemon, logs are available via Docker: +```bash +docker logs -f arcadia_backup_daemon +``` + +### System Cron Mode When using `backup-cron.sh`, logs are written to: ``` /var/log/arcadia-backup.log From 4e9d0c682caa1578b96bf4b03e3e909a09a3c7bb Mon Sep 17 00:00:00 2001 From: NathanJ60 Date: Wed, 17 Dec 2025 00:36:21 +0100 Subject: [PATCH 5/8] refactor: merge backup services into single container - Combine backup and backup-daemon into one service - Support both oneshot and daemon modes via BACKUP_MODE env var - Default to oneshot mode for backward compatibility - Daemon mode: docker compose --profile backup run -d -e BACKUP_MODE=daemon backup --- compose.yml | 39 ++++++++------------------------------- 1 file changed, 8 insertions(+), 31 deletions(-) diff --git a/compose.yml b/compose.yml index e8fb839d7..b27a23428 100644 --- a/compose.yml +++ b/compose.yml @@ -114,7 +114,9 @@ services: action: sync+restart target: /etc/nginx/conf.d/default.conf - # Backup service - oneshot mode (run with: docker compose --profile backup run --rm backup) + # Backup service - supports oneshot and daemon modes + # Oneshot (default): docker compose --profile backup run --rm backup + # Daemon mode: docker compose --profile backup run -d -e BACKUP_MODE=daemon backup backup: container_name: arcadia_backup image: alpine:3.19 @@ -123,33 +125,8 @@ services: - backup volumes: - ./backup.sh:/app/backup.sh:ro - - ./backup.conf:/app/backup.conf:ro - - ./backups:/backups - - /var/run/docker.sock:/var/run/docker.sock:ro - working_dir: /app - environment: - - BACKUP_DESTINATION=/backups - - TZ=Europe/Paris - command: > - sh -c " - apk add --no-cache bash postgresql16-client gnupg docker-cli zip && - chmod +x /app/backup.sh && - /app/backup.sh --db-docker - " - depends_on: - - db - - # Backup daemon - runs cron internally (start with: docker compose --profile backup-daemon up -d backup-daemon) - backup-daemon: - container_name: arcadia_backup_daemon - image: alpine:3.19 - restart: unless-stopped - profiles: - - backup-daemon - volumes: - - ./backup.sh:/app/backup.sh - - ./backup-cron.sh:/app/backup-cron.sh - - ./backup-entrypoint.sh:/app/entrypoint.sh + - ./backup-cron.sh:/app/backup-cron.sh:ro + - ./backup-entrypoint.sh:/app/entrypoint.sh:ro - ./backup.conf:/app/backup.conf:ro - ./backups:/backups - ./backend:/app/backend:ro @@ -157,15 +134,15 @@ services: - /var/run/docker.sock:/var/run/docker.sock:ro working_dir: /app environment: - - BACKUP_MODE=daemon - - BACKUP_CRON_SCHEDULE=0 3 * * * + - BACKUP_MODE=${BACKUP_MODE:-oneshot} + - BACKUP_CRON_SCHEDULE=${BACKUP_CRON_SCHEDULE:-0 3 * * *} - BACKUP_DESTINATION=/backups - TZ=Europe/Paris command: > sh -c " apk add --no-cache bash postgresql16-client gnupg docker-cli zip && chmod +x /app/*.sh && - exec /app/entrypoint.sh + exec /app/entrypoint.sh --db-docker " depends_on: - db From 3e03cbc16b7574076bf60fd535515739c85d0bd7 Mon Sep 17 00:00:00 2001 From: NathanJ60 Date: Wed, 17 Dec 2025 00:48:09 +0100 Subject: [PATCH 6/8] fix: support BACKUP_DESTINATION env var and remove chmod in container - backup.sh: read BACKUP_DESTINATION from env if set, fallback to '.' - compose.yml: remove chmod (files mounted read-only, already executable) --- backup.sh | 2 +- compose.yml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/backup.sh b/backup.sh index 1b398bc3e..811ad24aa 100644 --- a/backup.sh +++ b/backup.sh @@ -17,7 +17,7 @@ USE_DOCKER=false ENCRYPT=false BACKUP_PASSWORD="" BACKUP_PASSWORD_FILE="" -BACKUP_DESTINATION="." +BACKUP_DESTINATION="${BACKUP_DESTINATION:-.}" KEEP_TEMP=false REMOTE_ENABLED=false REMOTE_TYPE="" diff --git a/compose.yml b/compose.yml index b27a23428..3e0fa55a9 100644 --- a/compose.yml +++ b/compose.yml @@ -141,7 +141,6 @@ services: command: > sh -c " apk add --no-cache bash postgresql16-client gnupg docker-cli zip && - chmod +x /app/*.sh && exec /app/entrypoint.sh --db-docker " depends_on: From 6ea5f3952f6b54dac822c0387d5fe855a1846990 Mon Sep 17 00:00:00 2001 From: NathanJ60 Date: Wed, 17 Dec 2025 00:50:06 +0100 Subject: [PATCH 7/8] docs: update backup documentation for single container - Update commands to use BACKUP_MODE=daemon instead of separate service - Change container name references from arcadia_backup_daemon to arcadia_backup - Simplify daemon mode instructions --- docs/src/backup.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/src/backup.md b/docs/src/backup.md index 1442e7718..9af7ef641 100644 --- a/docs/src/backup.md +++ b/docs/src/backup.md @@ -101,14 +101,14 @@ The backup container can run in daemon mode with its own internal cron scheduler 2. **Start the backup daemon:** ```bash - docker compose --profile backup-daemon up -d backup-daemon + BACKUP_MODE=daemon docker compose --profile backup up -d backup ``` 3. **Configure the schedule (optional):** - Edit the `BACKUP_CRON_SCHEDULE` environment variable in `compose.yml` or set it when starting: + Set `BACKUP_CRON_SCHEDULE` when starting: ```bash - BACKUP_CRON_SCHEDULE="0 */6 * * *" docker compose --profile backup-daemon up -d backup-daemon + BACKUP_MODE=daemon BACKUP_CRON_SCHEDULE="0 */6 * * *" docker compose --profile backup up -d backup ``` Schedule examples: @@ -118,12 +118,12 @@ The backup container can run in daemon mode with its own internal cron scheduler 4. **View logs:** ```bash - docker logs -f arcadia_backup_daemon + docker logs -f arcadia_backup ``` 5. **Stop the daemon:** ```bash - docker compose --profile backup-daemon down + docker compose --profile backup down ``` ### Option 2: System Cron @@ -294,7 +294,7 @@ docker compose up -d db ### Docker Daemon Mode When using the backup daemon, logs are available via Docker: ```bash -docker logs -f arcadia_backup_daemon +docker logs -f arcadia_backup ``` ### System Cron Mode From fe3caf0a03e7162a7381df54c64edd7aab26cddf Mon Sep 17 00:00:00 2001 From: NathanJ60 Date: Thu, 18 Dec 2025 02:06:15 +0100 Subject: [PATCH 8/8] fix: simplify backup container volumes and use /tmp for temp files - Mount whole project folder instead of individual files - Use /tmp for backup temp directory (read-only mount compatible) - Fix entrypoint script path reference - Update docs wording: frontend assets -> frontend custom assets --- backup.sh | 6 +++--- compose.yml | 9 ++------- docs/src/backup.md | 2 +- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/backup.sh b/backup.sh index 811ad24aa..c775187a6 100644 --- a/backup.sh +++ b/backup.sh @@ -194,7 +194,7 @@ done # LOAD CONFIGURATION # ============================================================================ -BACKUP_DIR="backup_$(date +%Y%m%d_%H%M%S)" +BACKUP_DIR="/tmp/backup_$(date +%Y%m%d_%H%M%S)" TIMESTAMP=$(date +%Y%m%d_%H%M%S) # Source configuration from backend .env files @@ -408,7 +408,7 @@ if [ "$ENCRYPT" = true ]; then exit 1 fi - ARCHIVE_FILE="arcadia_backup_${TIMESTAMP}.tar.gz.gpg" + ARCHIVE_FILE="/tmp/arcadia_backup_${TIMESTAMP}.tar.gz.gpg" # Create encrypted archive tar -czf - -C "$(dirname "$BACKUP_DIR")" "$(basename "$BACKUP_DIR")" | \ @@ -426,7 +426,7 @@ if [ "$ENCRYPT" = true ]; then exit 1 fi else - ARCHIVE_FILE="arcadia_backup_${TIMESTAMP}.zip" + ARCHIVE_FILE="/tmp/arcadia_backup_${TIMESTAMP}.zip" if command -v zip >/dev/null 2>&1; then zip -rq "$ARCHIVE_FILE" "$BACKUP_DIR" diff --git a/compose.yml b/compose.yml index 3e0fa55a9..29e19e422 100644 --- a/compose.yml +++ b/compose.yml @@ -124,13 +124,8 @@ services: profiles: - backup volumes: - - ./backup.sh:/app/backup.sh:ro - - ./backup-cron.sh:/app/backup-cron.sh:ro - - ./backup-entrypoint.sh:/app/entrypoint.sh:ro - - ./backup.conf:/app/backup.conf:ro + - .:/app:ro - ./backups:/backups - - ./backend:/app/backend:ro - - ./frontend:/app/frontend:ro - /var/run/docker.sock:/var/run/docker.sock:ro working_dir: /app environment: @@ -141,7 +136,7 @@ services: command: > sh -c " apk add --no-cache bash postgresql16-client gnupg docker-cli zip && - exec /app/entrypoint.sh --db-docker + exec /app/backup-entrypoint.sh --db-docker " depends_on: - db diff --git a/docs/src/backup.md b/docs/src/backup.md index 9af7ef641..edb8e019c 100644 --- a/docs/src/backup.md +++ b/docs/src/backup.md @@ -1,6 +1,6 @@ # Backup -This page explains how to create backups of your Arcadia installation, including the database, configuration files, and frontend assets. +This page explains how to create backups of your Arcadia installation, including the database, configuration files, and frontend custom assets. ## Quick Start