Backups
Scheduled and on-demand backups of a managed service, to local disk or any S3-compatible bucket — and how to restore one.
Backups run the service's own client inside its own container. There is no separate backup image holding credentials, and no version-skew between a dump tool and the database it dumps.
Scheduling
Set a five-field cron expression on the service — 0 3 * * * for 03:00 daily. The maintenance
job asks which services are due and runs those.
The expression is validated for shape, not for range. Checking each field would duplicate the scheduler's own parser and drift from it. What the check does catch is what operators actually get wrong here: a six-field expression copied from a different scheduler, and free text.
BACKUP_TIMEOUT_SECONDS (default 3600) is the ceiling on one backup step. A dump of a large
database is slow, and killing it halfway produces a file that looks like a backup and is not
one.
Where they go
| Target | Destination |
|---|---|
LOCAL | /var/lib/sthora/backups/<slug>/ |
S3 | Any S3-compatible endpoint — bucket, region, endpoint, access key, secret key |
The S3 destination is write-only, exactly like an environment variable: it is stored encrypted and returned by no endpoint. The upload runs through a pinned AWS CLI image.
A service set to S3 with no destination configured fails its backup with a
message saying so. Set the destination before you set the target.
backupRetentionCount (1–365) prunes what falls out of retention after each successful
backup. Only LOCAL artefacts are pruned by the platform — an S3 bucket's lifecycle is the
bucket's own business.
What each kind produces
| Kind | Command | File |
|---|---|---|
| PostgreSQL | pg_dump --format custom | .dump |
| Redis | redis-cli SAVE, then copy dump.rdb | .rdb |
| MinIO | mc alias set, then tar -czf of the data directory | .tar.gz |
Postgres custom format is compressed and restorable selectively. A plain SQL dump of a database with a large table is several times the size.
Redis SAVE rather than BGSAVE because SAVE is synchronous: the file is complete when
the command returns. BGSAVE returns before the fork finishes, and the copy would race it.
MinIO gets a tar because object storage has no dump format. The archive is it.
A failed backup opens an alert
BACKUP_FAILED is a CRITICAL alert. A backup that quietly stopped running is worse than no
backup, because you find out at the moment you need it.
Restoring
Restores are a deliberate operator action, run from the box.
# PostgreSQL
docker cp <backup>.dump sthora-svc-<slug>:/tmp/restore.dump
docker exec sthora-svc-<slug> \
pg_restore --username sthora --dbname app --clean --if-exists /tmp/restore.dump
# Redis — it loads its dump only at startup, so the restart is required
docker cp <backup>.rdb sthora-svc-<slug>:/data/dump.rdb
docker restart sthora-svc-<slug>
# MinIO
docker cp <backup>.tar.gz sthora-svc-<slug>:/tmp/restore.tar.gz
docker exec sthora-svc-<slug> tar -xzf /tmp/restore.tar.gz -C /dataThe platform's own database
Separate from all of this. It is dumped nightly to
/var/lib/sthora/backups/platform/, and again by
install.sh --upgrade before it touches anything. See
backup and restore.
What is not backed up
An application's volumes. Back those up from inside the application, or keep anything you would regret losing in a managed service.