Operations

Backup & restore

What to back up, how, and how to restore Postgres and cache state.

Ductor's durability model is simple: PostgreSQL is the source of truth, and Redis/Dragonfly holds rebuildable operational state. Your backup strategy follows directly from that split.

Two things are Tier-0 recovery assets — lose either and no database restore can bring the data back:

  1. The database backup (Postgres).
  2. The connector/payload master key — and, for BYOK tenants, the tenant KEK custody. Losing the active master key with no rotation copy in the keyring makes every stored credential and any AEAD-encrypted payload permanently unrecoverable. The encryption is one-way by design. See Key management and Key custody.

What holds what

StoreContentsRecovery posture
PostgreSQLWorkflow runs (eec_workflow_run), step attempts (eec_workflow_step_attempt), pools, recipients, rules, tenants, connections (encrypted), idempotency records, auditMust be backed up. Losing it loses durable state.
Redis / DragonflyCache, flow-control counters, tiered queue, pub/subRebuildable, but its loss is disruptive (in-flight queue items, capacity counters).
Object storage (if archival on)Archived history/visibility streamsBack up per your storage backend's policy.
The connector encryption keyAEAD master key (config/secret, not in the database)Back up separately and securely — see the warning below.

The durable runtime tables above share an eec_ prefix — short for Enterprise Eventing Core, Ductor's event, task, and workflow store.

Back up the encryption key separately — it is Tier-0

Connector credentials in Postgres are encrypted with connector.encryption_key, which lives in your config/secret store — not in the database. A database restore is useless for connections without the matching key. Store the key (and any connector.rotation_keys) in your secret manager with its own backup and rotation policy, at the same retention class as the database backup. If you run the AEAD payload codec, the same key protects payload bytes; for BYOK tenants the tenant KEK custody is Tier-0 too.

Backing up PostgreSQL

Use standard PostgreSQL tooling — Ductor adds nothing special. The schema is plain Postgres (TimescaleDB extensions where used).

Logical dump

pg_dump --format=custom --no-owner \
  "postgres://ductor:$PGPASS@host:5432/ductor" \
  -f ductor-$(date +%F).dump

On Dokploy

Dokploy can schedule Postgres backups of the timescale service directly from its UI — configure a backup schedule and destination there.

On Kubernetes

Back up your managed/operator-run Postgres with its native mechanism (base backups + WAL archiving for point-in-time recovery, or scheduled pg_dump). Ductor connects to an external database in production Helm setups, so backups are that database's responsibility.

Prefer PITR for production

A nightly pg_dump bounds data loss to a day. For tighter RPO, use continuous archiving (base backup + WAL) so you can restore to any point in time.

Restoring PostgreSQL

Stop or scale down Ductor so nothing writes during the restore.

Restore the dump into a clean database:

pg_restore --clean --no-owner \
  -d "postgres://ductor:$PGPASS@host:5432/ductor" \
  ductor-2026-07-11.dump

Reconcile the schema. The restored data matches the schema at backup time. Run migrations up to the version your binary expects, then confirm:

ductor migrate up
ductor schema status      # expect: schema=ready

Ensure the connector encryption key in your config/secret store matches the key that encrypted the restored connections.

Start Ductor and verify /ready returns 200.

Redis / Dragonfly

Cache and flow-control state rebuilds itself as traffic resumes, so a cold cache is a performance dip, not data loss. The tiered queue is the exception — items in flight when Redis is lost may need to be re-driven. If you want a warm restart, back up Dragonfly with its snapshot mechanism (the compose volume is dragonfly_data); otherwise let it repopulate. Either way, Ductor's coordinator/worker model means progress already committed to Postgres is safe — a run resumes from durable state.

Additional recovery requirements

Two subsystems need consistency guarantees beyond a whole-database dump:

  • Billing ledger — the stripe_* tables (stripe_wallet_balance, stripe_charge_event, stripe_customer_mapping, stripe_payment_config) hold local wallet balances and payment policy that Stripe cannot reconstruct (see Billing & Usage → Stripe integration for what the wallet and charge subsystem is). Take a PITR-consistent pre-change snapshot with a single pg_dump --serializable-deferrable of those tables, quiesce billing writes, and restore into an isolated cluster before promoting. Treat the four-table snapshot and the master-key backup as the same retention class.

  • Multi-region replication failover — promote a follower cluster to leader when the primary region is unreachable. Verify the candidate has drained its inbox (replication_inbox_lag_seconds ~ 0), quiesce the leader, then flip the leader bit in each tenant's replication policy. Outbox/inbox lag forensics (replication_dispatcher_lag_seconds, replication_inbox_apply_failures_total) tell you whether a follower is safe to promote. Failover is irreversible without a second swap.

Restore drill

Rehearse restores before you need them:

  • Take a fresh pg_dump and restore it into a scratch database.
  • Run ductor migrate up and ductor schema status against it.
  • Point a throwaway Ductor at it with the real connector encryption key and confirm a connection decrypts.
  • Confirm /ready returns 200.