Document Owner: Platform Engineering Last Updated: 2026-03-12 Review Cadence: Quarterly Parent Epic: REN-4 (Disaster Recovery)
| Metric | Target | Notes |
|---|---|---|
| RPO (Recovery Point Objective) | 24 hours (database) | Daily automated backups via scripts/backup-cron.sh |
| RPO (Job Artifacts) | 1 hour | S3-compatible storage with cross-region replication |
| RTO (Recovery Time Objective) | 4 hours | Full environment rebuild from scratch |
| Component | Backup Method | Frequency | Retention | Storage Location |
|---|---|---|---|---|
| PostgreSQL 16 | pg_dump custom format + WAL archiving |
Daily (02:00 UTC) | 30 days | S3 backup bucket + 7-day local |
| Redis 7 | AOF + RDB snapshot | Continuous (AOF), hourly (RDB) | 7 days | S3 backup bucket |
| S3/R2 Objects | Cross-region replication or rclone sync |
Continuous | Same as source | Secondary region |
| Application Config | Manual export + version control | On change | Indefinite | Git repo + encrypted S3 |
| TLS Certificates | Automated via Coolify/Let's Encrypt | On renewal | N/A | Coolify config backup |
| Cloudflare Tunnel Credentials | Manual export | On change | Indefinite | Encrypted S3 |
The primary backup mechanism uses scripts/backup-db.sh which runs nightly via cron.
# Manual invocation
./scripts/backup-db.sh --upload --retention-days 7
# Verify backup exists
aws s3 ls s3://${BACKUP_S3_BUCKET}/db/ --recursive | tail -5Backup format: PostgreSQL custom format (compressed), filename pattern: rendertrust_db_YYYYMMDD_HHMMSS.dump
For point-in-time recovery (PITR), configure WAL archiving in postgresql.conf:
# WAL archiving for PITR
wal_level = replica
archive_mode = on
archive_command = 'aws s3 cp %p s3://${BACKUP_S3_BUCKET}/wal/%f'
archive_timeout = 300Ensure the PostgreSQL container or host has AWS CLI configured with appropriate credentials.
After each backup, verify integrity:
# Verify backup is valid and list contents
pg_restore --list <dump_file> > /dev/null 2>&1 && echo "VALID" || echo "CORRUPT"
# Check file size is reasonable (should be > 1KB for non-empty DB)
ls -lh <dump_file>
# Optional: restore to a scratch database to verify
createdb rendertrust_verify
pg_restore --dbname=rendertrust_verify --clean --if-exists <dump_file>
psql -d rendertrust_verify -c "SELECT count(*) FROM alembic_version;"
dropdb rendertrust_verifyRedis is configured with AOF persistence for durability:
# redis.conf
appendonly yes
appendfsync everysec
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mbRDB snapshots provide point-in-time backups:
# redis.conf
save 3600 1
save 300 100
save 60 10000
dbfilename dump.rdb# Trigger a manual RDB save
redis-cli BGSAVE
# Wait for save to complete
redis-cli LASTSAVE
# Copy AOF and RDB files to backup location
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
cp /var/lib/redis/appendonly.aof /backup/redis/appendonly_${TIMESTAMP}.aof
cp /var/lib/redis/dump.rdb /backup/redis/dump_${TIMESTAMP}.rdb
# Upload to S3
aws s3 cp /backup/redis/appendonly_${TIMESTAMP}.aof s3://${BACKUP_S3_BUCKET}/redis/
aws s3 cp /backup/redis/dump_${TIMESTAMP}.rdb s3://${BACKUP_S3_BUCKET}/redis/# Stop Redis
systemctl stop redis
# Replace AOF file
cp /backup/redis/appendonly_<TIMESTAMP>.aof /var/lib/redis/appendonly.aof
chown redis:redis /var/lib/redis/appendonly.aof
# Start Redis (it will replay the AOF)
systemctl start redis
# Verify
redis-cli PING
redis-cli DBSIZEIf using Cloudflare R2 or AWS S3, enable cross-region replication at the bucket level. This provides automatic, continuous backup of all objects.
For manual or scheduled sync to a secondary location:
# Configure rclone remote (one-time setup)
rclone config create backup-dest s3 \
provider=Other \
access_key_id=${BACKUP_S3_ACCESS_KEY} \
secret_access_key=${BACKUP_S3_SECRET_KEY} \
endpoint=${BACKUP_S3_ENDPOINT}
# Sync primary bucket to backup
rclone sync primary:${S3_BUCKET} backup-dest:${BACKUP_S3_BUCKET}/s3-mirror/ \
--transfers 16 \
--checkers 32 \
--log-file /var/log/rendertrust/rclone-sync.log \
--log-level INFO
# Verify sync
rclone check primary:${S3_BUCKET} backup-dest:${BACKUP_S3_BUCKET}/s3-mirror/# Sync S3 objects every hour
0 * * * * /usr/local/bin/rclone sync primary:${S3_BUCKET} backup-dest:${BACKUP_S3_BUCKET}/s3-mirror/ --log-file /var/log/rendertrust/rclone-sync.log --log-level INFOCritical env vars (stored in Coolify or .env on the host):
DATABASE_URL-- PostgreSQL connection stringREDIS_URL-- Redis connection stringSTRIPE_SECRET_KEY/STRIPE_WEBHOOK_SECRET-- Payment credentialsJWT_SECRET_KEY-- Authentication secretS3_ACCESS_KEY_ID/S3_SECRET_ACCESS_KEY/S3_ENDPOINT-- Object storagePOSTHOG_API_KEY-- AnalyticsCLOUDFLARE_TUNNEL_TOKEN-- Tunnel credentials
Backup procedure:
# Export Coolify environment (via Coolify API or manual export)
# NEVER commit secrets to git -- use encrypted storage
# Encrypt and upload to S3
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
tar czf - .env | gpg --symmetric --cipher-algo AES256 -o /tmp/env_backup_${TIMESTAMP}.tar.gz.gpg
aws s3 cp /tmp/env_backup_${TIMESTAMP}.tar.gz.gpg s3://${BACKUP_S3_BUCKET}/config/
rm /tmp/env_backup_${TIMESTAMP}.tar.gz.gpgCoolify manages Let's Encrypt certificates automatically. On recovery, Coolify will re-provision certificates. No manual backup required, but note:
- Custom certificates (if any) should be stored in encrypted S3
- Cloudflare origin certificates are managed in the Cloudflare dashboard
# Tunnel credentials file location (if using cloudflared directly)
# Typically: /etc/cloudflared/<tunnel-id>.json
# Back up to encrypted S3 alongside env varsScenario: Complete loss of the primary Hetzner VPS. Must rebuild from scratch.
Estimated time: 2-4 hours
- Log in to Hetzner Cloud Console
- Create new server:
- Type: CPX31 (or equivalent to lost server)
- Location: Same as original (or nearest alternative)
- Image: Ubuntu 22.04 LTS
- SSH Key: Add team SSH keys
- Note the new server IP address
# SSH into new server
ssh root@<NEW_SERVER_IP>
# Install Coolify (official one-liner)
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
# Wait for Coolify to start, then access at https://<NEW_SERVER_IP>:8000
# Complete initial setup (admin account, etc.)# Download encrypted env backup from S3
aws s3 cp s3://${BACKUP_S3_BUCKET}/config/env_backup_<LATEST>.tar.gz.gpg /tmp/
gpg --decrypt /tmp/env_backup_<LATEST>.tar.gz.gpg | tar xzf - -C /opt/rendertrust/
# Import environment variables into Coolify
# Use Coolify UI or API to set environment variables for each service- In Coolify, connect to the GitHub repository (ByBren-LLC/rendertrust)
- Configure build settings (Dockerfile, branch:
dev) - Set environment variables from the restored config
- Deploy the application
# Download latest backup from S3
aws s3 ls s3://${BACKUP_S3_BUCKET}/db/ | sort | tail -1
aws s3 cp s3://${BACKUP_S3_BUCKET}/db/rendertrust_db_<LATEST>.dump /tmp/
# Restore using the restore script
./scripts/restore-db.sh /tmp/rendertrust_db_<LATEST>.dump
# Or manually:
pg_restore --host=localhost --port=5432 --username=rendertrust \
--dbname=rendertrust --clean --if-exists \
/tmp/rendertrust_db_<LATEST>.dump
# Verify migration state
psql -d rendertrust -c "SELECT * FROM alembic_version;"
# Apply any pending migrations
cd /opt/rendertrust && alembic upgrade head# Download latest Redis backup
aws s3 cp s3://${BACKUP_S3_BUCKET}/redis/appendonly_<LATEST>.aof /var/lib/redis/appendonly.aof
chown redis:redis /var/lib/redis/appendonly.aof
# Restart Redis
systemctl restart redis
# Verify
redis-cli PING
redis-cli DBSIZE# Health check
curl -f https://api.rendertrust.com/health || echo "HEALTH CHECK FAILED"
# Verify database connectivity
curl -f https://api.rendertrust.com/api/v1/status || echo "API STATUS FAILED"
# Verify key endpoints
curl -s https://api.rendertrust.com/api/v1/credits/balance -H "Authorization: Bearer <test-token>"
# Run smoke test suite (if available)
pytest tests/smoke/ -v
# Check logs for errors
docker logs rendertrust-api --tail 100- Log in to Cloudflare dashboard
- Update A/AAAA records to point to new server IP (if not using Cloudflare Tunnel)
- If using Cloudflare Tunnel, update tunnel configuration to point to new server
- Verify DNS propagation:
dig api.rendertrust.com
# Re-install backup cron
crontab -l > /tmp/crontab.bak 2>/dev/null || true
echo "0 2 * * * /opt/rendertrust/scripts/backup-cron.sh" >> /tmp/crontab.bak
crontab /tmp/crontab.bak
# Verify
crontab -l- Application responds to health checks
- Users can authenticate (JWT validation)
- Database queries return expected data
- Stripe webhooks are receiving events
- Redis caching is operational
- S3 object access works
- Backup cron is scheduled
- Monitoring/alerting is reconnected
- PostHog events are flowing
Symptoms: Application errors referencing database integrity, failed queries, ORM exceptions.
# 1. Stop application to prevent further writes
docker stop rendertrust-api
# 2. Assess damage
psql -d rendertrust -c "SELECT count(*) FROM pg_stat_activity;"
psql -d rendertrust -c "\dt+" # Check table sizes
# 3. Restore from latest backup
./scripts/restore-db.sh <latest_dump_file_or_s3_path>
# 4. Apply any pending migrations
alembic upgrade head
# 5. Restart application
docker start rendertrust-api
# 6. Verify
curl -f https://api.rendertrust.com/healthData loss window: Up to 24 hours (last backup). Consider WAL replay for point-in-time recovery if WAL archiving is enabled.
Symptoms: Slow responses, cache misses, session issues, queue processing stalled.
# 1. Check Redis status
redis-cli PING
systemctl status redis
# 2. If Redis is down, attempt restart
systemctl restart redis
# 3. If data is corrupt, restore from backup
systemctl stop redis
cp /backup/redis/appendonly_<LATEST>.aof /var/lib/redis/appendonly.aof
chown redis:redis /var/lib/redis/appendonly.aof
systemctl start redis
# 4. If no backup available, flush and let the app repopulate
redis-cli FLUSHALL
systemctl restart redis
# 5. Verify
redis-cli PING
redis-cli DBSIZEImpact: Redis loss is recoverable without data loss to the application. Caches and queues will repopulate. Active job dispatches may need to be re-queued.
Symptoms: 404 errors on object retrieval, missing render artifacts.
# 1. Check S3 bucket status
aws s3 ls s3://${S3_BUCKET}/ --summarize
# 2. If cross-region replication is configured, sync from replica
rclone sync backup-dest:${BACKUP_S3_BUCKET}/s3-mirror/ primary:${S3_BUCKET}/ \
--transfers 16 \
--log-file /var/log/rendertrust/s3-restore.log
# 3. If no replication, restore from rclone backup
aws s3 sync s3://${BACKUP_S3_BUCKET}/s3-mirror/ s3://${S3_BUCKET}/
# 4. Verify
aws s3 ls s3://${S3_BUCKET}/ --summarizeData loss window: Up to 1 hour (hourly rclone sync).
Symptoms: Application not responding, container crashed, OOM kill.
# 1. Check container status
docker ps -a | grep rendertrust
# 2. Check logs for root cause
docker logs rendertrust-api --tail 200
# 3. Restart the application
docker restart rendertrust-api
# 4. If OOM, check memory and increase limits
docker stats rendertrust-api
# Update docker-compose.yml memory limits if needed
# 5. If code issue, redeploy from known-good commit
cd /opt/rendertrust
git log --oneline -5
git checkout <known-good-commit>
docker compose up -d --build
# 6. Verify
curl -f https://api.rendertrust.com/healthFrequency: First Monday of each month Duration: 1-2 hours Scope: Partial recovery test
Procedure:
- Verify latest backup exists and is recent (< 24 hours old)
- Download backup to a test environment
- Restore database to a scratch instance
- Verify row counts match production (within expected delta)
- Verify Alembic migration version matches
- Document results in Linear ticket
Checklist:
- Backup file exists in S3
- Backup file is less than 24 hours old
-
pg_restore --listsucceeds (backup is valid) - Restore to scratch database succeeds
- Row counts are reasonable
- Alembic version matches production
- Results documented
Frequency: First week of each quarter (January, April, July, October) Duration: 4-6 hours Scope: Full environment rebuild
Procedure:
- Provision a temporary Hetzner VPS
- Follow the complete "Section 5: Full Recovery Procedure"
- Verify all post-recovery checklist items pass
- Measure actual RTO (must be under 4 hours)
- Destroy temporary VPS after testing
- Document results and update runbook if procedures have changed
Checklist:
- Temporary VPS provisioned
- Coolify installed successfully
- Application deployed and running
- Database restored and verified
- Redis restored and operational
- Health checks passing
- API endpoints responding correctly
- Actual RTO measured and documented
- Temporary VPS destroyed
- Runbook updated if procedures changed
- Results documented in Linear ticket
| Priority | Contact | Role | Method | Response SLA |
|---|---|---|---|---|
| P1 (Critical) | J. Scott Graham | POPM / Platform Owner | Phone + Slack | 15 min |
| P2 (High) | On-call Engineer | Platform Engineering | Slack #incidents | 30 min |
| P3 (Medium) | Platform Team | Engineering | Slack #platform | 4 hours |
| P4 (Low) | Platform Team | Engineering | Linear ticket | Next business day |
| Trigger | Priority | Action |
|---|---|---|
| Full VPS loss | P1 | Immediately escalate to POPM, begin full recovery |
| Database corruption | P1 | Stop writes, escalate, begin DB restore |
| Backup failure (2+ consecutive) | P2 | Investigate root cause, manual backup |
| Redis failure | P2 | Restart Redis, restore from backup if needed |
| S3 partial data loss | P2 | Begin rclone restore from replica |
| Application crash (auto-recovers) | P3 | Investigate root cause, monitor |
| Single backup failure | P3 | Investigate, verify next backup succeeds |
- Primary: Slack
#incidentschannel - Secondary: Email to
platform@rendertrust.com - Status Page: Update
status.rendertrust.comfor user-facing incidents
See .env.template in the repository root for a complete list of required environment variables. Critical variables for DR:
DATABASE_URL-- PostgreSQL connection stringREDIS_URL-- Redis connection stringBACKUP_S3_BUCKET-- S3 bucket for backupsBACKUP_S3_ACCESS_KEY-- S3 credentials for backup operationsBACKUP_S3_SECRET_KEY-- S3 credentials for backup operationsBACKUP_S3_ENDPOINT-- S3-compatible endpoint URLALERT_WEBHOOK_URL-- Webhook for backup failure alerts
scripts/backup-db.sh-- Daily PostgreSQL backupscripts/restore-db.sh-- PostgreSQL restore from dump file or S3scripts/backup-cron.sh-- Cron wrapper for automated backups