Skip to content

Backup and Restore

Writ edited this page Aug 5, 2026 · 2 revisions

Backup and restore

What there is to lose

Where If you lose it
SECRET_ENCRYPTION_KEY .env Unrecoverable. Stored credentials cannot be decrypted, even from a perfect database backup.
Database + files writ-data volume Everything: workflows, runs, users, extracted data, uploads.
Certificates caddy-data volume Only certificates — Caddy re-issues. But Let's Encrypt allows 5 duplicates per domain per week.
The other secrets .env Sessions and agent tokens are invalidated; everyone signs in again and agents re-enrol.

The key and the data must not live in the same place. That is the entire point: a backup of the volume is useless to an attacker who does not also have the key, and useless to you if you kept the key only in the volume.

Back up

Everything the app owns is on one volume. SQLite is a single file, so a copy taken while the coordinator is stopped is complete and consistent:

docker compose --profile tls down
docker run --rm -v writ_writ-data:/data -v "$PWD":/backup alpine \
  tar czf /backup/writ-$(date +%F).tar.gz -C /data .
docker compose --profile tls up -d

The volume name is <project>_writ-data, where the project is the directory name (writ if you cloned normally). docker volume ls if unsure.

Without stopping

If you cannot take the downtime, use SQLite's own online backup — it is crash-consistent where a raw cp of a live database is not:

docker compose exec coordinator \
  sqlite3 /data/writ.db ".backup '/data/writ-backup.db'"
docker compose cp coordinator:/data/writ-backup.db ./writ-$(date +%F).db
docker compose exec coordinator rm /data/writ-backup.db

Note this covers the database only. Uploaded and extracted files live under /data/files and need copying separately.

The key

Once, by hand, into whatever you use for secrets — a password manager, a vault, an envelope in a drawer:

grep '^SECRET_ENCRYPTION_KEY=' .env

It never changes unless you rotate it, so this is a one-time job. Do it now rather than later.

Restore

# 1. Recreate .env with the ORIGINAL secrets — at minimum SECRET_ENCRYPTION_KEY.
#    Do NOT run gen-env.sh: it mints new keys and your stored credentials
#    become undecryptable.
cp /path/to/backup/.env .env
chmod 600 .env

# 2. Restore the volume.
docker compose down
docker volume create writ_writ-data
docker run --rm -v writ_writ-data:/data -v "$PWD":/backup alpine \
  tar xzf /backup/writ-2026-07-29.tar.gz -C /data

# 3. Start. Migrations run automatically and are idempotent.
docker compose --profile tls up -d --build

Then check: sign in, open a workflow that uses a stored credential, and run it. If the credential decrypts, you restored the right key.

Moving to another server

Same as a restore, plus:

  1. Point DNS at the new machine and let it propagate.
  2. Re-run ./scripts/deploy.sh <domain> <email> there — it rewrites the domain settings and gets a fresh certificate. (Do not copy caddy-data across; let it re-issue.)
  3. Re-enrol your agents. Their tokens still work, but if the hostname changed they are dialing the old address. Mint fresh pairing codes from Fleet.

Rotating secrets

Rotating any signing secret invalidates every session and agent token — everyone signs in again, every agent re-enrols. Plan for that.

SECRET_ENCRYPTION_KEY is different and much more dangerous: it is not a signing key, it is the key your data is encrypted with. Changing it does not re-encrypt anything — it makes the existing ciphertext unreadable. Do not rotate it unless you have a specific plan to decrypt and re-encrypt first.

What to actually do

  • SECRET_ENCRYPTION_KEY stored off this server, today
  • writ-data in a scheduled backup, with retention
  • A restore rehearsed once on a throwaway host — an untested backup is a hypothesis
  • The backup destination is not the same disk, host, or cloud account

Clone this wiki locally