Backups with Litestream
By default cronpad runs on plain SQLite with no replication. For continuous backups, its container image includes Litestream, which you turn on with one environment variable. It then streams every change to both SQLite files to object storage (S3, GCS, Azure Blob, any S3-compatible service, SFTP) within about a second, and on startup, if the data volume is empty, the container restores the latest copy automatically. You can also restore to any point in time within your retention window. The queue file (cronpad-queue.db) needs no backup: cronpad rebuilds it from the restored database.
Enable replication
Set the replica location and credentials on the container:
LITESTREAM_REPLICA_URL=s3://my-bucket/cronpad
AWS_ACCESS_KEY_ID=…
AWS_SECRET_ACCESS_KEY=…
# For S3-compatible services such as SeaweedFS, Garage or Cloudflare R2:
# LITESTREAM_ENDPOINT=https://<account>.r2.cloudflarestorage.com
When LITESTREAM_REPLICA_URL is set, the entrypoint runs cronpad under Litestream with this configuration (deploy/litestream.yml):
dbs:
- path: /data/cronpad.db
replicas:
- url: ${LITESTREAM_REPLICA_URL}/control
- path: /data/history.db
replicas:
- url: ${LITESTREAM_REPLICA_URL}/history
Without it, cronpad runs as plain SQLite with no replication; see SQLite only.
SQLite only (the default)
With LITESTREAM_REPLICA_URL unset, the entrypoint runs plain cronpad serve: no restore on start, no replication. Startup logs one INFO line, running SQLite only; set LITESTREAM_REPLICA_URL to replicate. The bare binary always runs this way, since Litestream is only bundled in the container image.
Earlier releases warned when no replica was set and offered LITESTREAM_DISABLED=true to silence it. That variable is no longer used: if it’s still set, startup mentions it at INFO level and otherwise ignores it.
What it means: your data lives only in the two SQLite files, cronpad.db (jobs, keys, settings) and history.db (run history), on the data volume. Nothing is streamed anywhere, so a lost disk loses everything written since your last copy. The queue file still needs no backup.
It’s a reasonable choice for local development, evaluation, and small installs where losing a day of history is acceptable or the host already snapshots its disks. Keep a separate copy of secrets.encryption_key regardless.
Simple alternatives to continuous replication:
-
Scheduled
sqlite3 .backup. Back up both databases, for example from cron or a systemd timer:sqlite3 /data/cronpad.db ".backup '/backups/cronpad-$(date +%F).db'" sqlite3 /data/history.db ".backup '/backups/history-$(date +%F).db'".backuptakes a consistent snapshot while cronpad is running. To restore, stop cronpad, put the files back ascronpad.dbandhistory.db, and delete any leftover-waland-shmfiles next to them. -
Volume or disk snapshots from your cloud provider or filesystem (ZFS, LVM, EBS), taken as a single atomic snapshot of the volume holding both files.
Automatic restore on start
On start, the container:
- Restores
cronpad.dbfrom the replica if the file doesn’t exist and a replica does. - Restores
history.dbthe same way, into a temporary file. If that fails, takes more than 5 seconds, or the replica is missing, cronpad starts with an empty history and logs an error. SetLITESTREAM_HISTORY_RESTORE_TIMEOUTto change this bound. A partial restore is never opened. - Starts replication and cronpad.
So recovering from a lost server is: start a new container with the same LITESTREAM_REPLICA_URL and an empty volume.
The shell-free container runs cronpad entrypoint, which performs these steps and replaces itself with litestream replicate -exec "cronpad serve". The bundled CLI is pinned to Litestream 0.3.13; the optional embedded library is omitted because its API is not stable. Control restore errors stop startup to avoid scheduling from an incomplete control database.
Manual and point-in-time restore
Stop cronpad first, then:
# Latest state
litestream restore -o /data/cronpad.db "$LITESTREAM_REPLICA_URL/control"
litestream restore -o /data/history.db "$LITESTREAM_REPLICA_URL/history"
# As of a moment in the past
litestream restore -timestamp 2026-11-10T03:00:00Z -o /data/cronpad.db "$LITESTREAM_REPLICA_URL/control"
After a point-in-time restore, cronpad’s misfire policy decides what happens to runs that came due between the restore point and now.
What else to keep safe
secrets.encryption_key. Store it in your secret manager. Restored secrets can’t be decrypted without it.- OAuth client secrets and SMTP credentials, which live in your config or environment, not the database.
Test your restores
Run tools/litestream-restore-test.sh from the repository for a local file:// recovery exercise. It downloads the pinned Litestream binary into scratch only when missing, creates a job and runs, destroys the whole data directory (including the queue file), then verifies restore and resumed scheduling. With the Litestream binary cached it runs without network access.
Restore into a scratch directory regularly and start a throwaway cronpad on it. Its queue file is created inside the scratch directory, so it can’t touch your production server’s queue. It will schedule the restored jobs, though, so start it with CRONPAD_SINKS__WEBHOOK__ENABLED=false and CRONPAD_SINKS__COMMAND__ENABLED=false: runs then fail with “sink disabled or unavailable” instead of reaching your real endpoints. A backup you haven’t restored is a hope, not a backup.