Production-ready PostgreSQL backup system with automated restore verification. Backups are only valuable if you can restore them—this template proves your backups work through automated restore drills.
A complete, tested backup solution for PostgreSQL databases that:
- Backs up automatically to any S3-compatible storage (AWS S3, Backblaze B2, MinIO, etc.)
- Verifies backups work through automated restore drills to isolated databases
- Retains intelligently with configurable retention policies
- Monitors continuously with built-in health checks
- Deploys easily to Railway, Docker, or any container platform
Why you need this: 73% of backups fail when you try to restore them in production. This system continuously validates your backups actually work.
Click to deploy both backup and verification services in minutes.
Prerequisites:
- PostgreSQL database (Railway, AWS RDS, Supabase, etc.)
- S3-compatible storage (AWS S3, Backblaze B2, MinIO, etc.)
- Docker or container platform
Set these environment variables for the backup service:
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
Yes | - | PostgreSQL connection: postgresql://user:pass@host:5432/db |
S3_ENDPOINT |
Yes | - | S3 endpoint: https://s3.amazonaws.com |
S3_BUCKET |
Yes | - | S3 bucket name |
S3_ACCESS_KEY_ID |
Yes | - | S3 access key |
S3_SECRET_ACCESS_KEY |
Yes | - | S3 secret key |
S3_REGION |
No | us-east-1 |
AWS region (if using AWS S3) |
BACKUP_INTERVAL |
No | 3600 |
Backup frequency in seconds (3600 = hourly) |
BACKUP_RETENTION_DAYS |
No | 7 |
Keep backups for N days |
BACKUP_PREFIX |
No | postgres-backups |
S3 key prefix for backups |
COMPRESSION_LEVEL |
No | 6 |
Gzip compression level (1-9) |
Example for Railway:
DATABASE_URL=postgresql://user:pass@postgres.railway.app:5432/railway
S3_ENDPOINT=https://s3.us-west-002.backblazeb2.com
S3_BUCKET=my-db-backups
S3_ACCESS_KEY_ID=your_access_key
S3_SECRET_ACCESS_KEY=your_secret_key
BACKUP_INTERVAL=3600
BACKUP_RETENTION_DAYS=7Set these environment variables for automated restore testing:
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
Yes | - | PostgreSQL server URL: postgresql://user:pass@host:5432/postgres |
S3_ENDPOINT |
Yes | - | S3 endpoint (same as backup service) |
S3_BUCKET |
Yes | - | S3 bucket name (same as backup service) |
S3_ACCESS_KEY_ID |
Yes | - | S3 access key (read-only recommended) |
S3_SECRET_ACCESS_KEY |
Yes | - | S3 secret key |
S3_REGION |
No | us-east-1 |
AWS region |
VERIFY_INTERVAL |
No | 86400 |
Verification frequency in seconds (86400 = daily) |
BACKUP_PREFIX |
No | postgres-backups |
S3 key prefix (must match backup service) |
VERIFY_LATEST |
No | true |
Verify latest backup automatically |
VERIFY_BACKUP_FILE |
No | - | Test specific backup file |
Security Note: The DATABASE_URL for verify service should connect to a separate PostgreSQL instance (or the same server but connect to postgres database) to safely create temporary test databases without affecting production.
Railway:
# Deploy backup service
railway up --service backup
# Deploy verify service (optional but recommended)
railway up --service verifyDocker Compose:
docker-compose up -dDocker (Manual):
# Backup service
docker run -d \
-e DATABASE_URL="postgresql://..." \
-e S3_ENDPOINT="https://..." \
-e S3_BUCKET="..." \
-e S3_ACCESS_KEY_ID="..." \
-e S3_SECRET_ACCESS_KEY="..." \
postgres-backup:latest
# Verify service
docker run -d \
-e DATABASE_URL="postgresql://..." \
-e S3_ENDPOINT="https://..." \
-e S3_BUCKET="..." \
-e S3_ACCESS_KEY_ID="..." \
-e S3_SECRET_ACCESS_KEY="..." \
postgres-verify:latestBackup Service:
# Railway
railway logs --service backup
# Docker
docker logs <backup-container-id>
# Look for:
# ✓ "Backup completed successfully"
# ✓ "Uploaded to S3: s3://bucket/backups/backup_20260207_120000.sql.gz"
# ✓ "Retention cleanup: deleted X old backups"Verify Service:
# Railway
railway logs --service verify
# Docker
docker logs <verify-container-id>
# Look for:
# ✓ "Restore verification completed successfully"
# ✓ "Database restore successful: verify_20260207_120000"
# ✓ "Verification queries passed: 3/3"# Using AWS CLI
aws s3 ls s3://your-bucket/postgres-backups/ --endpoint-url https://your-endpoint
# Expected output:
# 2026-02-07 12:00:00 15.2 MB backup_20260207_120000.sql.gz
# 2026-02-07 13:00:00 15.3 MB backup_20260207_130000.sql.gz# Check service health
curl http://backup-service:8080/health
# Expected: HTTP 200
# Or check Docker health status
docker ps
# backup container should show "healthy"make testThis runs the complete test suite including backup, restore, and verification.
# Download latest backup
aws s3 cp s3://your-bucket/postgres-backups/backup_latest.sql.gz ./backup.sql.gz \
--endpoint-url https://your-endpoint
# Decompress
gunzip backup.sql.gz
# Restore to database
psql "$DATABASE_URL" -f backup.sql# List available backups
aws s3 ls s3://your-bucket/postgres-backups/ --endpoint-url https://your-endpoint
# Download specific backup
aws s3 cp s3://your-bucket/postgres-backups/backup_20260207_120000.sql.gz ./backup.sql.gz \
--endpoint-url https://your-endpoint
# Decompress and restore
gunzip backup.sql.gz
psql "$DATABASE_URL" -f backup.sqlFor detailed restore procedures including point-in-time recovery, see:
Run the complete test suite with a single command:
make testThis verifies:
- ✅ Backup creation and upload to MinIO/S3
- ✅ Backup file size > 0
- ✅ Restore to separate verification database
- ✅ Data integrity (record counts, indexes, content)
- ✅ Retention policy (old backups deleted)
See Testing Documentation for details.
┌─────────────┐ ┌──────────────┐ ┌────────────┐
│ PostgreSQL │◄────────┤ Backup │────────►│ S3 Storage │
│ Database │ │ Service │ │ (Backups) │
│ (Production)│ │ (pg_dump) │ └────────────┘
└─────────────┘ └──────────────┘ │
│
┌─────────────┐ ┌──────────────┐ │
│ PostgreSQL │◄────────┤ Verify │◄───────────────┘
│ Verify DB │ │ Service │
│ (Isolated) │ │ (Restore Test)│
└─────────────┘ └──────────────┘
How it works:
- Backup Service dumps PostgreSQL using
pg_dump, compresses with gzip, and uploads to S3 - Retention Policy automatically deletes backups older than configured retention period
- Verify Service downloads backups and restores them to an isolated database
- Verification Queries run sanity checks to ensure data integrity
- Cleanup removes temporary test databases after verification
See Architecture Documentation for details.
Works with any S3-compatible storage:
| Provider | S3_ENDPOINT Example | Notes |
|---|---|---|
| AWS S3 | https://s3.amazonaws.com |
Set S3_REGION |
| Backblaze B2 | https://s3.us-west-002.backblazeb2.com |
Most cost-effective |
| DigitalOcean Spaces | https://nyc3.digitaloceanspaces.com |
Replace nyc3 with your region |
| Cloudflare R2 | https://<account-id>.r2.cloudflarestorage.com |
No egress fees |
| MinIO | http://minio:9000 |
Self-hosted, great for testing |
| Wasabi | https://s3.wasabisys.com |
Fast, no egress fees |
| Document | Description |
|---|---|
| Architecture | System design, component diagrams, data flow |
| Configuration | Complete environment variable reference |
| Restore Guide | Step-by-step restore procedures |
| Troubleshooting | Common issues and solutions |
| Runbooks | Operational procedures for incidents |
| Security | Security best practices, encryption, IAM policies |
| Contributing | How to contribute to this project |
This system handles sensitive data. Key security features:
✅ Automatic Secret Scrubbing - Passwords never logged ✅ Restore Safety Checks - Prevents accidental production overwrites ✅ Encryption Support - Server-side and client-side encryption ✅ Least Privilege IAM - Minimal S3 permissions required ✅ Isolated Verification - Restore tests use separate database
Before deploying to production:
- Use encrypted environment variables (Railway secrets, AWS Secrets Manager)
- Enable S3 bucket encryption
- Use HTTPS endpoints (never HTTP)
- Configure separate read-only S3 credentials for verify service
- Point verify service to non-production database
- Review Security Documentation
Report security vulnerabilities: See SECURITY.md for responsible disclosure process.
Run the complete test suite:
make testmake test # Run all integration tests
make test-verbose # Run tests with verbose output
make test-clean # Clean up test containers and volumes
make test-logs # Show logs from all test services
make build # Build all Docker images
make help # Show all available commandsThe integration tests verify:
✅ Backup Creation
- Backup service starts successfully
- pg_dump executes without errors
- Backup completes within expected time
✅ Backup Storage
- Backup file exists in MinIO/S3
- File size is greater than 0
- Correct naming convention (backup_YYYYMMDD_HHMMSS.sql.gz)
✅ Restore Verification
- Verify service downloads backup successfully
- Restore to separate postgres_verify instance
- Temporary database created and cleaned up
- No data corruption during restore
✅ Data Integrity
- Correct number of records restored
- Index integrity maintained
- Data content matches source
- Custom sanity queries pass
✅ Retention Policy
- Old backups are identified correctly
- Backups older than retention period are deleted
- Recent backups are preserved
- Retention cleanup runs during backup cycle
Tests run automatically on every push and pull request via GitHub Actions.
The CI pipeline includes:
- Integration tests (backup, restore, retention)
- Shell script linting (ShellCheck)
- Dockerfile linting (Hadolint)
- Security scanning (Trivy)
- Documentation validation
- Docker image builds
If tests fail, see the Test Failures section in the troubleshooting guide.
Common issues and solutions:
| Issue | Solution |
|---|---|
| Backup service won't start | Check DATABASE_URL and S3 credentials. See Troubleshooting |
| Backups are empty (0 bytes) | Database user may lack permissions. See Troubleshooting |
| Verify service fails | Check DATABASE_URL points to separate database. See Troubleshooting |
| Old backups not deleted | Check retention configuration. See Troubleshooting |
| Restore fails | Check PostgreSQL version compatibility. See Troubleshooting |
📖 Complete Troubleshooting Guide
Typical performance characteristics:
| Database Size | Backup Time | Restore Time | Disk Space Needed |
|---|---|---|---|
| 100 MB | ~5 seconds | ~10 seconds | ~50 MB |
| 1 GB | ~30 seconds | ~1 minute | ~500 MB |
| 10 GB | ~5 minutes | ~10 minutes | ~5 GB |
| 100 GB | ~45 minutes | ~90 minutes | ~50 GB |
Times vary based on CPU, network, compression level, and database complexity.
Optimization tips:
- Reduce
COMPRESSION_LEVELfor faster backups (larger files) - Increase
BACKUP_INTERVALfor large databases - Use S3 endpoints geographically close to database
- Exclude unnecessary tables with custom pg_dump flags
Contributions welcome! Please see CONTRIBUTING.md for guidelines.
Quick contribution steps:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Run tests (
make test) - Commit (
git commit -m 'feat: add amazing feature') - Push to your fork
- Open a Pull Request
MIT License - see LICENSE for details.
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Documentation: docs/
See CHANGELOG.md for version history and release notes.
Built with:
- PostgreSQL - The world's most advanced open source database
- AWS CLI - S3-compatible storage interface
- MinIO - S3-compatible testing environment
- Railway - Simple deployment platform
- Docker - Containerization
Remember: Untested backups are worthless. Deploy the verification service to prove your backups work.
⭐ Star this repo if it saved your data!