Automatic, encrypted database backups. Zero code changes.
Archon is one Docker container you run next to your app. On a schedule, it dumps your database, encrypts the dump, fingerprints it with a checksum, ships it to S3, Azure or local disk, and deletes old copies. When you need it back, one API call verifies and restores it.
- 1 Dump
- 2 Encrypt
- 3 Checksum
- 4 Store
- 5 Retain
Dump. On schedule, Archon runs the database's own tool, such as pg_dump, in a subprocess.
You add one container. Archon handles the rest.
Which databases, how often, where to store, how many copies to keep.
Dump, encrypt with AES-256, checksum with SHA-256, upload, prune old copies.
One API call. The file is verified before it is decrypted, and decrypted before anything is written.
See it work
a backup, then a tampered file refused on restore$ curl -X POST :8765/backup -d '{"database":"primary_postgres"}' 202 {"job_id":"b3f1…","status":"queued"} backup_started primary_postgres pg_dump encrypt AES-256-CBC checksum sha256 9c4e…a71f storage.write s3://backups/ backup_completed …2026-09-21T14-00-01_daily.sql.enc retention daily 14 · weekly 8 · pruned 1 $ curl -X POST :8765/restore -d '{…,"confirm":true}' integrity_failed sha256 mismatch restore aborted · database untouched
Every team rebuilds backups. Most rebuild them badly.
A cron job here, a shell script there. No encryption, no checksum, and nobody finds out it was broken until the day it is needed. Then the next project does it again, slightly differently.
- A
pg_dumpcron job with no encryption - No way to tell whether last night's backup worked
- Backups pile up until the disk fills
- Restore means SSH and guesswork
- Written again for every project and language
- AES-256-CBC on every file; the key never touches disk
- A SHA-256 sidecar, verified before every restore
- Hourly, daily, weekly and monthly retention, enforced
- One
POST /restorecall, drop-and-recreate, audited - One image and one config file for any stack
Nothing asked of your codebase. No SDK import, no app-side hook, no vendor lock-in. If you can add a block to docker-compose.yml, you are done.
Two fixed pipelines. No shortcuts, no reordering.
Every backup and every restore goes through the same steps in the same order. The restore path checks integrity before it decrypts, and decrypts before it writes anything.
Backup
POST /backup · or on scheduleprovider.backup()Native tool per engine, run in its own subprocess.
encrypt()AES-256-CBC with a key held only in memory.
checksum()SHA-256 written as a .sha256 sidecar file.
storage.write()Local disk, Amazon S3 or Azure Blob.
retention.enforce()Prunes old files by your hourly to monthly mix.
Restore
POST /restore · confirm: true requiredstorage.read()Fetches the file and its checksum sidecar.
verify_checksum()On mismatch the restore stops here.
GATE · aborts on mismatchdecrypt()Only after the file is proven intact.
provider.restore()Drop, recreate, restore. No partial state.
Built for the day something goes wrong.
Requests return 202 with a job ID. A second backup for the same database queues behind the first, never in parallel and never dropped.
Open a session on any backup, browse tables, pick rows, resolve foreign keys, and apply only those rows back.
daily at 14:00 Asia/Kolkata becomes a cron trigger. Each database has its own schedule and target.
/logs/stream tails structured JSON over SSE: queued, started, completed, integrity_failed.
The same events go out as HMAC-signed HTTP calls, so other systems react without polling.
React UI for backups, restores, logs and settings. Dark by default, made to be read during an incident.
Two keys, one config file, one container.
Add this block to your existing docker-compose.yml. Nothing else in your project changes.
archon:
image: archon:latest
volumes:
- ./archon.config.yaml:/app/config.yaml
- ./backups:/app/backups
ports:
- "8765:8765"
env_file: .envBuild the image, copy the example config, and run it with your keys.
docker build -t archon:latest . cp config.yaml.example archon.config.yaml docker run --rm \ -v $(pwd)/archon.config.yaml:/app/config.yaml \ -v $(pwd)/backups:/app/backups \ -p 8765:8765 \ --env-file .env \ archon:latest
Put both in .env. Keep the encryption key safe: without it, backups cannot be decrypted.
ARCHON_API_KEY=$(openssl rand -hex 32) ENCRYPTION_KEY=$(openssl rand -base64 32) DB_USER=myuser DB_PASSWORD=mypassword
One block per database.
Each database gets its own schedule, storage target and retention mix. Postgres, MongoDB, SQLite and MySQL can sit in the same file.
databases:
- name: primary_postgres
type: postgres
host: postgres
port: 5432
db: mydb
user: ${DB_USER}
password: ${DB_PASSWORD}
schedule:
frequency: daily
at: "14:00"
timezone: "Asia/Kolkata"
storage: s3
retention:
daily: 14
weekly: 8${VAR} values come from the environment, so secrets stay out of the file.
Config is read once at startup. Call POST /reload to pick up changes without a restart.
max_parallel caps how many databases back up at once across the whole sidecar.
The full annotated reference is config.yaml.example.
Native tools, always drop-and-recreate on restore.
pg_dump / pg_restoreDrops and recreates the database before restoring.
mongodump / mongorestoreRestores with --drop.
mysqldumpFull dump, full restore.
hot file copyReplaces the file wholesale.
A small REST surface.
Every route except /health needs the X-API-Key header.
| Method | Path | Returns |
|---|---|---|
| POST | /backup | 202 and job ID(s) |
| GET | /jobs/{job_id} | Job status and result |
| POST | /restore | 200, 400 or 500 |
| GET | /backups | Backup listing |
| GET | /status | Scheduler status per database |
| GET | /logs · /logs/stream | Recent logs, or a live SSE tail |
| DELETE | /backups/{filename} | Removes the file and its checksum |
| POST | /reload | Reloads config without a restart |
| POST | /granular/session | Starts a row-level restore session |
| GET | /granular/session/{id}/tables · /table/{t}/rows | Browse a backup |
| POST | /granular/session/{id}/resolve-multi · /restore-multi | Resolve foreign keys, apply rows |
| DELETE | /granular/session/{id} | Closes the session |
| GET | /health | Liveness probe, no key needed |
Why a sidecar.
| Archon | Cron script | Managed (RDS, Atlas) | |
|---|---|---|---|
| Works with any Docker stack | Yes | Yes | Vendor-locked |
| Zero changes to your app | Yes | Usually not | Yes |
| AES-256 encryption at rest | Yes | Usually skipped | You don't hold the key |
| Checksum verified before restore | Yes | No | Opaque |
| Many databases and targets, one config | Yes | One-off per DB | One provider |
| Row-level restore | Yes | No | No |
| Self-hosted, you own the files | Yes | Yes | No |
Rules that never change, by design.
- Checksum verification always runs before decryption and before any database write.
- Every backup gets a
.sha256sidecar. Deleting one deletes both. POST /restorewithoutconfirm: trueis refused. There is no silent restore.- The encryption key is never written to disk.
- Engine-specific logic stays in its provider; cloud SDK calls stay in their storage class.
- Config changes apply only through an explicit
POST /reload.
Not in v1: Slack or email alerts (webhooks cover events), point-in-time recovery, per-tenant backups, config file watching, and job history beyond 24 hours in memory.
Backups nobody thinks about.
Until the day everybody is glad they exist. Add one block to your compose file and point it at your database.