{"id":39810774,"url":"https://github.com/jackkweyunga/pg-backups","last_synced_at":"2026-01-18T12:43:57.047Z","repository":{"id":322899989,"uuid":"1070411376","full_name":"jackkweyunga/pg-backups","owner":"jackkweyunga","description":"Ready to use docker images for Backups for PostgreSQL to local folder, s3 bucket or Remote servers","archived":false,"fork":false,"pushed_at":"2025-11-06T23:03:23.000Z","size":56,"stargazers_count":6,"open_issues_count":0,"forks_count":3,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-11-07T01:06:30.262Z","etag":null,"topics":["backups","disaster-recovery","postgresql"],"latest_commit_sha":null,"homepage":"","language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jackkweyunga.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-05T21:34:36.000Z","updated_at":"2025-11-06T23:03:27.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/jackkweyunga/pg-backups","commit_stats":null,"previous_names":["jackkweyunga/pg-backups"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/jackkweyunga/pg-backups","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jackkweyunga%2Fpg-backups","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jackkweyunga%2Fpg-backups/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jackkweyunga%2Fpg-backups/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jackkweyunga%2Fpg-backups/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jackkweyunga","download_url":"https://codeload.github.com/jackkweyunga/pg-backups/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jackkweyunga%2Fpg-backups/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28536008,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-18T10:13:46.436Z","status":"ssl_error","status_checked_at":"2026-01-18T10:13:11.045Z","response_time":98,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["backups","disaster-recovery","postgresql"],"created_at":"2026-01-18T12:43:56.905Z","updated_at":"2026-01-18T12:43:57.028Z","avatar_url":"https://github.com/jackkweyunga.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PostgreSQL Disaster Recovery System\n\nComplete PostgreSQL backup and restore solution with automated backups to multiple destinations and an interactive restore tool.\n\n## Table of Contents\n- [Quick Start](#quick-start)\n- [Backup System](#backup-system)\n- [Restore Tool](#restore-tool)\n- [Manual Restore](#manual-restore-procedures)\n- [Configuration](#configuration)\n- [Monitoring](#monitoring)\n- [Testing](#testing)\n- [Troubleshooting](#troubleshooting)\n\n---\n\n## Quick Start\n\n### Deploy Backup Service\n```bash\n# Copy .env.example to .env and configure\ncp .env.example .env\nnano .env\n\n# Deploy with docker compose\ndocker compose up -d\n```\n\n### Run Interactive Restore Tool\n```bash\ndocker run --rm -it \\\n  -v $(pwd)/backups:/backups \\\n  --network your_postgres_network \\\n  -e PGHOST=postgres-primary \\\n  -e PGUSER=postgres \\\n  -e PGPASSWORD=yourpassword \\\n  ghcr.io/jackkweyunga/pg-backups-restore:17-alpine\n```\n\n---\n\n## Backup System\n\n### Understanding Backups\n\nEach backup run creates a timestamped folder with three backup types:\n\n```\nbackups/\n└── 20251005_120000/              # Timestamp folder (YYYYMMDD_HHMMSS)\n    ├── postgres_cluster.sql.gz   # Full cluster (all DBs + roles)\n    ├── postgres_globals.sql.gz   # Only users/roles/permissions\n    └── postgres_db_mydb.sql.gz   # Individual databases\n```\n\n**Why three types?**\n- **Full cluster** - Complete disaster recovery (everything)\n- **Globals only** - Restore just users/roles without touching data\n- **Individual DBs** - Selective restore of specific databases\n\n### Backup Destinations\n\nBackups are stored in up to 3 locations:\n\n1. **Local** (`/backups` volume) - Fast access, short retention\n2. **S3/Spaces** - Cloud storage, medium retention\n3. **Remote Server** (rsync) - Off-site backup, long retention\n\n### Daily Operations\n\n**Check backup status:**\n```bash\n# View logs\ndocker compose logs -f pg-backup\n\n# List backup folders\nls -lh backups/\n\n# Check last backup\nls -lt backups/ | head -5\n```\n\n**Run manual backup:**\n```bash\ndocker compose exec pg-backup /tmp/backup-script.sh\n```\n\n**Verify backup integrity:**\n```bash\nBACKUP_FOLDER=\"20251005_120000\"\n\nfor file in backups/${BACKUP_FOLDER}/*.sql.gz; do\n    if gunzip -t \"$file\" 2\u003e/dev/null; then\n        echo \"✓ $(basename $file)\"\n    else\n        echo \"✗ $(basename $file) - CORRUPTED\"\n    fi\ndone\n```\n\n---\n\n## Restore Tool\n\n### Interactive Mode (Recommended)\n\nThe restore tool provides a menu-driven interface for easy recovery:\n\n```bash\ndocker run --rm -it \\\n  -v $(pwd)/backups:/backups \\\n  --network postgres_network \\\n  -e PGHOST=postgres-primary \\\n  -e PGUSER=postgres \\\n  -e PGPASSWORD=secret \\\n  ghcr.io/jackkweyunga/pg-backups-restore:17-alpine\n```\n\n**Features:**\n- ✅ Automatic backup discovery (local/S3/remote)\n- ✅ Integrity verification before restore\n- ✅ Interactive menus with backup age and size\n- ✅ Multiple restore types (cluster/database/globals)\n- ✅ Dry-run mode\n- ✅ Post-restore verification\n\n**Menu Flow:**\n```\nMain Menu → Select Source → Choose Backup → Verify Integrity\n                                                    ↓\n            Verify Results ← Execute Restore ← Select Type\n```\n\n### With S3 Access\n\n```bash\ndocker run --rm -it \\\n  -v $(pwd)/backups:/backups \\\n  -v $(pwd)/restore:/restore \\\n  --network postgres_network \\\n  -e PGHOST=postgres-primary \\\n  -e PGUSER=postgres \\\n  -e PGPASSWORD=secret \\\n  -e S3_ENABLED=true \\\n  -e S3_BUCKET=my-backups \\\n  -e S3_REGION=fra1 \\\n  -e S3_ACCESS_KEY=DO801... \\\n  -e S3_SECRET_KEY=NffZb... \\\n  -e S3_ENDPOINT=https://my-backups.fra1.digitaloceanspaces.com \\\n  ghcr.io/jackkweyunga/pg-backups-restore:17-alpine\n```\n\n### With Remote Server Access\n\n```bash\ndocker run --rm -it \\\n  -v $(pwd)/backups:/backups \\\n  -v ~/.ssh/id_rsa:/root/.ssh/id_rsa:ro \\\n  --network postgres_network \\\n  -e PGHOST=postgres-primary \\\n  -e PGUSER=postgres \\\n  -e PGPASSWORD=secret \\\n  -e RSYNC_ENABLED=true \\\n  -e RSYNC_HOST=196.200... \\\n  -e RSYNC_USER=dani \\\n  -e RSYNC_PATH=/home/user/test-pg-backups \\\n  ghcr.io/jackkweyunga/pg-backups-restore:17-alpine\n```\n\n### Non-Interactive Mode (Automation)\n\nFor scripts or automated recovery:\n\n```bash\ndocker run --rm \\\n  -v $(pwd)/backups:/backups \\\n  --network postgres_network \\\n  -e PGHOST=postgres-primary \\\n  -e PGUSER=postgres \\\n  -e PGPASSWORD=secret \\\n  -e RESTORE_SOURCE=local \\\n  -e RESTORE_FOLDER=20251005_120000 \\\n  -e RESTORE_TYPE=cluster \\\n  ghcr.io/jackkweyunga/pg-backups-restore:17-alpine\n```\n\n**Environment Variables:**\n- `RESTORE_SOURCE` - `local`, `s3`, or `remote`\n- `RESTORE_FOLDER` - Timestamp folder (e.g., `20251005_120000`)\n- `RESTORE_TYPE` - `cluster`, `globals`, or `database`\n- `RESTORE_DATABASE` - Database name (if `RESTORE_TYPE=database`)\n\n---\n\n## Manual Restore Procedures\n\nIf you prefer manual control over the restore tool:\n\n### Quick Restore Commands\n\n**Restore full cluster:**\n```bash\nBACKUP_FOLDER=\"20251005_120000\"\ngunzip -c backups/${BACKUP_FOLDER}/postgres_cluster.sql.gz | \\\n    docker exec -i postgres-primary psql -U postgres\n```\n\n**Restore single database:**\n```bash\nBACKUP_FOLDER=\"20251005_120000\"\nDB_NAME=\"mydb\"\ngunzip -c backups/${BACKUP_FOLDER}/postgres_db_${DB_NAME}.sql.gz | \\\n    docker exec -i postgres-primary psql -U postgres\n```\n\n**Restore only users/roles:**\n```bash\nBACKUP_FOLDER=\"20251005_120000\"\ngunzip -c backups/${BACKUP_FOLDER}/postgres_globals.sql.gz | \\\n    docker exec -i postgres-primary psql -U postgres\n```\n\n### Complete Disaster Recovery\n\n1. **Find latest backup:**\n   ```bash\n   ls -lht backups/ | head -5\n   ```\n\n2. **Stop applications:**\n   ```bash\n   docker compose stop your-app\n   ```\n\n3. **Restore cluster:**\n   ```bash\n   gunzip -c backups/20251005_120000/postgres_cluster.sql.gz | \\\n       docker exec -i postgres-primary psql -U postgres\n   ```\n\n4. **Verify:**\n   ```bash\n   docker exec postgres-primary psql -U postgres -c \"\\l\"\n   docker exec postgres-primary psql -U postgres -c \"\\du\"\n   ```\n\n5. **Restart applications:**\n   ```bash\n   docker compose start your-app\n   ```\n\n### Restore from S3\n\n```bash\nBACKUP_FOLDER=\"20251005_120000\"\n\n# Download backup folder\naws s3 sync s3://bucket/postgres-backups/${BACKUP_FOLDER}/ ./restore/ \\\n    --endpoint-url=https://fra1.digitaloceanspaces.com\n\n# Restore\ngunzip -c restore/postgres_cluster.sql.gz | \\\n    docker exec -i postgres-primary psql -U postgres\n```\n\n### Restore from Remote Server\n\n```bash\nBACKUP_FOLDER=\"20251005_120000\"\n\n# Download via rsync\nrsync -avz user@server:/backups/postgres/${BACKUP_FOLDER}/ ./restore/\n\n# Restore\ngunzip -c restore/postgres_cluster.sql.gz | \\\n    docker exec -i postgres-primary psql -U postgres\n```\n\n---\n\n## Configuration\n\nAll configuration is via environment variables in `.env` file.\n\n### PostgreSQL Connection\n```bash\nPGHOST=postgres-primary\nPGPORT=5432\nPGUSER=postgres\nPGPASSWORD=devpassword\n```\n\n### Backup Schedule\n```bash\n# Cron format: minute hour day month weekday\nBACKUP_SCHEDULE=0 */6 * * *    # Every 6 hours\nLOCAL_RETENTION_DAYS=1         # Keep backups for 1 day\n```\n\n### S3/Spaces Configuration\n```bash\nS3_ENABLED=true\nS3_BUCKET=your-bucket-name\nS3_REGION=fra1\nS3_ACCESS_KEY=DO801...\nS3_SECRET_KEY=NffZb...\nS3_ENDPOINT=https://konekti-backups.fra1.digitaloceanspaces.com\nS3_RETENTION_DAYS=7\n```\n\n### Remote Server (Rsync)\n```bash\nRSYNC_ENABLED=true\nRSYNC_HOST=196.200.229.166\nRSYNC_USER=dani\nRSYNC_PORT=22\nRSYNC_PATH=/home/dani/test-pg-backups\nRSYNC_RETENTION_DAYS=30\n```\n\n### Notifications (Rocket.Chat)\n```bash\nROCKET_CHAT_ENABLED=true\nROCKET_CHAT_BASE_URL=https://your-rocketchat-server.com\nROCKET_CHAT_USER_ID=your_user_id\nROCKET_CHAT_AUTH_TOKEN=your_auth_token\nROCKET_CHAT_ROOM_ID=your_room_id\n```\n\n### Recommended Settings\n\n**Development:**\n```bash\nBACKUP_SCHEDULE=0 */6 * * *\nLOCAL_RETENTION_DAYS=1\nS3_RETENTION_DAYS=7\nRSYNC_ENABLED=false\n```\n\n**Production:**\n```bash\nBACKUP_SCHEDULE=0 */2 * * *\nLOCAL_RETENTION_DAYS=2\nS3_RETENTION_DAYS=30\nRSYNC_ENABLED=true\nRSYNC_RETENTION_DAYS=90\n```\n\n---\n\n## Monitoring\n\n### Backup Health Check\n\n```bash\n#!/bin/bash\n# check-backups.sh\n\nLATEST=$(ls -t backups/20* 2\u003e/dev/null | head -1)\n\nif [ -z \"$LATEST\" ]; then\n    echo \"❌ No backups found!\"\n    exit 1\nfi\n\n# Get folder modification time\nBACKUP_TIME=$(stat -c %Y \"backups/$LATEST\" 2\u003e/dev/null || stat -f %m \"backups/$LATEST\")\nCURRENT_TIME=$(date +%s)\nAGE_HOURS=$(( ($CURRENT_TIME - $BACKUP_TIME) / 3600 ))\n\nif [ $AGE_HOURS -gt 12 ]; then\n    echo \"⚠️  Last backup is $AGE_HOURS hours old\"\n    exit 1\nelse\n    echo \"✅ Backup healthy (last: $AGE_HOURS hours ago)\"\nfi\n```\n\n### Storage Usage\n\n```bash\n# Local storage\ndu -sh backups/\nls -lh backups/ | wc -l\n\n# S3 usage\naws s3 ls s3://bucket/postgres-backups/ --endpoint-url=https://endpoint.com --recursive --summarize\n\n# Remote storage\nssh user@server \"du -sh /backups/postgres/\"\n```\n\n### View Logs\n\n```bash\n# Real-time logs\ndocker compose logs -f pg-backup\n\n# Last 100 lines\ndocker compose logs --tail=100 pg-backup\n\n# Check for errors\ndocker compose logs pg-backup | grep -i error\n```\n\n---\n\n## Testing\n\n### Automated Restore Testing\n\nThe system includes an automated restore test tool that validates backups by performing actual restores in isolated test containers.\n\n**Run manually:**\n```bash\n./restore-test.sh\n```\n\n**Run with Docker (scheduled):**\n```yaml\n# docker-compose.yml\npg-restore-test:\n  image: ghcr.io/jackkweyunga/pg-backups-restore-test:17-alpine\n  volumes:\n    - /var/run/docker.sock:/var/run/docker.sock\n    - ./backups:/backups:ro\n    - ./restore-test-logs:/restore-test-logs\n  environment:\n    - TEST_SCHEDULE=0 0 1 * *  # Monthly on 1st at midnight\n```\n\n**What it does:**\n1. Finds latest backup folder\n2. Creates isolated test container (PostgreSQL + PostGIS)\n3. Restores full cluster backup\n4. Verifies databases and roles\n5. Cleans up test container\n6. Logs results to timestamped file\n\n**Check test results:**\n```bash\n# View latest test log\nls -lt restore-test-logs/ | head -1\n\n# Check for errors\ngrep -i error restore-test-logs/restore-test_*.log | grep -v \"already exists\" | grep -v \"cannot be dropped\"\n\n# View full log\ncat restore-test-logs/restore-test_20251006_120000.log\n```\n\n**Log files:**\n```\nrestore-test-logs/\n├── restore-test_20251006_120000.log\n├── restore-test_20251006_150000.log\n└── restore-test_20251007_120000.log\n```\n\n### Monthly Restore Test (Manual Script)\n\nFor reference, the restore test script:\n\n```bash\n#!/bin/bash\n# restore-test.sh\n\nTIMESTAMP=$(date +\"%Y%m%d_%H%M%S\")\nLOG_DIR=\"restore-test-logs\"\nmkdir -p \"$LOG_DIR\"\nLOG_FILE=\"${LOG_DIR}/restore-test_${TIMESTAMP}.log\"\n\nBACKUP_FOLDER=$(ls -t backups/ | head -1)\n\necho \"Testing restore of backup: $BACKUP_FOLDER\" | tee -a \"$LOG_FILE\"\n\n# Create test container (with PostGIS if needed)\ndocker run -d --name postgres-test \\\n    -e POSTGRES_PASSWORD=test \\\n    postgis/postgis:17-3.5\n\nsleep 5\n\n# Restore\ngunzip -c backups/${BACKUP_FOLDER}/postgres_cluster.sql.gz | \\\n    docker exec -i postgres-test psql -U postgres 2\u003e\u00261 | tee -a \"$LOG_FILE\"\n\n# Verify\necho \"Databases:\" | tee -a \"$LOG_FILE\"\ndocker exec postgres-test psql -U postgres -c \"\\l\" | tee -a \"$LOG_FILE\"\n\necho -e \"\\nRoles:\" | tee -a \"$LOG_FILE\"\ndocker exec postgres-test psql -U postgres -c \"\\du\" | tee -a \"$LOG_FILE\"\n\n# Cleanup\ndocker rm -f postgres-test\n\necho \"✓ Restore test completed successfully\" | tee -a \"$LOG_FILE\"\n```\n\n### Backup Integrity Check\n\n```bash\n# Test all backups in latest folder\nBACKUP_FOLDER=$(ls -t backups/ | head -1)\n\nfor file in backups/${BACKUP_FOLDER}/*.sql.gz; do\n    if gunzip -t \"$file\" 2\u003e/dev/null; then\n        echo \"✓ $(basename $file)\"\n    else\n        echo \"✗ $(basename $file) - CORRUPTED\"\n    fi\ndone\n```\n\n---\n\n## Troubleshooting\n\n### Backup Issues\n\n| Problem | Solution |\n|---------|----------|\n| No backups created | Check logs: `docker compose logs pg-backup` |\n| S3 upload fails | Verify credentials and bucket access |\n| Rsync fails | Test SSH: `ssh user@server` |\n| Container restarts | Check restart policy in docker-compose.yml |\n| Disk full | Reduce retention days or increase disk space |\n\n### Restore Issues\n\n| Problem | Solution |\n|---------|----------|\n| \"role does not exist\" | Restore globals first, then database |\n| \"database already exists\" | Drop database first or restore to different name |\n| Backup corrupted | Download from S3/remote, test with `gunzip -t` |\n| Restore too slow | Disable fsync temporarily (test only) |\n| Connection refused | Check PostgreSQL is running, verify network |\n\n### Common Restore Errors\n\n**Error: \"role does not exist\"**\n```bash\n# Restore globals first\ngunzip -c backups/${BACKUP_FOLDER}/postgres_globals.sql.gz | \\\n    docker exec -i postgres-primary psql -U postgres\n\n# Then restore database\ngunzip -c backups/${BACKUP_FOLDER}/postgres_db_mydb.sql.gz | \\\n    docker exec -i postgres-primary psql -U postgres\n```\n\n**Error: \"database already exists\"**\n```bash\n# Drop database first\ndocker exec postgres-primary psql -U postgres -c \"DROP DATABASE IF EXISTS mydb;\"\n```\n\n**Backup file corrupted**\n```bash\n# Test integrity\ngunzip -t backups/${BACKUP_FOLDER}/postgres_cluster.sql.gz\n\n# If corrupted, try S3\naws s3 sync s3://bucket/postgres-backups/${BACKUP_FOLDER}/ ./restore/ \\\n    --endpoint-url=https://endpoint.com\n```\n\n---\n\n## Recovery Time Estimates\n\nBased on 1GB database:\n\n| Scenario | Time | Notes |\n|----------|------|-------|\n| Local restore (manual) | 2-3 min | Direct decompress + restore |\n| Local restore (tool) | 3-4 min | Includes verification |\n| S3 restore | 5-10 min | Download + decompress + restore |\n| Remote restore | 10-15 min | Download + decompress + restore |\n| Fresh server setup | 20-30 min | Install Docker + setup + restore |\n\n---\n\n## Security\n\n### Rotate S3 Credentials\n\n```bash\n# 1. Generate new keys in DigitalOcean\n# 2. Update .env\nnano .env\n\n# 3. Restart service\ndocker compose restart pg-backup\n\n# 4. Revoke old keys\n```\n\n### Rotate SSH Keys\n\n```bash\n# Generate new key\nssh-keygen -t rsa -b 4096 -f ~/.ssh/pg_backup_new -N \"\"\n\n# Copy to remote\nssh-copy-id -i ~/.ssh/pg_backup_new.pub user@server\n\n# Test\nssh -i ~/.ssh/pg_backup_new user@server\n\n# Update volume mount in docker-compose.yml\n# Restart service\ndocker compose restart pg-backup\n```\n\n---\n\n## Best Practices\n\n### Backups\n\n✅ **DO:**\n- Test backups monthly\n- Keep 3 copies (local, S3, remote) - 3-2-1 rule\n- Verify integrity after each backup\n- Monitor backup age and size\n- Document your backup schedule\n- Use appropriate retention periods\n\n❌ **DON'T:**\n- Rely on single backup location\n- Delete old backups without verifying new ones\n- Ignore backup failures\n- Skip regular testing\n- Store backups on same server as database\n\n### Restores\n\n✅ **DO:**\n- Test in non-production first\n- Stop applications before major restores\n- Verify backup integrity before restore\n- Document restore procedures\n- Time your restores (know your RTO)\n- Keep restore tool image updated\n\n❌ **DON'T:**\n- Restore to production without testing\n- Skip verification after restore\n- Panic during recovery\n- Forget to restart applications\n- Skip documentation\n\n---\n\n## Recovery Checklist\n\nUse during actual disaster recovery:\n\n- [ ] Stay calm, don't rush\n- [ ] Identify what needs restoring\n- [ ] Find appropriate backup folder\n- [ ] Test backup integrity (`gunzip -t`)\n- [ ] Stop affected applications\n- [ ] Choose restore method (tool vs manual)\n- [ ] Perform restore\n- [ ] Verify databases (`\\l`)\n- [ ] Verify table data (`SELECT COUNT(*)`)\n- [ ] Test application connectivity\n- [ ] Restart applications\n- [ ] Monitor for issues (30+ minutes)\n- [ ] Document incident and timeline\n\n---\n\n## Architecture\n\n### File Structure\n\n```\npg-backups/\n├── backup-script.sh                    # Main backup logic\n├── backup-cron.sh                      # Cron wrapper\n├── restore-tool.sh                     # Interactive restore CLI\n├── restore-test.sh                     # Automated restore test\n├── lib/                                # Helper libraries\n│   ├── backup-discovery.sh             # Find backups\n│   ├── backup-download.sh              # Download from S3/remote\n│   └── restore-executor.sh             # Execute restores\n├── Dockerfile.backup-17-alpine         # Backup service image\n├── Dockerfile.restore-17-alpine        # Restore tool image\n├── Dockerfile.restore-test-17-alpine   # Restore test image\n├── docker-compose.yml                  # Service definition\n├── .env.example                        # Configuration template\n├── backups/                            # Local backup storage\n│   └── YYYYMMDD_HHMMSS/                # Timestamped folders\n└── restore-test-logs/                  # Test results\n    └── restore-test_YYYYMMDD_HHMMSS.log\n```\n\n### Docker Images\n\nThree Docker images built via GitHub Actions matrix for multiple PostgreSQL versions:\n\n1. **Backup Image** - `ghcr.io/jackkweyunga/pg-backups:17-alpine`\n   - Automated scheduled backups with cron\n   - Supports S3, remote sync, notifications\n\n2. **Restore Image** - `ghcr.io/jackkweyunga/pg-backups-restore:17-alpine`\n   - Interactive CLI for backup restoration\n   - Supports local, S3, remote sources\n\n3. **Restore Test Image** - `ghcr.io/jackkweyunga/pg-backups-restore-test:17-alpine`\n   - Monthly automated restore validation\n   - Creates test containers and verifies backups\n\n**GitHub Actions Matrix:**\nWorkflow builds for PostgreSQL versions defined in matrix:\n- `17-alpine`\n- `18-alpine`\n\nTo add more versions, edit `.github/workflows/docker-publish.yml` matrix.\n\n**Manual Build:**\n```bash\n# Build with custom PostgreSQL version\ndocker build --build-arg POSTGRES_TAG=18.0-latest -f Dockerfile.backup -t pg-backups:18 .\n```\n\n---\n\n## Monthly Checklist\n\n- [ ] Run restore test (automated via restore-test image)\n- [ ] Review restore test logs in `restore-test-logs/`\n- [ ] Verify all backup destinations (local/S3/remote)\n- [ ] Check backup logs for errors\n- [ ] Verify backup file integrity\n- [ ] Review storage usage and costs\n- [ ] Test restore speed (time it)\n- [ ] Update documentation if needed\n- [ ] Rotate credentials (quarterly)\n- [ ] Review and adjust retention periods\n- [ ] Test restore tool with latest backup\n\n---\n\n## Support\n\n**Need help during recovery?**\n- Don't panic\n- Follow the recovery checklist\n- Test in separate container first\n- Check troubleshooting section\n- Review logs for specific errors\n\n**Backup Locations:**\n- Local: `./backups/`\n- S3: `s3://your-bucket/postgres-backups/`\n- Remote: `user@server:/backups/postgres/`\n\n**Key Containers:**\n- PostgreSQL: `postgres-primary`\n- Backup: `pg-backup`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjackkweyunga%2Fpg-backups","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjackkweyunga%2Fpg-backups","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjackkweyunga%2Fpg-backups/lists"}