Dev ops prep

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-21 17:53:42 +00:00
parent 3398130bff
commit 4a200822bb
14 changed files with 2027 additions and 0 deletions

193
scripts/ops/backup-db.sh Normal file
View File

@@ -0,0 +1,193 @@
#!/bin/bash
# =============================================================================
# IGNY8 Database Backup Script
# =============================================================================
# Usage: ./backup-db.sh [daily|weekly|monthly|pre-deploy]
# Creates compressed database backup with automatic retention
# =============================================================================
set -e
# Configuration
BACKUP_TYPE="${1:-daily}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DATE_FOLDER=$(date +%Y%m%d)
# Directories
BACKUP_ROOT="/data/backups"
DAILY_DIR="${BACKUP_ROOT}/daily"
WEEKLY_DIR="${BACKUP_ROOT}/weekly"
MONTHLY_DIR="${BACKUP_ROOT}/monthly"
PREDEPLOY_DIR="${BACKUP_ROOT}/pre-deploy"
# Database settings (from docker environment)
DB_CONTAINER="postgres"
DB_NAME="igny8_db"
DB_USER="igny8"
# Retention settings
DAILY_RETENTION_DAYS=7
WEEKLY_RETENTION_WEEKS=4
MONTHLY_RETENTION_MONTHS=12
PREDEPLOY_RETENTION_COUNT=5
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log() {
echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
log_success() {
log "${GREEN}$1${NC}"
}
log_warn() {
log "${YELLOW}⚠️ $1${NC}"
}
log_error() {
log "${RED}$1${NC}"
}
# Create directories if they don't exist
create_dirs() {
mkdir -p "${DAILY_DIR}"
mkdir -p "${WEEKLY_DIR}"
mkdir -p "${MONTHLY_DIR}"
mkdir -p "${PREDEPLOY_DIR}"
}
# Determine backup destination based on type
get_backup_path() {
case $BACKUP_TYPE in
daily)
echo "${DAILY_DIR}/${DATE_FOLDER}"
;;
weekly)
echo "${WEEKLY_DIR}/week_$(date +%Y_%W)"
;;
monthly)
echo "${MONTHLY_DIR}/$(date +%Y_%m)"
;;
pre-deploy)
echo "${PREDEPLOY_DIR}/${TIMESTAMP}"
;;
*)
log_error "Invalid backup type: $BACKUP_TYPE"
exit 1
;;
esac
}
# Perform database backup
backup_database() {
local backup_dir="$1"
local backup_file="${backup_dir}/db_${DB_NAME}_${TIMESTAMP}.sql.gz"
mkdir -p "${backup_dir}"
log "Starting database backup: ${DB_NAME}"
log "Destination: ${backup_file}"
# Check if postgres container is running
if ! docker ps --format '{{.Names}}' | grep -q "^${DB_CONTAINER}$"; then
log_error "PostgreSQL container '${DB_CONTAINER}' is not running"
exit 1
fi
# Perform backup with compression
if docker exec "${DB_CONTAINER}" pg_dump -U "${DB_USER}" "${DB_NAME}" | gzip > "${backup_file}"; then
local size=$(du -h "${backup_file}" | cut -f1)
log_success "Database backup complete: ${backup_file} (${size})"
# Create latest symlink
ln -sf "${backup_file}" "${BACKUP_ROOT}/latest_db.sql.gz"
return 0
else
log_error "Database backup failed"
rm -f "${backup_file}"
return 1
fi
}
# Clean up old backups based on retention policy
cleanup_old_backups() {
log "Cleaning up old backups..."
# Daily backups - keep last N days
if [[ -d "${DAILY_DIR}" ]]; then
find "${DAILY_DIR}" -type d -mtime +${DAILY_RETENTION_DAYS} -exec rm -rf {} \; 2>/dev/null || true
local daily_count=$(find "${DAILY_DIR}" -type f -name "*.sql.gz" | wc -l)
log "Daily backups: ${daily_count} files"
fi
# Weekly backups - keep last N weeks
if [[ -d "${WEEKLY_DIR}" ]]; then
find "${WEEKLY_DIR}" -type d -mtime +$((WEEKLY_RETENTION_WEEKS * 7)) -exec rm -rf {} \; 2>/dev/null || true
local weekly_count=$(find "${WEEKLY_DIR}" -type f -name "*.sql.gz" | wc -l)
log "Weekly backups: ${weekly_count} files"
fi
# Monthly backups - keep last N months
if [[ -d "${MONTHLY_DIR}" ]]; then
find "${MONTHLY_DIR}" -type d -mtime +$((MONTHLY_RETENTION_MONTHS * 30)) -exec rm -rf {} \; 2>/dev/null || true
local monthly_count=$(find "${MONTHLY_DIR}" -type f -name "*.sql.gz" | wc -l)
log "Monthly backups: ${monthly_count} files"
fi
# Pre-deploy backups - keep last N
if [[ -d "${PREDEPLOY_DIR}" ]]; then
local predeploy_dirs=$(ls -dt "${PREDEPLOY_DIR}"/*/ 2>/dev/null | tail -n +$((PREDEPLOY_RETENTION_COUNT + 1)))
if [[ -n "$predeploy_dirs" ]]; then
echo "$predeploy_dirs" | xargs rm -rf 2>/dev/null || true
fi
local predeploy_count=$(find "${PREDEPLOY_DIR}" -maxdepth 1 -type d | wc -l)
log "Pre-deploy backups: $((predeploy_count - 1)) snapshots"
fi
log_success "Cleanup complete"
}
# Display backup summary
show_summary() {
echo ""
echo "=========================================="
echo "BACKUP SUMMARY"
echo "=========================================="
echo "Type: ${BACKUP_TYPE}"
echo "Database: ${DB_NAME}"
echo "Timestamp: ${TIMESTAMP}"
echo ""
echo "Disk Usage:"
du -sh "${BACKUP_ROOT}"/* 2>/dev/null || echo "No backups yet"
echo ""
echo "Latest backup: $(readlink -f ${BACKUP_ROOT}/latest_db.sql.gz 2>/dev/null || echo 'None')"
echo "=========================================="
}
# Main execution
main() {
echo "=========================================="
echo "IGNY8 Database Backup"
echo "=========================================="
echo ""
create_dirs
local backup_path=$(get_backup_path)
if backup_database "${backup_path}"; then
cleanup_old_backups
show_summary
exit 0
else
exit 1
fi
}
main "$@"

150
scripts/ops/backup-full.sh Normal file
View File

@@ -0,0 +1,150 @@
#!/bin/bash
# =============================================================================
# IGNY8 Full Backup Script
# =============================================================================
# Creates complete backup: database + configuration + code snapshot
# Usage: ./backup-full.sh [weekly|monthly]
# =============================================================================
set -e
# Configuration
BACKUP_TYPE="${1:-weekly}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
APP_DIR="/data/app/igny8"
BACKUP_ROOT="/data/backups"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() {
echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
log_success() {
log "${GREEN}$1${NC}"
}
log_error() {
log "${RED}$1${NC}"
}
# Determine backup directory
case $BACKUP_TYPE in
weekly)
BACKUP_DIR="${BACKUP_ROOT}/weekly/week_$(date +%Y_%W)"
;;
monthly)
BACKUP_DIR="${BACKUP_ROOT}/monthly/$(date +%Y_%m)"
;;
*)
log_error "Invalid backup type. Use: weekly or monthly"
exit 1
;;
esac
mkdir -p "${BACKUP_DIR}"
echo "=========================================="
echo "IGNY8 Full Backup"
echo "=========================================="
echo "Type: ${BACKUP_TYPE}"
echo "Destination: ${BACKUP_DIR}"
echo ""
# Step 1: Database backup
log "Step 1/4: Backing up database..."
/data/app/igny8/scripts/ops/backup-db.sh "${BACKUP_TYPE}" || {
log_error "Database backup failed"
exit 1
}
# Step 2: Configuration backup
log "Step 2/4: Backing up configuration files..."
CONFIG_BACKUP="${BACKUP_DIR}/config_${TIMESTAMP}.tar.gz"
tar -czf "${CONFIG_BACKUP}" \
-C "${APP_DIR}" \
.env \
.env.staging 2>/dev/null \
docker-compose.app.yml \
docker-compose.staging.yml 2>/dev/null \
--ignore-failed-read || true
if [[ -f "${CONFIG_BACKUP}" ]]; then
log_success "Config backup: ${CONFIG_BACKUP}"
else
log_error "Config backup failed"
fi
# Step 3: Code snapshot (without node_modules, __pycache__, etc.)
log "Step 3/4: Creating code snapshot..."
CODE_BACKUP="${BACKUP_DIR}/code_${TIMESTAMP}.tar.gz"
tar -czf "${CODE_BACKUP}" \
-C "/data/app" \
--exclude='igny8/node_modules' \
--exclude='igny8/frontend/node_modules' \
--exclude='igny8/__pycache__' \
--exclude='igny8/**/__pycache__' \
--exclude='igny8/*.pyc' \
--exclude='igny8/**/*.pyc' \
--exclude='igny8/.git' \
--exclude='igny8/backend/staticfiles' \
--exclude='igny8/frontend/dist' \
--exclude='igny8/backend/media' \
--exclude='igny8/logs' \
igny8/ 2>/dev/null || true
if [[ -f "${CODE_BACKUP}" ]]; then
local size=$(du -h "${CODE_BACKUP}" | cut -f1)
log_success "Code backup: ${CODE_BACKUP} (${size})"
else
log_error "Code backup failed"
fi
# Step 4: Media files (if they exist)
log "Step 4/4: Backing up media files..."
MEDIA_DIR="${APP_DIR}/backend/media"
if [[ -d "${MEDIA_DIR}" ]] && [[ "$(ls -A ${MEDIA_DIR})" ]]; then
MEDIA_BACKUP="${BACKUP_DIR}/media_${TIMESTAMP}.tar.gz"
tar -czf "${MEDIA_BACKUP}" -C "${APP_DIR}/backend" media/ 2>/dev/null || true
if [[ -f "${MEDIA_BACKUP}" ]]; then
local media_size=$(du -h "${MEDIA_BACKUP}" | cut -f1)
log_success "Media backup: ${MEDIA_BACKUP} (${media_size})"
fi
else
log "No media files to backup"
fi
# Create manifest
MANIFEST="${BACKUP_DIR}/manifest.txt"
cat > "${MANIFEST}" << EOF
IGNY8 Full Backup Manifest
===========================
Timestamp: ${TIMESTAMP}
Type: ${BACKUP_TYPE}
Server: $(hostname)
Files:
$(ls -la "${BACKUP_DIR}")
Sizes:
$(du -sh "${BACKUP_DIR}"/*)
Git Info:
$(cd "${APP_DIR}" && git log -1 --format='Commit: %H%nDate: %ci%nMessage: %s' 2>/dev/null || echo 'Not a git repo')
EOF
echo ""
echo "=========================================="
echo "BACKUP COMPLETE"
echo "=========================================="
echo "Location: ${BACKUP_DIR}"
echo ""
echo "Contents:"
ls -lh "${BACKUP_DIR}"
echo ""
echo "Total size: $(du -sh ${BACKUP_DIR} | cut -f1)"
echo "=========================================="

View File

@@ -0,0 +1,145 @@
#!/bin/bash
# =============================================================================
# IGNY8 Deploy to Production
# =============================================================================
# Safely deploys code to production with backup and rollback capability
# Usage: ./deploy-production.sh
# =============================================================================
set -e
APP_DIR="/data/app/igny8"
COMPOSE_FILE="docker-compose.app.yml"
PROJECT_NAME="igny8-app"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log() {
echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1"
}
log_success() {
echo -e "${GREEN}$1${NC}"
}
log_warn() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
log_error() {
echo -e "${RED}$1${NC}"
}
echo "=========================================="
echo "IGNY8 PRODUCTION DEPLOYMENT"
echo "=========================================="
echo ""
# Safety confirmation
log_warn "This will deploy to PRODUCTION!"
log_warn "Make sure you have tested on staging first."
echo ""
read -p "Type 'deploy' to continue: " confirm
if [[ "${confirm}" != "deploy" ]]; then
log "Deployment cancelled"
exit 0
fi
cd "${APP_DIR}"
# Verify on main branch
CURRENT_BRANCH=$(git branch --show-current)
if [[ "${CURRENT_BRANCH}" != "main" ]]; then
log_error "Not on main branch (currently on: ${CURRENT_BRANCH})"
log "Switch to main: git checkout main && git pull"
exit 1
fi
# Step 1: Create pre-deploy backup
log "Step 1/8: Creating pre-deploy backup..."
/data/app/igny8/scripts/ops/backup-db.sh pre-deploy
log_success "Backup created"
# Step 2: Tag current images for rollback
log "Step 2/8: Tagging current images for rollback..."
docker tag igny8-backend:latest igny8-backend:rollback 2>/dev/null || true
docker tag igny8-frontend-dev:latest igny8-frontend-dev:rollback 2>/dev/null || true
docker tag igny8-marketing-dev:latest igny8-marketing-dev:rollback 2>/dev/null || true
log_success "Rollback images tagged"
# Step 3: Pull latest code
log "Step 3/8: Pulling latest code..."
git pull origin main
log_success "Code updated"
# Step 4: Build new images
log "Step 4/8: Building new images..."
docker build -t igny8-backend:latest -f backend/Dockerfile backend/
docker build -t igny8-frontend-dev:latest -f frontend/Dockerfile.dev frontend/
docker build -t igny8-marketing-dev:latest -f frontend/Dockerfile.marketing.dev frontend/
log_success "Images built"
# Step 5: Check for pending migrations
log "Step 5/8: Checking for pending migrations..."
PENDING=$(docker exec igny8_backend python manage.py showmigrations --plan 2>/dev/null | grep "\[ \]" || true)
if [[ -n "${PENDING}" ]]; then
log_warn "Pending migrations found:"
echo "${PENDING}"
read -p "Apply migrations? (yes/no): " apply_migrations
if [[ "${apply_migrations}" != "yes" ]]; then
log "Skipping migrations"
fi
fi
# Step 6: Restart containers
log "Step 6/8: Restarting containers..."
docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" down
docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" up -d
# Step 7: Wait for backend to be healthy
log "Step 7/8: Waiting for backend to be healthy..."
for i in {1..30}; do
if docker exec igny8_backend python -c "import urllib.request; urllib.request.urlopen('http://localhost:8010/api/v1/system/status/').read()" 2>/dev/null; then
log_success "Backend is healthy"
break
fi
if [[ $i -eq 30 ]]; then
log_error "Backend failed to become healthy"
log_error "ROLLING BACK..."
/data/app/igny8/scripts/ops/rollback.sh --auto
exit 1
fi
echo -n "."
sleep 2
done
# Step 8: Apply migrations and collect static
log "Step 8/8: Applying migrations and collecting static..."
if [[ "${apply_migrations}" == "yes" ]] || [[ -z "${PENDING}" ]]; then
docker exec igny8_backend python manage.py migrate --noinput
fi
docker exec igny8_backend python manage.py collectstatic --noinput
echo ""
echo "=========================================="
log_success "PRODUCTION DEPLOYMENT COMPLETE"
echo "=========================================="
echo ""
echo "Production is live!"
echo " App: https://app.igny8.com"
echo " API: https://api.igny8.com"
echo ""
echo "Monitor logs for 10 minutes:"
echo " docker compose -f ${COMPOSE_FILE} -p ${PROJECT_NAME} logs -f"
echo ""
echo "If issues occur, rollback:"
echo " /data/app/igny8/scripts/ops/rollback.sh"
echo ""
echo "Container status:"
docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" ps
echo "=========================================="

View File

@@ -0,0 +1,125 @@
#!/bin/bash
# =============================================================================
# IGNY8 Deploy to Staging
# =============================================================================
# Safely deploys code to staging environment
# Usage: ./deploy-staging.sh
# =============================================================================
set -e
APP_DIR="/data/app/igny8"
COMPOSE_FILE="docker-compose.staging.yml"
PROJECT_NAME="igny8-staging"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log() {
echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1"
}
log_success() {
echo -e "${GREEN}$1${NC}"
}
log_warn() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
log_error() {
echo -e "${RED}$1${NC}"
}
echo "=========================================="
echo "IGNY8 STAGING DEPLOYMENT"
echo "=========================================="
echo ""
cd "${APP_DIR}"
# Check if staging compose file exists
if [[ ! -f "${COMPOSE_FILE}" ]]; then
log_error "Staging compose file not found: ${COMPOSE_FILE}"
log "Please create it from the documentation template"
exit 1
fi
# Check if .env.staging exists
if [[ ! -f ".env.staging" ]]; then
log_error "Staging environment file not found: .env.staging"
log "Please create it from .env.example"
exit 1
fi
# Step 1: Pull latest code
log "Step 1/6: Pulling latest code..."
git fetch origin
CURRENT_BRANCH=$(git branch --show-current)
log "Current branch: ${CURRENT_BRANCH}"
if [[ "${CURRENT_BRANCH}" == "staging" ]]; then
git pull origin staging
elif [[ "${CURRENT_BRANCH}" == "main" ]]; then
log_warn "On main branch, switching to staging..."
git checkout staging
git pull origin staging
else
log_warn "On feature branch '${CURRENT_BRANCH}', not pulling..."
fi
# Step 2: Build staging images
log "Step 2/6: Building staging images..."
docker build -t igny8-backend:staging -f backend/Dockerfile backend/
docker build -t igny8-frontend-dev:staging -f frontend/Dockerfile.dev frontend/
docker build -t igny8-marketing-dev:staging -f frontend/Dockerfile.marketing.dev frontend/
log_success "Images built"
# Step 3: Stop existing staging containers
log "Step 3/6: Stopping existing staging containers..."
docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" down 2>/dev/null || true
# Step 4: Start staging containers
log "Step 4/6: Starting staging containers..."
docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" up -d
# Step 5: Wait for backend to be healthy
log "Step 5/6: Waiting for backend to be healthy..."
for i in {1..30}; do
if docker exec igny8_staging_backend python -c "import urllib.request; urllib.request.urlopen('http://localhost:8010/api/v1/system/status/').read()" 2>/dev/null; then
log_success "Backend is healthy"
break
fi
if [[ $i -eq 30 ]]; then
log_error "Backend failed to become healthy"
docker logs igny8_staging_backend --tail 50
exit 1
fi
echo -n "."
sleep 2
done
# Step 6: Run migrations
log "Step 6/6: Running database migrations..."
docker exec igny8_staging_backend python manage.py migrate --noinput
docker exec igny8_staging_backend python manage.py collectstatic --noinput
echo ""
echo "=========================================="
log_success "STAGING DEPLOYMENT COMPLETE"
echo "=========================================="
echo ""
echo "Staging environment is ready:"
echo " App: https://staging.igny8.com"
echo " API: https://staging-api.igny8.com"
echo ""
echo "View logs:"
echo " docker compose -f ${COMPOSE_FILE} -p ${PROJECT_NAME} logs -f"
echo ""
echo "Container status:"
docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" ps
echo "=========================================="

199
scripts/ops/health-check.sh Normal file
View File

@@ -0,0 +1,199 @@
#!/bin/bash
# =============================================================================
# IGNY8 Health Check Script
# =============================================================================
# Checks health of all services and reports status
# Usage: ./health-check.sh [--quiet]
# =============================================================================
set -e
QUIET="${1}"
EXIT_CODE=0
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Output functions
output() {
if [[ "${QUIET}" != "--quiet" ]]; then
echo -e "$1"
fi
}
check_passed() {
output "${GREEN}$1${NC}"
}
check_failed() {
output "${RED}$1${NC}"
EXIT_CODE=1
}
check_warn() {
output "${YELLOW}⚠️ $1${NC}"
}
# Check HTTP endpoint
check_endpoint() {
local name="$1"
local url="$2"
local timeout="${3:-10}"
local response
local http_code
local time_total
response=$(curl -s -o /dev/null -w "%{http_code}|%{time_total}" --max-time "${timeout}" "${url}" 2>/dev/null || echo "000|0")
http_code=$(echo "${response}" | cut -d'|' -f1)
time_total=$(echo "${response}" | cut -d'|' -f2)
if [[ "${http_code}" == "200" ]]; then
check_passed "${name}: OK (${time_total}s)"
return 0
else
check_failed "${name}: FAILED (HTTP ${http_code})"
return 1
fi
}
# Check container status
check_container() {
local name="$1"
if docker ps --format '{{.Names}}' | grep -q "^${name}$"; then
local status=$(docker inspect --format='{{.State.Health.Status}}' "${name}" 2>/dev/null || echo "none")
if [[ "${status}" == "healthy" ]]; then
check_passed "Container ${name}: Running (healthy)"
elif [[ "${status}" == "none" ]]; then
check_passed "Container ${name}: Running (no healthcheck)"
else
check_warn "Container ${name}: Running (${status})"
fi
return 0
else
check_failed "Container ${name}: NOT RUNNING"
return 1
fi
}
# Check disk space
check_disk() {
local path="$1"
local threshold="${2:-80}"
local usage=$(df "${path}" | tail -1 | awk '{print $5}' | sed 's/%//')
if [[ ${usage} -lt ${threshold} ]]; then
check_passed "Disk ${path}: ${usage}% used"
elif [[ ${usage} -lt 90 ]]; then
check_warn "Disk ${path}: ${usage}% used (warning)"
else
check_failed "Disk ${path}: ${usage}% used (critical)"
fi
}
# Check Redis connection
check_redis() {
if docker exec redis redis-cli ping 2>/dev/null | grep -q "PONG"; then
check_passed "Redis: Connected"
return 0
else
check_failed "Redis: NOT RESPONDING"
return 1
fi
}
# Check PostgreSQL connection
check_postgres() {
if docker exec postgres pg_isready -U igny8 2>/dev/null | grep -q "accepting"; then
check_passed "PostgreSQL: Accepting connections"
return 0
else
check_failed "PostgreSQL: NOT READY"
return 1
fi
}
# Check Celery queue
check_celery_queue() {
local queue_length=$(docker exec redis redis-cli llen celery 2>/dev/null || echo "error")
if [[ "${queue_length}" == "error" ]]; then
check_warn "Celery queue: Cannot check"
elif [[ ${queue_length} -lt 50 ]]; then
check_passed "Celery queue: ${queue_length} tasks"
elif [[ ${queue_length} -lt 200 ]]; then
check_warn "Celery queue: ${queue_length} tasks (high)"
else
check_failed "Celery queue: ${queue_length} tasks (critical)"
fi
}
# Main health check
main() {
if [[ "${QUIET}" != "--quiet" ]]; then
echo "=========================================="
echo "IGNY8 Health Check"
echo "$(date '+%Y-%m-%d %H:%M:%S')"
echo "=========================================="
echo ""
fi
# Infrastructure
output "INFRASTRUCTURE:"
check_postgres
check_redis
check_disk "/data"
echo ""
# Production containers
output "PRODUCTION CONTAINERS:"
check_container "igny8_backend"
check_container "igny8_frontend"
check_container "igny8_celery_worker"
check_container "igny8_celery_beat"
echo ""
# Production endpoints
output "PRODUCTION ENDPOINTS:"
check_endpoint "API Status" "http://localhost:8011/api/v1/system/status/" 5
check_endpoint "Frontend" "http://localhost:8021" 5
echo ""
# Celery
output "BACKGROUND TASKS:"
check_celery_queue
echo ""
# Staging (if running)
if docker ps --format '{{.Names}}' | grep -q "igny8_staging_backend"; then
output "STAGING CONTAINERS:"
check_container "igny8_staging_backend"
check_container "igny8_staging_frontend"
echo ""
output "STAGING ENDPOINTS:"
check_endpoint "Staging API" "http://localhost:8012/api/v1/system/status/" 5
check_endpoint "Staging Frontend" "http://localhost:8024" 5
echo ""
fi
# Summary
if [[ "${QUIET}" != "--quiet" ]]; then
echo "=========================================="
if [[ ${EXIT_CODE} -eq 0 ]]; then
echo -e "${GREEN}All checks passed${NC}"
else
echo -e "${RED}Some checks failed${NC}"
fi
echo "=========================================="
fi
exit ${EXIT_CODE}
}
main "$@"

49
scripts/ops/igny8-cron Normal file
View File

@@ -0,0 +1,49 @@
# =============================================================================
# IGNY8 Automated Tasks (Cron Configuration)
# =============================================================================
# Install: sudo cp igny8-cron /etc/cron.d/igny8
# Verify: sudo crontab -l -u root
# =============================================================================
# Environment
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# =============================================================================
# BACKUP JOBS
# =============================================================================
# Daily database backup at 1:00 AM
0 1 * * * root /data/app/igny8/scripts/ops/backup-db.sh daily >> /data/logs/backup.log 2>&1
# Weekly full backup on Sunday at 2:00 AM
0 2 * * 0 root /data/app/igny8/scripts/ops/backup-full.sh weekly >> /data/logs/backup.log 2>&1
# Monthly full backup on 1st of month at 3:00 AM
0 3 1 * * root /data/app/igny8/scripts/ops/backup-full.sh monthly >> /data/logs/backup.log 2>&1
# =============================================================================
# HEALTH & MONITORING
# =============================================================================
# Health check every 5 minutes (logs only failures)
*/5 * * * * root /data/app/igny8/scripts/ops/health-check.sh --quiet || echo "[$(date)] Health check failed" >> /data/logs/health.log 2>&1
# =============================================================================
# MAINTENANCE
# =============================================================================
# Log rotation daily at midnight
0 0 * * * root /data/app/igny8/scripts/ops/log-rotate.sh >> /data/logs/maintenance.log 2>&1
# Docker cleanup weekly on Saturday at 4:00 AM
0 4 * * 6 root docker system prune -f >> /data/logs/maintenance.log 2>&1
# =============================================================================
# OPTIONAL: External backup sync (uncomment if using remote backup)
# =============================================================================
# Sync backups to remote storage daily at 5:00 AM
# 0 5 * * * root rsync -avz /data/backups/ user@backup-server:/backups/igny8/ >> /data/logs/backup.log 2>&1
# =============================================================================

108
scripts/ops/log-rotate.sh Normal file
View File

@@ -0,0 +1,108 @@
#!/bin/bash
# =============================================================================
# IGNY8 Log Rotation Script
# =============================================================================
# Rotates and compresses old log files
# Usage: ./log-rotate.sh
# =============================================================================
set -e
LOG_DIR="/data/logs"
ARCHIVE_DIR="${LOG_DIR}/archive"
RETENTION_DAYS=30
# Colors
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
log() {
echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1"
}
log_success() {
echo -e "${GREEN}$1${NC}"
}
echo "=========================================="
echo "IGNY8 Log Rotation"
echo "$(date '+%Y-%m-%d %H:%M:%S')"
echo "=========================================="
echo ""
# Create archive directory
mkdir -p "${ARCHIVE_DIR}"
# Rotate function
rotate_log() {
local log_file="$1"
local max_size="${2:-50M}"
if [[ ! -f "${log_file}" ]]; then
return
fi
local size=$(stat -f%z "${log_file}" 2>/dev/null || stat -c%s "${log_file}" 2>/dev/null || echo 0)
local max_bytes=$((50 * 1024 * 1024)) # 50MB
if [[ ${size} -gt ${max_bytes} ]]; then
local basename=$(basename "${log_file}")
local timestamp=$(date +%Y%m%d_%H%M%S)
local archive_file="${ARCHIVE_DIR}/${basename}.${timestamp}.gz"
log "Rotating ${log_file} ($(numfmt --to=iec ${size}))"
gzip -c "${log_file}" > "${archive_file}"
> "${log_file}" # Truncate original
log_success "Archived to ${archive_file}"
fi
}
# Rotate production logs
log "Checking production logs..."
for log_file in "${LOG_DIR}/production"/*.log; do
[[ -f "${log_file}" ]] && rotate_log "${log_file}"
done
# Rotate staging logs
log "Checking staging logs..."
for log_file in "${LOG_DIR}/staging"/*.log; do
[[ -f "${log_file}" ]] && rotate_log "${log_file}"
done
# Rotate Caddy logs
log "Checking Caddy logs..."
for log_file in "${LOG_DIR}/caddy"/*.log; do
[[ -f "${log_file}" ]] && rotate_log "${log_file}"
done
# Clean up old archives
log "Cleaning old archives (> ${RETENTION_DAYS} days)..."
DELETED=$(find "${ARCHIVE_DIR}" -name "*.gz" -mtime +${RETENTION_DAYS} -delete -print | wc -l)
if [[ ${DELETED} -gt 0 ]]; then
log "Deleted ${DELETED} old archive files"
fi
# Docker container logs
log "Pruning Docker container logs..."
for container in igny8_backend igny8_frontend igny8_celery_worker igny8_celery_beat; do
if docker ps -a --format '{{.Names}}' | grep -q "^${container}$"; then
LOG_FILE=$(docker inspect --format='{{.LogPath}}' "${container}" 2>/dev/null || true)
if [[ -n "${LOG_FILE}" ]] && [[ -f "${LOG_FILE}" ]]; then
local size=$(stat -c%s "${LOG_FILE}" 2>/dev/null || echo 0)
if [[ ${size} -gt $((100 * 1024 * 1024)) ]]; then # 100MB
log "Truncating Docker log for ${container}"
truncate -s 0 "${LOG_FILE}"
fi
fi
fi
done
# Report disk usage
echo ""
echo "Log disk usage:"
du -sh "${LOG_DIR}"/* 2>/dev/null || echo "No logs found"
echo ""
log_success "Log rotation complete"
echo "=========================================="

142
scripts/ops/restore-db.sh Normal file
View File

@@ -0,0 +1,142 @@
#!/bin/bash
# =============================================================================
# IGNY8 Database Restore Script
# =============================================================================
# Usage: ./restore-db.sh <backup_file> [target_db]
# Restores database from a backup file (.sql or .sql.gz)
# =============================================================================
set -e
# Configuration
BACKUP_FILE="${1}"
TARGET_DB="${2:-igny8_db}"
DB_CONTAINER="postgres"
DB_USER="igny8"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() {
echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
log_success() {
log "${GREEN}$1${NC}"
}
log_warn() {
log "${YELLOW}⚠️ $1${NC}"
}
log_error() {
log "${RED}$1${NC}"
}
# Validate input
if [[ -z "${BACKUP_FILE}" ]]; then
echo "Usage: ./restore-db.sh <backup_file> [target_db]"
echo ""
echo "Arguments:"
echo " backup_file Path to backup file (.sql or .sql.gz)"
echo " target_db Target database (default: igny8_db)"
echo ""
echo "Available backups:"
ls -la /data/backups/latest_db.sql.gz 2>/dev/null || echo " No latest backup found"
echo ""
find /data/backups -name "*.sql.gz" -type f 2>/dev/null | head -10
exit 1
fi
if [[ ! -f "${BACKUP_FILE}" ]]; then
log_error "Backup file not found: ${BACKUP_FILE}"
exit 1
fi
echo "=========================================="
echo "IGNY8 Database Restore"
echo "=========================================="
echo ""
echo "Backup file: ${BACKUP_FILE}"
echo "Target DB: ${TARGET_DB}"
echo ""
# Safety confirmation
log_warn "This will REPLACE the database '${TARGET_DB}' with backup data!"
read -p "Are you sure? Type 'yes' to continue: " confirm
if [[ "${confirm}" != "yes" ]]; then
log "Restore cancelled"
exit 0
fi
# Check if postgres container is running
if ! docker ps --format '{{.Names}}' | grep -q "^${DB_CONTAINER}$"; then
log_error "PostgreSQL container '${DB_CONTAINER}' is not running"
exit 1
fi
# Create pre-restore backup
log "Creating pre-restore backup of current database..."
PRE_RESTORE_BACKUP="/data/backups/pre-restore_${TARGET_DB}_$(date +%Y%m%d_%H%M%S).sql.gz"
if docker exec "${DB_CONTAINER}" pg_dump -U "${DB_USER}" "${TARGET_DB}" 2>/dev/null | gzip > "${PRE_RESTORE_BACKUP}"; then
log_success "Pre-restore backup: ${PRE_RESTORE_BACKUP}"
else
log_warn "Could not create pre-restore backup (database might not exist)"
fi
# Stop application containers to prevent connections
log "Stopping application containers..."
docker compose -f /data/app/igny8/docker-compose.app.yml -p igny8-app stop igny8_backend igny8_celery_worker igny8_celery_beat 2>/dev/null || true
# Drop and recreate database
log "Dropping existing database..."
docker exec "${DB_CONTAINER}" psql -U postgres -c "DROP DATABASE IF EXISTS ${TARGET_DB};" || true
log "Creating fresh database..."
docker exec "${DB_CONTAINER}" psql -U postgres -c "CREATE DATABASE ${TARGET_DB} OWNER ${DB_USER};"
# Restore from backup
log "Restoring database from backup..."
if [[ "${BACKUP_FILE}" == *.gz ]]; then
# Compressed backup
gunzip -c "${BACKUP_FILE}" | docker exec -i "${DB_CONTAINER}" psql -U "${DB_USER}" -d "${TARGET_DB}"
else
# Uncompressed backup
docker exec -i "${DB_CONTAINER}" psql -U "${DB_USER}" -d "${TARGET_DB}" < "${BACKUP_FILE}"
fi
if [[ $? -eq 0 ]]; then
log_success "Database restore complete"
else
log_error "Database restore failed"
exit 1
fi
# Restart application containers
log "Restarting application containers..."
docker compose -f /data/app/igny8/docker-compose.app.yml -p igny8-app start igny8_backend igny8_celery_worker igny8_celery_beat 2>/dev/null || true
# Wait for backend to be healthy
log "Waiting for backend to be healthy..."
sleep 10
# Verify restoration
log "Verifying database..."
TABLE_COUNT=$(docker exec "${DB_CONTAINER}" psql -U "${DB_USER}" -d "${TARGET_DB}" -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';")
log_success "Database restored with ${TABLE_COUNT} tables"
echo ""
echo "=========================================="
echo "RESTORE COMPLETE"
echo "=========================================="
echo "Database '${TARGET_DB}' has been restored"
echo ""
echo "Pre-restore backup saved to:"
echo " ${PRE_RESTORE_BACKUP}"
echo ""
echo "To rollback this restore, run:"
echo " ./restore-db.sh ${PRE_RESTORE_BACKUP} ${TARGET_DB}"
echo "=========================================="

108
scripts/ops/rollback.sh Normal file
View File

@@ -0,0 +1,108 @@
#!/bin/bash
# =============================================================================
# IGNY8 Rollback Script
# =============================================================================
# Rolls back production to previous version using tagged images
# Usage: ./rollback.sh [--auto]
# =============================================================================
set -e
APP_DIR="/data/app/igny8"
COMPOSE_FILE="docker-compose.app.yml"
PROJECT_NAME="igny8-app"
AUTO_MODE="${1}"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log() {
echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1"
}
log_success() {
echo -e "${GREEN}$1${NC}"
}
log_warn() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
log_error() {
echo -e "${RED}$1${NC}"
}
echo "=========================================="
echo "IGNY8 PRODUCTION ROLLBACK"
echo "=========================================="
echo ""
# Check for rollback images
if ! docker image inspect igny8-backend:rollback &>/dev/null; then
log_error "No rollback image found!"
log "Rollback images are created during deployment."
log "To restore from backup, use:"
log " /data/app/igny8/scripts/ops/restore-db.sh /data/backups/latest_db.sql.gz"
exit 1
fi
# Confirmation (unless auto mode)
if [[ "${AUTO_MODE}" != "--auto" ]]; then
log_warn "This will rollback production to the previous version!"
read -p "Type 'rollback' to continue: " confirm
if [[ "${confirm}" != "rollback" ]]; then
log "Rollback cancelled"
exit 0
fi
fi
cd "${APP_DIR}"
# Step 1: Stop current containers
log "Step 1/4: Stopping current containers..."
docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" down
# Step 2: Restore rollback images
log "Step 2/4: Restoring previous images..."
docker tag igny8-backend:rollback igny8-backend:latest
docker tag igny8-frontend-dev:rollback igny8-frontend-dev:latest
docker tag igny8-marketing-dev:rollback igny8-marketing-dev:latest 2>/dev/null || true
log_success "Images restored"
# Step 3: Start containers
log "Step 3/4: Starting containers with previous version..."
docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" up -d
# Step 4: Wait for backend
log "Step 4/4: Waiting for backend to be healthy..."
for i in {1..30}; do
if docker exec igny8_backend python -c "import urllib.request; urllib.request.urlopen('http://localhost:8010/api/v1/system/status/').read()" 2>/dev/null; then
log_success "Backend is healthy"
break
fi
if [[ $i -eq 30 ]]; then
log_error "Backend failed to start after rollback"
log "Manual intervention required!"
exit 1
fi
echo -n "."
sleep 2
done
echo ""
echo "=========================================="
log_success "ROLLBACK COMPLETE"
echo "=========================================="
echo ""
echo "Production is running on previous version."
echo ""
echo "If database rollback is also needed:"
echo " /data/app/igny8/scripts/ops/restore-db.sh /data/backups/pre-deploy/LATEST/db_igny8_*.sql.gz"
echo ""
echo "Container status:"
docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" ps
echo "=========================================="

View File

@@ -0,0 +1,127 @@
#!/bin/bash
# =============================================================================
# IGNY8 Sync Production Data to Staging
# =============================================================================
# Syncs production database to staging with data sanitization
# Usage: ./sync-prod-to-staging.sh
# =============================================================================
set -e
DB_CONTAINER="postgres"
PROD_DB="igny8_db"
STAGING_DB="igny8_staging_db"
DB_USER="igny8"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log() {
echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1"
}
log_success() {
echo -e "${GREEN}$1${NC}"
}
log_warn() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
log_error() {
echo -e "${RED}$1${NC}"
}
echo "=========================================="
echo "SYNC PRODUCTION → STAGING"
echo "=========================================="
echo ""
log_warn "This will REPLACE staging database with production data!"
log_warn "Data will be sanitized (emails, passwords anonymized)"
echo ""
read -p "Type 'sync' to continue: " confirm
if [[ "${confirm}" != "sync" ]]; then
log "Sync cancelled"
exit 0
fi
# Check if staging containers are running
STAGING_RUNNING=$(docker ps --format '{{.Names}}' | grep -c "igny8_staging" || true)
if [[ ${STAGING_RUNNING} -gt 0 ]]; then
log "Stopping staging containers..."
docker compose -f /data/app/igny8/docker-compose.staging.yml -p igny8-staging stop igny8_staging_backend igny8_staging_celery_worker igny8_staging_celery_beat 2>/dev/null || true
fi
# Step 1: Dump production
log "Step 1/5: Dumping production database..."
TEMP_DUMP="/tmp/prod_sync_$(date +%Y%m%d_%H%M%S).sql"
docker exec "${DB_CONTAINER}" pg_dump -U "${DB_USER}" "${PROD_DB}" > "${TEMP_DUMP}"
log_success "Production dump created: ${TEMP_DUMP}"
# Step 2: Drop staging database
log "Step 2/5: Dropping staging database..."
docker exec "${DB_CONTAINER}" psql -U postgres -c "DROP DATABASE IF EXISTS ${STAGING_DB};"
docker exec "${DB_CONTAINER}" psql -U postgres -c "CREATE DATABASE ${STAGING_DB} OWNER ${DB_USER};"
log_success "Staging database recreated"
# Step 3: Restore to staging
log "Step 3/5: Restoring to staging database..."
docker exec -i "${DB_CONTAINER}" psql -U "${DB_USER}" -d "${STAGING_DB}" < "${TEMP_DUMP}"
log_success "Data restored to staging"
# Step 4: Sanitize data
log "Step 4/5: Sanitizing staging data..."
docker exec "${DB_CONTAINER}" psql -U "${DB_USER}" -d "${STAGING_DB}" << 'EOF'
-- Sanitize user emails (keep superusers intact)
UPDATE auth_user
SET email = CONCAT('staging_', id, '@igny8.test')
WHERE is_superuser = FALSE;
-- Invalidate all sessions
DELETE FROM django_session;
-- Update notification endpoints to staging
UPDATE notification_settings
SET webhook_url = REPLACE(webhook_url, 'app.igny8.com', 'staging.igny8.com')
WHERE webhook_url IS NOT NULL;
-- Log sanitization
SELECT 'Sanitized ' || COUNT(*) || ' user emails' FROM auth_user WHERE email LIKE '%@igny8.test';
EOF
log_success "Data sanitized"
# Step 5: Run migrations (in case staging has newer schema)
log "Step 5/5: Checking migrations..."
if docker ps --format '{{.Names}}' | grep -q "igny8_staging_backend"; then
docker exec igny8_staging_backend python manage.py migrate --noinput 2>/dev/null || true
fi
# Cleanup
rm -f "${TEMP_DUMP}"
# Restart staging
if [[ ${STAGING_RUNNING} -gt 0 ]]; then
log "Restarting staging containers..."
docker compose -f /data/app/igny8/docker-compose.staging.yml -p igny8-staging start igny8_staging_backend igny8_staging_celery_worker igny8_staging_celery_beat 2>/dev/null || true
fi
echo ""
echo "=========================================="
log_success "SYNC COMPLETE"
echo "=========================================="
echo ""
echo "Staging database now contains sanitized production data."
echo ""
echo "Changes made:"
echo " - User emails changed to staging_*@igny8.test"
echo " - All sessions invalidated"
echo " - Webhook URLs updated to staging domain"
echo ""
echo "To test with a specific user:"
echo " docker exec -it igny8_staging_backend python manage.py changepassword <username>"
echo "=========================================="