Spitfire
InstallDocker · Kubernetes · Windows · runner

Installation guide: set up the Spitfire load testing platform

Installing is one command. It downloads the release's installer bundle from spitfire.tr, checks its SHA-256 and unpacks it into ~/spitfire; the image comes from Docker Hub. The same command also updates, keeping your settings.

Detailed documentation →

Requirements

  • Linux or macOS with Docker and Docker Compose v2.
  • Windows 10/11 or Windows Server: Docker Desktop (Linux containers) and PowerShell. On a Windows server without Docker a runner installs as a Windows service.
  • For Kubernetes also kubectl and access to a cluster.
  • Access to Docker Hub and spitfire.tr. No Go, Node or source code needed.

Single machine (Docker)

Starts Postgres, the controller, the web UI and 2 runners.

curl -fsSL https://spitfire.tr/install.sh | bash -s -- docker

Options: --port 8470, --runners 4, --location istanbul; see ~/spitfire/install.sh --help.

Kubernetes

Installs into the current kubectl context, namespace spitfire; the nodes pull the image from Docker Hub.

curl -fsSL https://spitfire.tr/install.sh | bash -s -- kubernetes
curl -fsSL https://spitfire.tr/install.sh | bash -s -- kubernetes --context prod --namespace loadtest --runners 3

Pods run as non-root with a read-only root filesystem, no capabilities and the RuntimeDefault seccomp profile. NetworkPolicies let only the controller reach Postgres and only this installation's runners reach the plaintext runner port (8475), where the CNI enforces them 0.5.5+.

On-demand runners: with --runners 0 --on-demand-runners 20 no load generator stays up. When a run asks for more runners than are idle (Run dialog → By count; it suggests a count from the test's peak VUs), the rest start as pods of the same image, the run starts once they connect (usually 20–60 s) and the pods are deleted when it ends. At most N pods at once; the controller gets pod rights in its own namespace only, and only while the option is on 0.5.16+.

Windows (Docker Desktop)

One PowerShell command: it verifies the installer bundle, unpacks it into %USERPROFILE%\spitfire and starts Postgres, the controller and 2 runners on Docker Desktop. No administrator rights needed; the script is allowed to run for that process only.

irm https://spitfire.tr/install.ps1 | iex

With options (port, number of runners, location):

& ([scriptblock]::Create((irm https://spitfire.tr/install.ps1))) docker -Port 8470 -Runners 4 -Location istanbul

A runner on a Windows server without Docker: run the command from Runners → Registration key → Windows in an Administrator PowerShell. The runner becomes the service spitfire-runner-1, started with Windows and restarted if it crashes; its settings live under %ProgramData%\Spitfire, readable only by SYSTEM and Administrators.

Setup code: Select-String SPITFIRE_SETUP_CODE $env:USERPROFILE\spitfire\deploy\docker\.env. Removal: powershell -ExecutionPolicy Bypass -File $env:USERPROFILE\spitfire\uninstall.ps1 docker. Windows install ships with release 0.5.3.

First sign-in and the setup code

The installer prints the address and a one-time setup code. Open the address and create the first admin with the code.

Lost the code? No new one is needed; it is kept until the first admin exists:

grep SPITFIRE_SETUP_CODE ~/spitfire/deploy/docker/.env
kubectl -n spitfire get secret spitfire-secrets -o jsonpath='{.data.SPITFIRE_SETUP_CODE}' | base64 -d

The session key is kept in a cookie scripts cannot read (HttpOnly); the session survives a reload, and when it ends the UI returns to the sign-in screen 0.5.6+.

Sign-in attempt limits 0.5.4+

Against password guessing, sign-in, the setup code and the SSO return are limited: one client address may try 30 times in a row, then once every 2 s; an account accepts 10 wrong passwords, then one a minute (from any address; a correct password does not count). Past a limit the screen says how long to wait, and the API answers 429 with Retry-After.

If many users come through one proxy or NAT, raise the address limit: SPITFIRE_SIGNIN_BURST=200 in deploy/docker/.env (or spitfire-secrets). The client address comes from X-Forwarded-For only when the request arrives from loopback or a private network (an ingress or reverse proxy in front); a proxy on a public address (a cloud load balancer, Cloudflare) goes in SPITFIRE_TRUSTED_PROXIES 0.5.5+.

For an API that wants a client certificate (mTLS) or uses a private CA, add Connections → Client certificate (mTLS); HTTP, WebSocket and SSE steps pick it under Client certificate. 0.5.13+

For a ready test file, Tests → Open file: Spitfire JSON (examples: usespitfire.com/examples), a k6 script (.js) or a JMeter plan (.jmx). k6 and JMeter files are read and converted, and what cannot be converted is reported with its line (migration guide) 0.5.14+.

A whole suite at once: select several files in Open file; the bulk import page converts, validates and lists each with its report, and saves the ones you pick. A whole folder on the command line: spitfire convert ./jmeter-tests -o tests/ -r report.csv 0.5.16+.

Environments: on the editor's Environments tab each environment (staging, production…) gives some variables their values and maps the steps' connections to others of the same type. The Environment and scale section of the Run and Schedule dialogs picks the environment, scales the load by a percentage (VUs, rates and stage targets) and changes variables for that run only; the run page shows what was used 0.5.14+.

The UI opens in the dark theme; the sun/moon button in the top bar switches to the light theme, and the choice is remembered in that browser 0.5.15+.

Runners in other locations

Remote runners dial out to the controller over TLS with key pinning; no inbound port on the load server. In the UI, Runners → Registration key prepares the command with the token and pin:

curl -fsSL https://spitfire.tr/install.sh | bash -s -- runner --controller spitfire.example.com:8471 --token sfrun_… --ca-pin sha256:… --runners 2

For servers without Docker the same dialog also has a single-binary install with systemd.

Runners installed without Docker (systemd or the Windows service) update themselves from then on: press Update next to an older runner on the Runners page. The runner downloads the new release from the controller over the gRPC port, checks Spitfire's signature itself (it accepts nothing unsigned or older) and restarts; a runner in a run is not updated. Runners in Docker and Kubernetes update with their image, through the install command. Runners installed earlier get this after running their install command once more 0.5.9+. With Update runners automatically on (Runners page) the controller does it itself: idle runners, one at a time per location 0.5.11+.

CI/CD 0.5.3+

A saved test runs from the pipeline on the controller, on your runners; the step waits and passes or fails on the thresholds. Credential: a personal token from profile menu → API tokens, stored as the pipeline secret SPITFIRE_TOKEN. The token can only read, start and stop runs. The CI button on a test's page prepares the steps below with that test's name.

GitHub Actions

# .github/workflows/load-test.yml içinde bir adım / a step
- name: Load test
  env:
    SPITFIRE_URL: https://spitfire.example.com
    SPITFIRE_TOKEN: ${{ secrets.SPITFIRE_TOKEN }}
  run: |
    curl -fsSL "$SPITFIRE_URL/api/v1/runner-dist/spitfire-cli-linux-amd64.gz" | gunzip > spitfire && chmod +x spitfire
    ./spitfire cloud run "Checkout" --report report.pdf

GitLab CI

load-test:
  image: algebransoft/spitfire:latest
  variables:
    SPITFIRE_URL: https://spitfire.example.com   # SPITFIRE_TOKEN: CI/CD → Variables (masked)
  script:
    - spitfire cloud run "Checkout" -l istanbul=60 -l frankfurt=40 -o summary.json

Windows (PowerShell)

$env:SPITFIRE_URL = 'https://spitfire.example.com'   # $env:SPITFIRE_TOKEN: pipeline secret
Invoke-WebRequest "$env:SPITFIRE_URL/api/v1/runner-dist/spitfire-cli-windows-amd64.exe" -OutFile spitfire.exe -UseBasicParsing
.\spitfire.exe cloud run 'Checkout'
exit $LASTEXITCODE

Exit codes: 0 passed, 99 thresholds failed (a threshold that measured nothing counts as failed), 97 a threshold aborted the run, 2 the test changes data and --confirm-writes was not given, 1 other errors. Options: --runners N, -l istanbul=60:2 (location share), --label zone=a, --timeout 20m, -o summary.json, --report report.pdf, --report-lang en.

Environment, scale and one-off variables: spitfire cloud run "Checkout flow" --env staging --scale 0.5 --var user=ci. The same flags work for a local spitfire run test.json 0.5.14+.

Which test to run, where in the pipeline and with which thresholds: the performance testing in CI/CD guide.

Version comparison 0.9.0+

A comparative run applies one test to two of its environments at the same time, with the same load, and compares them step by step. Arm A is the reference (usually the release in production), arm B the candidate; the scenario, thresholds and load model are the same and only the environment (address, variables, connection mapping) differs. Every runner splits its VUs evenly between the arms and load stages change on both at once; if an arm drops, both stop and the comparison is invalid. When and why: version comparison.

Two environments

Define two environments on the editor's Environments tab (say test and dev); each gives its own base address and variables. Its Equivalence (comparative runs) section takes the environment's CPU, memory, instances (pods), database version, data volume, cache state and the other environments it shares a database, cache or server with. None of it is measured; it is shown side by side before the run, with differing values highlighted. The Version endpoint (optional) is an address and where the version is: a JSONPath ($.version), a header (X-App-Version) or the plain body. An empty version label is read from it; when both arms report the same version the run can be recorded as A/A.

"environments": {
  "test": {
    "variables": { "base": "https://test.shop.example.com" },
    "declared": { "cpu": "4 vCPU", "memory": "8 GiB", "instances": 3, "dbVersion": "PostgreSQL 16.4", "cache": "warm" },
    "version": { "url": "{{base}}/version", "from": "jsonpath", "expr": "$.version" }
  },
  "dev": {
    "variables": { "base": "https://dev.shop.example.com" },
    "declared": { "cpu": "4 vCPU", "memory": "8 GiB", "instances": 3, "dbVersion": "PostgreSQL 16.4", "cache": "warm" },
    "version": { "url": "{{base}}/version", "from": "jsonpath", "expr": "$.version" }
  }
}

A ready-made test with two environments: surum-karsilastirma.json on the examples page.

Starting it and the warm-up

On the test's page, Run ▾ → Comparative run: each arm's environment and version label, the load per arm (a share of the test's load, 50% by default, so both arms together load the runners as the test does alone), the warm-up and the acceptable difference (p95 ±10% and error rate ±0.5 points by default). The warm-up (60 s by default) is left out of the comparison, so caches, connection pools and JIT warming do not skew it. For a test whose load ramps up slowly, keep the warm-up at least as long as the ramp. Steps that change data are confirmed for each environment separately.

Before the arms start, the Equivalence check sends 20 light requests (GET or HEAD only) to each environment from the chosen runners: reachability, response and TCP connect times, TLS and HTTP version side by side, and a warning when both environments share a database, cache or server. Nothing is blocked, but starting with warnings needs an explicit confirmation.

With a single environment, or two that share resources, pick Mode → Sequential (taking turns) in the dialog: the arms take turns on the same runners (A, B, A, B) and every turn leaves out its own warm-up 0.15.0+.

A/A calibration 0.13.0+

Two environments are never quite equal. While both run the same release, Run ▾ → Calibrate makes a short comparative run (10 min by default, 25% of the test's peak per arm, the first minute as warm-up) and records the pair's natural difference step by step. Later comparisons take it out: the table shows the raw, calibration and corrected differences side by side, and the verdict reads the corrected one. For a calibration older than 30 days the verdict card suggests recalibrating.

From CI 0.14.0+

Give spitfire cloud run the --compare flag: both arms run, the per-step differences are printed and the command exits by the verdict. --scale is then each arm's share (0.5 by default); --env and --var do not go with --compare.

spitfire cloud run "Checkout flow" --compare A=test,B=dev --label-a v2.3 --label-b "$GIT_SHA" --report comparison.pdf
  • 0: better or no difference (inconclusive too; 98 with --fail-on-inconclusive).
  • 98: worse.
  • 97: invalid, an arm failed or a runner was lost; with --strict a failed equivalence check (an environment did not answer) keeps the comparison from starting.
  • 99: the candidate arm (B) failed its own thresholds; arm A's are printed, not counted.
  • 2: writes not confirmed (--confirm-writes confirms both environments), 1: other errors.

--report comparison.pdf saves the comparative report, -o comparison.json the result as JSON; --pr-comment writes the comparison's PR/MR comment. Ready GitHub Actions and GitLab CI steps: version comparison.

License

Version comparison is 1 comparative run a month in the free edition (plus 2 A/A calibrations a month, which do not use it up); every paid plan has it. Comparisons started from CI or a schedule come with Growth yearly, Scale yearly and Enterprise yearly; those started in the web UI are not affected. The virtual user, requests/s and runner limits apply to both arms together: each arm uses half. Comparative runs need current runners.

Integrations 0.5.3+

In the UI, Integrations (admin): announce run start and end with a webhook, and hand live metrics to your monitoring over OpenTelemetry or Prometheus. Set Spitfire's public address on the same page first; run links in the notifications use it.

Webhook

On the selected events (run.started, run.finished, schedule.failed) JSON is POSTed to the URL. Headers: X-Spitfire-Event, X-Spitfire-Delivery, X-Spitfire-Schema; with a secret, X-Spitfire-Signature: sha256=<hex> (the HMAC-SHA256 of the body). Extra headers (e.g. the receiver's token) can be added. 2xx is success; network errors, 408, 429 and 5xx are retried after 2 s, 15 s and 1 min, other 4xx are not. Every attempt shows under Deliveries. targets are the hosts the test loads; summary comes with run.finished only. Within a schema version fields are only ever added.

{
  "schema": "spitfire.run-event/v1",
  "event": "run.finished",
  "deliveryId": "6f1c…",
  "sentAt": "2026-09-29T12:00:58Z",
  "controller": { "url": "https://spitfire.example.com", "version": "0.32.0" },
  "run": {
    "id": "3b9e…", "url": "https://spitfire.example.com/runs/3b9e…",
    "testId": "a1f0…", "testName": "Checkout", "testVersion": 7,
    "status": "finished", "result": "thresholds_failed",
    "startedAt": "2026-09-29T11:55:00Z", "endedAt": "2026-09-29T12:00:57Z",
    "plannedSeconds": 360, "plannedEndAt": "2026-09-29T12:01:00Z",
    "maxVUs": 200, "runners": 2, "locations": ["istanbul=60%", "frankfurt=40%"],
    "requestedBy": "Ayşe", "note": "GitHub Actions · shop@1a2b3c4d", "tags": ["ci"],
    "modifiesData": false
  },
  "targets": [
    { "protocol": "http", "host": "shop.example.com", "url": "https://shop.example.com", "steps": 3 },
    { "protocol": "postgres", "host": "db.internal", "port": "5432", "connection": "orders", "steps": 1 }
  ],
  "summary": {
    "requests": 182340, "failed": 12, "rps": 506.5, "errorRate": 0.00007,
    "p50Ms": 38, "p95Ms": 612, "p99Ms": 980, "peakVUs": 200, "checksPassRate": 0.999,
    "thresholds": [{ "metric": "req_duration", "expr": "p(95)<500", "observed": 612, "passed": false }]
  }
}

If the controller restarts while a run is live, the run closes as interrupted and its run.finished is sent at startup (result: error, abortReason: controller restarted), so the receiver does not keep it open (0.5.4+).

Verifying the signature

import hashlib, hmac

def verified(body: bytes, header: str, secret: str) -> bool:
    # header: the X-Spitfire-Signature value, "sha256=<hex>"
    want = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, header or "")
import { createHmac, timingSafeEqual } from 'node:crypto'

export function verified(body, header, secret) {   // body: the raw request bytes
  const want = Buffer.from('sha256=' + createHmac('sha256', secret).update(body).digest('hex'))
  const got = Buffer.from(header ?? '')
  return got.length === want.length && timingSafeEqual(got, want)
}

Prometheus

/metrics: runners (spitfire_runners), active runs and each live run's spitfire_run_vus, spitfire_run_requests_per_second, spitfire_run_error_ratio, spitfire_run_latency_ms{quantile}, spitfire_run_requests_total, spitfire_step_requests_total. Credential: a personal API token.

The controller's own health is on the same endpoint 0.5.5+: spitfire_internal_runner_messages_dropped_total, spitfire_internal_db_writes_dropped_total / _failed_total (above zero, the results have gaps), spitfire_internal_deliveries_total{result}, spitfire_internal_deliveries_pending (webhooks waiting), spitfire_internal_http_responses_total{code}, plus Go runtime values. For log collectors: SPITFIRE_LOG_FORMAT=json (runners: SPITFIRE_RUNNER_LOG_FORMAT).

scrape_configs:
  - job_name: spitfire
    scheme: https
    metrics_path: /metrics
    authorization:
      credentials: sfpat_…        # a personal API token
    static_configs:
      - targets: ['spitfire.example.com']

OpenTelemetry (OTLP)

While a run is live its metrics are pushed over OTLP/HTTP (JSON) to a collector: the endpoint is the base URL, /v1/metrics is appended (e.g. https://otel-collector:4318); credential headers (Authorization, Api-Token…) are stored encrypted. Metrics: spitfire.vus, spitfire.request.rate, spitfire.request.error_rate, spitfire.request.duration.p50/p95/p99, spitfire.requests, spitfire.step.requests; attributes spitfire.run.id, spitfire.test.name and server.address for the target host.

Reports 0.5.3+

On a run's page, Report: HTML (new tab), PDF, CSV (step and location summary, or per second). A share link shows the report to someone without an account for 1–90 days; it can be revoked, and every view is written to the audit log. To fetch a report from a script:

curl -fsS -H "Authorization: Bearer $SPITFIRE_TOKEN" "https://spitfire.example.com/api/v1/runs/<run-id>/report?format=pdf&lang=tr" -o rapor.pdf
curl -fsS -H "Authorization: Bearer $SPITFIRE_TOKEN" "https://spitfire.example.com/api/v1/runs/<run-id>/export?kind=timeseries" -o saniyeler.csv

Failed request samples: the run page shows a few failed requests per step and outcome (target, error, failed check, a few response headers, the first 2 KB of the body; tokens in the address masked, no cookies). For a test whose responses must not be stored, errorSamples: off in its options 0.5.13+. Version history (test page): each save's diff with the current one, and restoring an old version as a new one 0.5.13+.

Findings: a finished run's page lists what its own numbers show, with those numbers: a clearly slowest step, kinds of errors (5xx, 429, other 4xx, timeouts, refused connections, DNS/TLS), throughput that stopped following the load, iterations that could not start, long connection setup, a gap between locations, failed checks, a regression against the baseline, a long latency tail. A rule fires only when the effect is clear and never claims a cause inside the system 0.5.14+.

Breaking point: Find the breaking point in the Run dialog climbs one scenario's load in steps (start, step, up to; each step a 10 s ramp and a hold) and judges every hold on that step's own p95 and error rate. The first step that fails stops the run; the run page shows the capacity, the step table and why. The test's thresholds do not apply to this run, and the test does not change 0.5.14+.

Scheduled runs and notifications 0.5.3+

On a test's page, Schedule: hourly, nightly, weekdays, weekly or any 5-field cron expression, in the time zone you pick. A schedule runs as the user who created it, with that user's current rights. At most every 5 minutes; a firing is skipped while the previous run still runs; one more than 10 minutes late (controller was down) is not caught up but marked missed. All of them are under Test → Schedules; "Run now" tries one immediately.

Notification channels

Integrations → Notification channels → Add channel, pick the kind:

  • Slack: create an Incoming Webhook in Slack and paste its URL. A coloured message: result, requests, p95, error rate, failed thresholds, a link to the run.
  • Microsoft Teams: in the channel create the Workflows → "Post to a channel when a webhook request is received" flow and paste its URL. The same facts in an adaptive card with an "Open the run" button.
  • E-mail: enter your server in the E-mail (SMTP) card on the same page (STARTTLS, TLS, or plain on a trusted network; the password is stored encrypted) and send a test mail. The finished run's PDF report is attached.

Each channel picks its events (run.started, run.finished, run.regressed, schedule.failed); with Only notify on problems passing runs and starts are not sent, while runs that break a threshold, fail or are stopped, and scheduled runs that did not start, are. A channel can be limited to certain tests. The message language (Turkish/English) is under Integrations → General. schedule.failed also reaches JSON webhooks, with run.testId and a schedule field.

Regressions against the baseline: when the test has a baseline run, every finished run is compared with it (p95, p99, error rate, requests/s; with the test's tolerances, or 5% warn / 15% fail). The message lists what got worse; a clear regression also sends run.regressed (off by default on new channels; a problem for channels that notify only on problems) 0.5.13+.

Mobile app and gate 0.5.17+

The Spitfire mobile app (Android) follows runs live and stops one with a tap; it starts a saved test in an environment and at a scale, shows results, findings and the change against the baseline, and shares a report link. Editing tests, imports and admin screens stay in the web UI. Add the controller's address in the app; sign in with a password, LDAP or OIDC (OIDC opens in the browser and returns to the app with a code bound to it by PKCE).

The app, screenshots and privacy policy: mobile app.

Notifications

An admin turns notifications on under Mobile & Gate. Users are told only about tests they can see and choose the events in the app: a failed run or a broken threshold, a regression against the baseline, a scheduled run that did not start, every finished run, a run starting. On the controller every notification is encrypted with a key only that phone can open (X25519 + AES-256-GCM); nobody on the way, Spitfire's notification server and Google included, can read it. A notification holds the test name, the result and the status; no measurements, target addresses or data.

  • The controller has internet access: pick "Straight from the controller"; it sends notifications to relay.spitfire.tr itself (it follows the proxy setting).
  • Otherwise a gate: a small service on a machine that can reach the internet. It connects to the controller with a token (like a runner), takes the notifications and hands them to the notification server; it only connects outwards and opens no way into your network. Create gate token on that page gives the Linux (systemd) and Docker commands:
curl -fsSLo install-gate.sh https://spitfire.example.com/api/v1/runner-dist/install-gate.sh
echo "<sha256>  install-gate.sh" | sha256sum -c -
sudo bash install-gate.sh --from https://spitfire.example.com --token sfgate_...

The gate needs outbound access to the controller's web port and to relay.spitfire.tr:443. Revoking its token stops it. While there is no gate or no internet, notifications wait up to 24 hours.

Single sign-on (OIDC, SAML, LDAP) 0.5.3+

In the UI, Single sign-on (admin). Set Spitfire's public address on the Integrations page first; the redirect URI is built from it.

OpenID Connect

Create a web application (authorization code) at the identity provider and add the redirect URI:

https://spitfire.example.com/api/v1/auth/oidc/callback
  • Entra ID: issuer https://login.microsoftonline.com/<tenant-id>/v2.0; for groups, turn on the "groups" claim in the app's token configuration (group object ids arrive).
  • Okta: issuer https://<domain>.okta.com (or an authorization server); define a "groups" claim for groups.
  • Keycloak: issuer https://<server>/realms/<realm>; add a "Group Membership" mapper to the client (claim name groups, full path off).
  • Google: issuer https://accounts.google.com; Google sends no groups, so limit by allowed domain.

Spitfire uses PKCE and a nonce and verifies the ID token's signature with the provider's keys; unverified e-mail addresses are refused. "Test discovery" finds the provider without saving.

SAML 2.0 0.22.0+

Create a SAML application at the identity provider (ADFS, Entra ID, Okta, Keycloak…) and give it Spitfire's service-provider metadata; the entity ID is the metadata URL itself, and the ACS URL is read from it:

https://spitfire.example.com/api/v1/auth/saml/metadata

Give Spitfire the provider's metadata URL or XML and pick the e-mail, name and group attribute names. Requests are signed; the response's signature, audience, validity and request id are checked; encrypted assertions are supported. Only sign-ins started from Spitfire are accepted (IdP-initiated responses are refused). "Test the metadata" reads the provider without saving.

LDAP / Active Directory

A server (ldaps://dc1.corp.local:636, or ldap:// with StartTLS), a service account that can read, and a user search base are enough. The default filter matches uid, sAMAccountName and mail; users type their directory user name or e-mail in the normal sign-in form. The password only goes to the directory and is not stored in Spitfire. Groups come from memberOf; for directories without it give a group search base. "Try" binds as a user and shows what Spitfire would do (access, role) without saving.

Accounts, roles, groups

Accounts can open on first sign-in (the license's user limit applies), or an admin creates the account on the Users page with sign-in method OIDC/SAML/LDAP. Admin groups decide the role at every sign-in; membership sync adds users to the Spitfire groups named like their provider groups and removes them when that stops (memberships added by hand are left alone). A password account with the same e-mail is never bound to an SSO identity on its own; an admin changes the account's sign-in method. With Password sign-in: admins only everyone else signs in with SSO, and an admin password stays as break-glass access.

Forgotten passwords 0.5.18+

Forgot password on the sign-in page and in the mobile app answers the same for every address; it never tells whether an account exists. What happens next depends on whether the installation sends e-mail:

  • With e-mail set up (Integrations → E-mail and the public address): the user gets a one-time link valid for 30 minutes. Setting the password with it ends every session of the account.
  • Without e-mail: the request is flagged on the Users page. The admin sets a temporary password; at the next sign-in the user picks their own before anything else.

Single sign-on (OIDC, SAML, LDAP) accounts have no Spitfire password; the e-mail tells them so. Users change their own password from Change password in the user menu, or in the mobile app.

When nobody can sign in

If the only admin forgot their password and there is no e-mail, on the controller's host:

cd ~/spitfire && docker compose -p spitfire -f deploy/docker/docker-compose.yml exec controller spitfire-controller reset-password admin@example.com

# Kubernetes
kubectl -n spitfire exec -it deploy/spitfire-controller -- spitfire-controller reset-password admin@example.com

It asks for the new password twice. --generate prints a random one (changed at the next sign-in); --enable also re-enables a disabled account. The audit log records it.

Updating

Run the install command again (irm … | iex on Windows). The new release unpacks into the same folder; keys (deploy/*/.env or spitfire-secrets) and data are kept.

curl -fsSL https://spitfire.tr/install.sh | bash -s -- docker

When the version changes, the installer first dumps the database to ~/spitfire/backups/*.dump (newest 5; restore with pg_restore). Without a dump there is no update; --no-backup (Windows: -NoBackup) skips it 0.5.5+.

When a new release is out, the controller learns it from spitfire.tr once a day; admins see its notes and the command for this installation (folder or namespace included) at the top of the UI. Only the version number is sent; without internet access set SPITFIRE_UPDATE_CHECK=off. Every release: release notes 0.5.7+.

If an update goes wrong, ~/spitfire/install.sh docker --rollback (Kubernetes: kubernetes --rollback; Windows: -Rollback) first dumps the current database, then restores the newest dump and starts the release that took it. Changes made after that dump are lost; running it again undoes the rollback 0.5.10+.

The installed version, the last check's result and the command for this installation are always on the Version and updates card of the License page; Check now does not wait for the daily check. The release list is signed: a list whose signature does not hold is not shown 0.5.8+.

Behind a corporate proxy, run the install command with the proxy set; HTTPS_PROXY, HTTP_PROXY and NO_PROXY are passed on to the controller (the update check, webhooks, SSO). Add SSO or webhook addresses on the internal network to NO_PROXY 0.5.8+.

export HTTPS_PROXY=http://proxy.firma.local:3128 NO_PROXY=.firma.local && curl -fsSL https://spitfire.tr/install.sh | bash -s -- docker

Docker pulls images with the Docker service's proxy setting, not the shell's (Docker Desktop: Settings → Resources → Proxies; Linux: HTTPS_PROXY via systemctl edit docker). On Windows set $env:HTTPS_PROXY='http://proxy.firma.local:3128' first.

A specific release or another folder:

curl -fsSL https://spitfire.tr/install.sh | SPITFIRE_VERSION=0.32.0 SPITFIRE_DIR=/opt/spitfire bash -s -- docker

Back up the keys: without SPITFIRE_SECRET_KEY stored connection passwords cannot be read.

Support bundle 0.18.0+

When you ask us about a problem, attach the support bundle. In the UI, as an admin: Infrastructure → System events → Download support bundle: controller and runner logs, version and install details, settings with secrets removed, the license status (not the key itself), the last 30 days of system events and a summary of the latest failed runs in one zip. Its files are listed before the download.

When the UI does not open, on the machine with the installation:

~/spitfire/install.sh docker --support-bundle
~/spitfire/install.sh kubernetes --support-bundle
powershell -ExecutionPolicy Bypass -File .\install.ps1 -SupportBundle

The bundle holds no test definitions, request/response bodies, data files, connection details, user names or e-mails; passwords, tokens, keys and connection strings become [redacted]. Spitfire never sends it anywhere on its own: you download it, look inside if you like, and send it to contact@spitfire.tr yourself. Add the Error code the UI shows on a server error too; it lets us find that request in the log.

Removing

~/spitfire/uninstall.sh docker

With the data: --purge. For Kubernetes and runners use kubernetes / runner.

License

After installing, Spitfire runs for 14 days without a key, within the free edition's limits; then starting new runs needs a key (tests and results are kept). Get a free key with your e-mail address; it does not expire. A bought license's key goes to the same place. Paste the key on the License page in the UI; it is verified offline. Tests and results never leave your installation; the only outbound request is the daily version check under Updating, which you can turn off. Every testing feature and protocol is open in every edition. Team and organisation features (SSO, audit log, multiple runners) are in the paid plans.

Data retention (License page): how many days per-second data and logs, runs (at least 7; baselines are never deleted) and the audit log (at least 30) are kept; what you leave empty stays forever. Before saving, the page shows what the next daily pass would delete 0.5.13+.