Version: 1.2.0This documentation is for Spitfire 1.2.0.
High availability and disaster recovery
This page covers what Spitfire keeps running today and what it does not, the scheduled encrypted backup, the restore drill, the downtime of an update and what happens while the controller is down. The backup commands themselves are in Installation → Backups.
What it is for
So you can write Spitfire into your organisation's continuity plan (RPO/RTO) correctly: which part is a single copy, which one is replaced by a new one, how much data a restore loses and how long it takes.
When to use it
- For the architecture and continuity sign-off before installing.
- When you set up the scheduled backup.
- When you run a restore drill, at least once a year.
- When you plan a maintenance window for an update.
Current state
| Part | State |
|---|---|
| Controller (UI, API, scheduler, synthetic monitoring, runner connections) | A single copy. On Kubernetes replicas: 1 with strategy: Recreate: the live state of runs is in the controller's memory, two copies never run at once. If the pod or the server goes down, Kubernetes/Docker restarts it; meanwhile the UI and the API are down. |
| Runners | Stateless and replaceable. Results stream to the controller, nothing stays on the runner. If a runner goes down, start a new one; it connects with the same token. For a runner lost during a run, the test's When a runner is lost option applies (Test editor). |
| Postgres and ClickHouse that come with the installation | A single copy, on the installation's own disk. |
| Your own Postgres or ClickHouse | High availability (replicas, failover, backups) belongs to your database team. Spitfire uses one connection address; it must stay the same after a failover. |
| Backup | install.sh --backup takes an encrypted full backup in one file (Postgres + the installation's keys). You set up the schedule (below); the product has no backup scheduler of its own. |
| APM store (ClickHouse: traces, logs) | Not in the backup. This data is limited by its retention (1–30 days) anyway; after a restore it fills with new data. |
An active/passive second controller is planned; it does not exist today.
While the controller is down
| What | What happens |
|---|---|
| Running runs | A runner that cannot reach the controller for 15 s stops its load by itself ("controller unreachable for more than 15s"): the target never gets unsupervised load. When the controller is back, these runs are Interrupted, reason "controller restarted". A controller that is stopped cleanly (update, docker compose stop) first stops the active runs and saves their results (up to 30 s). |
| Idle runners | They retry at growing intervals (at most 15 s) and connect by themselves once the controller is up. |
| Scheduled runs | They do not start. If the controller was down for less than 10 minutes, the missed run starts at startup; if longer, that run is recorded as missed, is not caught up, and its notification goes out. |
| Synthetic monitoring | Pauses; continues at the next interval after startup. The checks in between are not made later. |
| UI, API, CI pipelines | Down; CI steps that call the API fail. |
| OTLP receiver (APM) | Your services' OpenTelemetry SDKs retry for a short while; data beyond their buffers is lost. |
Step by step
1. Scheduled encrypted backup (Docker)
Create the backup folder:
mkdir -p -m 700 ~/spitfire-backupsWrite the backup passphrase (at least 12 characters) to a file only you can read:
install -m 600 /dev/null ~/.spitfire-backup-pass && printf '%s' 'YOUR-PASSPHRASE' > ~/.spitfire-backup-passTake one backup by hand and see that it works:
SPITFIRE_BACKUP_PASSPHRASE="$(cat ~/.spitfire-backup-pass)" ~/spitfire/install.sh docker --backup ~/spitfire-backups/spitfire-$(date +%F).sfbakFor a backup every night at 03:00 and deleting backups older than 14 days, open
crontab -eand add these two lines:0 3 * * * SPITFIRE_BACKUP_PASSPHRASE="$(cat $HOME/.spitfire-backup-pass)" $HOME/spitfire/install.sh docker --backup $HOME/spitfire-backups/spitfire-$(date +\%F).sfbak >> $HOME/spitfire-backups/backup.log 2>&1 30 3 * * * find $HOME/spitfire-backups -name '*.sfbak' -mtime +14 -deleteCopy the
~/spitfire-backupsfolder to another server with your own backup system. Keep the passphrase apart from the backups (in a password vault): a backup does not open without it.
Success check: the next morning today's file is there and the log has no error:
ls -lh ~/spitfire-backups/ && tail -n 5 ~/spitfire-backups/backup.logThe RPO (the most data you can lose) is your backup interval: with a nightly backup, at most the last 24 hours.
2. Scheduled backup (Kubernetes and Helm)
With the Postgres that comes with the installation (the default), the same command runs from an admin machine with kubectl access. The installation package unpacked under ~/spitfire is enough (usespitfire.com/install).
Take a backup by hand:
SPITFIRE_BACKUP_PASSPHRASE="$(cat ~/.spitfire-backup-pass)" ~/spitfire/install.sh kubernetes --namespace spitfire --backup ~/spitfire-backups/spitfire-$(date +%F).sfbakSet up steps 4 and 5 of the Docker part with
kubernetes --namespace spitfirein place ofdocker.
If you use your own database (externalDatabase.existingSecret), your own backup system backs up the database; take only the keys from Spitfire:
kubectl -n spitfire get secret spitfire-secrets -o yaml > ~/spitfire-backups/spitfire-secrets.yamlSuccess check: ls -lh ~/spitfire-backups/ shows today's file.
3. Restore drill
Run the drill on a separate trial machine, not on the running installation. The time it takes is your RTO; write that into your continuity plan.
Note the start time:
dateInstall Spitfire on the trial machine (the same release or a newer one):
curl -fsSL https://spitfire.sh | bash -s -- dockerCopy the newest
.sfbakfile to this machine (e.g. withscp) and restore it:SPITFIRE_BACKUP_PASSPHRASE="$(cat ~/.spitfire-backup-pass)" ~/spitfire/install.sh docker --restore ~/spitfire-backups/spitfire-2026-11-04.sfbakOn Kubernetes:
~/spitfire/install.sh kubernetes --namespace spitfire --restore ….See that the controller is ready:
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8470/readyzSign in to the UI with a user from the backup; in Runs see the runs up to the backup's day, in Connections see that a connection's Test button works (the secrets can be read).
Note the end time (
date). The difference between the two is your RTO.
Success check: step 4 prints 204, and the sign-in in step 5 shows the backup's data.
In a real disaster use the new server in place of the trial machine and point the name (DNS) the runners connect to at the new server. The controller's TLS identity comes back with the backup's key, so the runners' CA pin does not change; the runners connect by themselves.
4. Updating and rolling back
On Docker and install.sh kubernetes installations, an update that changes the version first dumps the database (~/spitfire/backups/*.dump, the last 5 are kept).
Update:
curl -fsSL https://spitfire.sh | bash -s -- dockerIf something goes wrong, undo the last update (the database returns to its state in that dump and the version to the previous one; changes made after it are lost):
~/spitfire/install.sh docker --rollbackKubernetes:
~/spitfire/install.sh kubernetes --rollback.
A Helm installation has no automatic dump. Take the backup from part 2 before upgrading, then upgrade:
helm repo update spitfire && helm upgrade spitfire spitfire/spitfire -n spitfire --reuse-valuesTo roll back, first return to the old release, then restore the backup you took before the upgrade (the new release may have changed the database schema):
helm rollback spitfire -n spitfireSPITFIRE_BACKUP_PASSPHRASE="$(cat ~/.spitfire-backup-pass)" ~/spitfire/install.sh kubernetes --namespace spitfire --restore ~/spitfire-backups/spitfire-2026-11-04.sfbakDowntime: on an update the controller stops once and starts with the new release (a single copy, Recreate). The downtime is the time to stop the active runs (up to 30 s), to start and to migrate the schema, and is in the order of minutes. The dump is taken while the controller runs; with a large database it makes the whole update longer. To measure the downtime on your own installation, start this in a second terminal before updating; lines other than 204 are downtime:
while :; do printf '%s %s\n' "$(date +%T)" "$(curl -s -o /dev/null -m 2 -w '%{http_code}' http://localhost:8470/readyz)"; sleep 1; doneSuccess check: after the update the lines print 204 again and the Version and updates card on the License page shows the new release.
Update while no run is going and between scheduled runs: runs going during the update are stopped.
Capacity measurement
The largest verified test (most VUs, sustained requests/s, duration, runner count, CPU/memory per runner, result loss and error rate) will be added once it is measured.
| Measure | Value |
|---|---|
| Most real VUs | will be added once measured |
| Sustained requests/s | will be added once measured |
| Duration | will be added once measured |
| Runner count, CPU/memory per runner | will be added once measured |
| Result loss (requests sent vs stored) | will be added once measured |
| Error rate | will be added once measured |
The measurement is made with Spitfire's own benchmark tool against a target that does almost no work per request: the requests the target counted are compared with those the controller stored, and the runners' CPU and memory are sampled during the run. For planning on your own hardware, use the method in Runners → Capacity.
Common problems
| Symptom | Cause | Fix |
|---|---|---|
No nightly backup, the log says docker: command not found |
cron's PATH is short |
Add the line PATH=/usr/local/bin:/usr/bin:/bin at the top of the crontab. |
| The log says "A passphrase is needed." | SPITFIRE_BACKUP_PASSPHRASE came empty |
Check that ~/.spitfire-backup-pass exists and is not empty. |
| The restore says "The backup is from a newer release" | The Spitfire on the trial machine is older than the one that took the backup | Update the trial machine first, then restore. |
| After a restore, a connection's Test button gives a password/secret error | A plain dump was restored instead of the backup, the keys are old | Restore with the .sfbak, or set SPITFIRE_SECRET_KEY to the value in your backup. |
| After the controller comes back, a run is Interrupted, reason "controller restarted" | The controller stopped during the run | Expected; start the run again. |