-
Notifications
You must be signed in to change notification settings - Fork 0
Backup and Restore
| 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.
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 -dThe volume name is <project>_writ-data, where the project is the directory name
(writ if you cloned normally). docker volume ls if unsure.
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.dbNote this covers the database only. Uploaded and extracted files live under
/data/files and need copying separately.
Once, by hand, into whatever you use for secrets — a password manager, a vault, an envelope in a drawer:
grep '^SECRET_ENCRYPTION_KEY=' .envIt never changes unless you rotate it, so this is a one-time job. Do it now rather than later.
# 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 --buildThen check: sign in, open a workflow that uses a stored credential, and run it. If the credential decrypts, you restored the right key.
Same as a restore, plus:
- Point DNS at the new machine and let it propagate.
- Re-run
./scripts/deploy.sh <domain> <email>there — it rewrites the domain settings and gets a fresh certificate. (Do not copycaddy-dataacross; let it re-issue.) - 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 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.
-
SECRET_ENCRYPTION_KEYstored off this server, today -
writ-datain 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
usewrit/writ · AGPL-3.0-only · Issues · Discussions · Report a vulnerability
Getting started
Using it
Integrations
Operations
Reference