When to use this runbook: planning a backup strategy for a new Powernode deployment, automating backups on an existing deployment, recovering from data loss, or running a quarterly restore drill. Companion to production-deployment.md, which references but does not deeply cover backup procedure.
- What gets backed up
- Backup procedure
- Retention policy
- Restore procedure
- Quarterly restore drill
- pgvector considerations
- Point-in-time recovery (PITR)
- Disaster scenarios
A Powernode backup contains the full primary database dump, including:
- All table data — accounts, users, agents, conversations, messages, learnings, knowledge entries, shared memory pools, AI agent executions, audit logs.
- Vector embeddings stored in pgvector columns (e.g.
ai_knowledge_graph_nodes.embedding,ai_shared_knowledges.embedding). Postgres backs these up as standard column data; no special handling is needed once the pgvector extension is installed on the restore target. - Schema — all migrations, indexes (including pgvector HNSW indexes), constraints, sequences.
- Extension declarations —
CREATE EXTENSION pgvectorandCREATE EXTENSION pgcryptoare emitted bypg_dumpand replayed on restore. The pgvector extension binary must be installed on the restore target before restoring; otherwise the restore fails on theCREATE EXTENSIONline.
Not in the backup:
- Vault secrets — keys/secrets live in HashiCorp Vault and have their own backup process (see infrastructure/vault-example/). The DB only stores Vault key paths, not values.
- Generated PDFs/CSVs — these live on the worker filesystem (
worker/storage/reports/) and are regenerable from data. Snapshot the filesystem separately if you want point-in-time report continuity. - Sidekiq Redis state — in-flight jobs. Sidekiq is treated as ephemeral; on restore, scheduled jobs will be re-emitted by their owning models.
The repository ships scripts/backup/backup-database.sh. Schedule it via cron on the database host (or an adjacent host with network access to Postgres):
# /etc/cron.d/powernode-backup
0 2 * * * powernode cd /opt/powernode && /opt/powernode/scripts/backup/backup-database.sh >> /var/log/powernode-backup.log 2>&1Required environment variables (loaded from /etc/powernode/backend-default.conf or the operator's preferred env file):
| Variable | Purpose |
|---|---|
POSTGRES_HOST |
Database host (default localhost) |
POSTGRES_USER |
Postgres role with pg_dump access to the application database |
POSTGRES_PASSWORD |
Password for that role |
POSTGRES_DB |
Application database name (powernode_production) |
BACKUP_DIR |
Local backup directory (default /backups) |
RETENTION_DAYS |
Local retention (default 30) |
S3_BUCKET |
Optional S3 bucket for off-host replication |
AWS_REGION |
AWS region when using S3 |
Each invocation writes ${BACKUP_DIR}/powernode_YYYYMMDD_HHMMSS.sql.gz and, if S3_BUCKET is set, uploads the file to s3://${S3_BUCKET}/backups/.
Run the same script with an explicit name for triage backups (e.g. before a risky migration):
sudo -u powernode \
BACKUP_DIR=/var/backups/powernode \
/opt/powernode/scripts/backup/backup-database.sh "pre_migration_${USER}_$(date +%s)"The script logs file size and (when S3_BUCKET is set) the S3 ETag. Always verify both:
ls -la /backups/ | head
aws s3 ls "s3://${S3_BUCKET}/backups/" | tail -5A backup smaller than ~10% of the previous successful backup is suspicious — investigate before relying on it.
| Tier | Retention | Storage |
|---|---|---|
| Daily | 30 days | Local disk (BACKUP_DIR) |
| Weekly | 13 weeks | S3 (move oldest-of-week before cleanup; rotate via lifecycle policy) |
| Monthly | 12 months | S3 (set lifecycle to Glacier for archival cost reduction) |
RETENTION_DAYS=30 on the daily cron handles local cleanup. Weekly/monthly tiering happens via S3 lifecycle policy — Powernode does not currently ship one. Sample policy:
{
"Rules": [
{
"ID": "weekly-glacier",
"Status": "Enabled",
"Prefix": "backups/",
"Transitions": [
{ "Days": 90, "StorageClass": "GLACIER" }
],
"Expiration": { "Days": 365 }
}
]
}Use
scripts/backup/restore-database.sh. The script drops and recreates the target database — never run it against production without an explicit recovery decision.
- Stop all Powernode services so they don't write during restore:
sudo systemctl stop powernode.target
- Confirm the pgvector + pgcrypto extensions are installed on the restore target:
Both rows must come back. Install via
sudo -u postgres psql -d postgres -c "SELECT name FROM pg_available_extensions WHERE name IN ('vector','pgcrypto');"apt install postgresql-15-pgvector(or the version-matched package) before continuing. - Validate the backup file integrity:
gunzip -t /backups/powernode_20260518_020000.sql.gz && echo "gzip OK"
sudo -u powernode \
POSTGRES_HOST=localhost \
POSTGRES_USER=postgres \
POSTGRES_PASSWORD=... \
POSTGRES_DB=powernode_production \
/opt/powernode/scripts/backup/restore-database.sh /backups/powernode_20260518_020000.sql.gzsudo -u powernode \
AWS_REGION=us-west-2 \
POSTGRES_HOST=localhost \
POSTGRES_USER=postgres \
POSTGRES_PASSWORD=... \
POSTGRES_DB=powernode_production \
/opt/powernode/scripts/backup/restore-database.sh "s3://your-bucket/backups/powernode_20260518_020000.sql.gz"After the restore script exits cleanly:
- Schema version:
No
cd /opt/powernode/server && bundle exec rails db:migrate:status | tail -20
downrows should appear past the latest backup's recorded migration. - Row counts against an expected baseline:
sudo -u postgres psql powernode_production -c " SELECT 'users' AS table, COUNT(*) FROM users UNION ALL SELECT 'accounts', COUNT(*) FROM accounts UNION ALL SELECT 'ai_agents', COUNT(*) FROM ai_agents UNION ALL SELECT 'audit_logs', COUNT(*) FROM audit_logs;"
- Vector indexes:
All HNSW/IVFFlat indexes from before the restore should be present.
sudo -u postgres psql powernode_production -c " SELECT indexname FROM pg_indexes WHERE indexdef LIKE '%hnsw%' OR indexdef LIKE '%ivfflat%';"
- App boot:
All services should be
sudo systemctl start powernode.target sudo scripts/systemd/powernode-installer.sh status
active (running)within 30 seconds.
Production backups that have never been tested for restore are not backups — they are unverified files. Run a drill at minimum every 90 days:
- Provision a throwaway database on a non-production host (
createdb powernode_restore_drill). - Restore the most recent production backup into it.
- Run the post-restore verification steps; record row counts, duration, any error output.
- Boot a Powernode instance pointed at the drill DB (
POSTGRES_DB=powernode_restore_drill), verify a few API endpoints respond (/api/v1/health,/api/v1/auth/loginwith a known user). - Tear down the drill DB (
dropdb powernode_restore_drill). - Log results to your incident response tooling.
A failed drill is a P1 — your stated RTO does not hold until it is resolved.
- Extension binary version: pgvector 0.5.0 changed index format. If you restore a 0.5+ backup onto a 0.4.x server you will get index-corruption errors. Match the extension version on the restore target. Check with
SELECT extversion FROM pg_extension WHERE extname = 'vector';. - HNSW build time: HNSW indexes are large. On a database with millions of vector rows, the
CREATE INDEXstatements emitted bypg_dumpcan take 30+ minutes on restore. Plan recovery windows accordingly. - Embedding column sizes: existing embedding columns are 1536 dims (OpenAI) and 768 dims (Ollama-default). A dump preserves these; if you change embedding model post-restore, you will need to re-embed via
cd worker && bundle exec rails ai:reembed.
Powernode does not ship a PITR setup out of the box — the recommended path for organizations needing PITR:
- Enable WAL archiving in
postgresql.conf:wal_level = replica archive_mode = on archive_command = 'aws s3 cp %p s3://${WAL_BUCKET}/wal/%f' - Take regular base backups with
pg_basebackup -D /backups/base -F t -z -X stream. - Configure
recovery.conf(Postgres 11) orpostgresql.auto.confrecovery target settings (Postgres 12+) on the restore host.
If PITR is required for compliance, retain WAL archives for at least the legal retention window for transactional data (often 7 years for financial records — confer with your compliance team).
| Scenario | Response |
|---|---|
| Corrupted table after a bad migration | Restore the most recent backup into a sidecar DB, pg_dump --table=<name> the affected table, psql it into production. Avoid full-DB restore if isolated. |
| Entire database lost (volume failure) | Provision new DB host, install pgvector matching version, restore from latest backup, point services at new host, run post-restore verification. |
| Region failure | Restore from cross-region S3 copy of latest backup into a host in a healthy region. Update DNS / load balancer to point at new endpoint. |
| Ransomware encryption of backup directory | Restore from S3 (assumed immutable / versioned / cross-region). If S3 is also compromised, your RPO is whatever the oldest off-platform archive provides — this is why monthly Glacier tier is non-optional. |
- production-deployment.md — initial deployment + service management
- docker-swarm.md — Swarm-specific operations
- performance-tuning.md — Postgres tuning parameters