Version: 0.56.0This documentation is for Spitfire 0.56.0.
Installation, updates and backups
This page covers installing Spitfire on Docker, Kubernetes or Windows (Docker Desktop), creating the first admin, updating, rolling back, backing up and uninstalling. Installing runners in remote locations is a separate topic: Runners and locations.
What it is for
The installer (install.sh, on Windows install.ps1) does all of this with one command:
- Downloads the release's installer bundle (install.sh, uninstall.sh, Compose/Kubernetes files; no source code) from
spitfire.tr/download, checks its SHA-256 and unpacks it into~/spitfire. - Pulls the image from Docker Hub:
usespitfire/spitfire:<VERSION>(public, amd64 + arm64). Go and Node are not needed. - Starts Postgres, the controller (web UI + API) and the runners.
- Generates the secrets (
SPITFIRE_SECRET_KEY,SPITFIRE_JWT_SECRET, the setup code…) once and keeps them as they are on every later run. - Prints the UI address and the setup code for the first admin at the end.
Running the same command again is an update.
When to use it
- At the first installation.
- When a new release is out (the blue "Spitfire X is out" banner in the UI).
- To change the controller's port, the number of runners or the proxy (run it again with the option).
- When an update went wrong (
--rollback). - When support asks for a support bundle and the UI does not open (
--support-bundle).
Requirements
| Installation | Needs |
|---|---|
| Docker (Linux/macOS) | Docker Engine (Linux) or Docker Desktop (macOS), the Docker Compose v2 plugin, curl. The installing user must reach the Docker daemon (on Linux: be in the docker group). |
| Kubernetes | The above plus kubectl and an active context. Nodes pull the image from Docker Hub (internet access needed; otherwise --build --registry). |
| Windows | Docker Desktop, Windows PowerShell 5.1 or PowerShell 7, tar.exe (ships with Windows 10 1803 and later). |
| Remote runner server | Only Docker; on Linux without Docker a systemd service, on Windows without Docker a Windows service. See Runners. |
When the installer finds something missing it stops with an ERROR: line followed by a Fix: line; read that first. The installer speaks English or Turkish depending on the system locale (LANG); SPITFIRE_LANG=en or SPITFIRE_LANG=tr forces one (for example curl -fsSL https://spitfire.sh | SPITFIRE_LANG=en bash -s -- docker).
Installing with Docker
Check that Docker runs:
docker infomust return without an error.Run the install command:
curl -fsSL https://spitfire.sh | bash -s -- dockerThis installs Postgres + controller + 2 runners on one machine. For another release write
SPITFIRE_VERSION=0.5.3, for another folderSPITFIRE_DIR=/opt/spitfire(both beforebash):curl -fsSL https://spitfire.sh | SPITFIRE_DIR=/opt/spitfire bash -s -- dockerhttps://spitfire.shis the short address ofhttps://spitfire.tr/install.sh(on Windowshttps://spitfire.sh/ps=https://spitfire.tr/install.ps1); both serve the same script, so a proxy or firewall that only allows spitfire.tr can use the long form.The installation ends with the line
Installation complete. Open in your browser: http://.... Copy the Setup code: XXXX-XXXX-XXXX line right below it.Open the address in your browser and continue with First sign-in and setup code.
Default ports: UI and API 8470, gRPC (TLS) for remote runners 8471. To change them:
~/spitfire/install.sh docker --port 8480 --grpc-port 8481 --runners 4Demo target
If you have no system to test yet, or want to get to know the tool first, run the installer with --demo
(Windows: -Demo):
curl -fsSL https://spitfire.sh | bash -s -- docker --demoA small HTTP API is installed next to Spitfire (/health, /api/items, /api/items/{id} and /ocpp/{id} for OCPP).
Only the local runners reach it; no port is published. On Tests → From a template the HTTP and OCPP templates
start on it, so you can create a test and run it at once. To move to your own system, change the address in the template
or in the editor. The setting is kept on later installs (updates); to remove it,
~/spitfire/install.sh docker --no-demo. There is no demo target on Kubernetes.
Installing on Kubernetes
Check that you are on the right cluster with
kubectl config current-context.Run the install command:
curl -fsSL https://spitfire.sh | bash -s -- kubernetesThe default namespace is
spitfire, the context is the current one. Change them with--namespace NSand--context C.By default the UI opens on NodePort 30470 (change with
--node-port N;--service-type LoadBalancerorClusterIPare also possible). If this machine cannot open the UI through the NodePort, the installer starts akubectl port-forwardin the background (deploy/kubernetes/.port-forward.pid).Copy the setup code from the output and open the address.
Security settings of the Kubernetes installation: pods run as non-root with a read-only root filesystem, no capabilities and the RuntimeDefault seccomp profile. NetworkPolicies (where the CNI enforces them) let only the controller reach Postgres and only this installation's runners reach the plaintext runner port (8475). Stopping the controller stops active runs and stores their results first (up to 30 s; the pod gets 45 s).
Common Kubernetes options:
| Option | Meaning |
|---|---|
--namespace NS, --context C |
Target (default spitfire, current-context) |
--service-type T |
NodePort (default), LoadBalancer, ClusterIP |
--node-port N |
UI NodePort (default 30470) |
--storage-class SC, --storage-size S |
Postgres PVC (default: the cluster default, 5Gi) |
--local-port N |
Local port for the port-forward fallback |
--runners N |
Number of always-on runners |
--on-demand-runners N |
Runners started as pods during a run (see Runners) |
--build --registry REG |
Builds the image from source and pushes it to your registry |
An image built with --build is loaded directly into kind, minikube, k3d and Docker Desktop; other clusters need --build --registry REG. --build only works in the source tree; the bundle from spitfire.tr carries no source code.
With Helm
For clusters managed with Helm, Argo CD or Flux there is a chart. It installs the same pieces with the
same security settings and names (spitfire-controller, spitfire-secrets…):
helm repo add spitfire https://spitfire.sh/helm
helm install spitfire spitfire/spitfire -n spitfire --create-namespacehttps://spitfire.sh/helm is the short address of https://spitfire.tr/download/helm; the chart packages themselves come from spitfire.tr, so behind a proxy or firewall allow spitfire.tr (or add the long address).
The install notes print how to open the UI and how to read the setup code. Common values:
| Value | Meaning |
|---|---|
controller.service.type |
ClusterIP (default; with an ingress or port-forward), NodePort, LoadBalancer |
ingress.enabled, ingress.host, ingress.className, ingress.tls |
The UI behind your ingress controller |
controller.grpcPublicAddr |
host:8471 that runners in other locations dial |
runners.replicas, runners.location |
Always-on runners (default 2, local) |
onDemandRunners.max |
Runners started as pods during a run (0: off) |
postgres.enabled, externalDatabase.existingSecret |
Postgres in the cluster, or your own (a Secret with the DSN) |
secrets.existingSecret |
Bring the installation's keys yourself instead of generated ones |
admin.email, admin.password, license.key |
The first admin and a license key (both optional) |
proxy.httpsProxy, proxy.noProxy |
Outbound proxy for the controller |
The installation's keys are generated on the first install, kept on every upgrade and left in place by
helm uninstall, as is the database volume; back up the spitfire-secrets Secret. Update with
helm repo update spitfire && helm upgrade spitfire spitfire/spitfire -n spitfire --reuse-values; the
Version card shows this command on a Helm installation.
Installing on Windows
Start Docker Desktop.
In PowerShell:
irm https://spitfire.sh/ps | iexThis installs in
dockermode; the bundle is unpacked into%USERPROFILE%\spitfire(change it with$env:SPITFIRE_DIR, pick a release with$env:SPITFIRE_VERSION).To pass options, call the script as a scriptblock:
& ([scriptblock]::Create((irm https://spitfire.sh/ps))) docker -Port 8480 -GrpcPort 8481 -Runners 4
The install.ps1 options mean the same as those of install.sh: -Runners N, -Location NAME, -Version V, -Image IMAGE, -AdminEmail/-AdminPassword, -Port N, -GrpcPort N, -GrpcPublicAddr H:P, -NoBackup, -Rollback, -SupportBundle, -Yes, -Help.
To install only a runner on a Windows server without Docker, use the command on the Windows tab of the key dialog on the Runners page: Runners.
Installer options
~/spitfire/install.sh --help lists them all. The most used:
| Option | Meaning |
|---|---|
--port 8480 --grpc-port 8481 |
Docker: UI and runner ports (default 8470/8471) |
--runners N |
Number of local runners (default 2) |
--location NAME |
Location of the local runners (default local; a-z 0-9 - _ .) |
--demo / --no-demo |
Docker: install / remove the demo target |
--grpc-public-addr HOST:PORT |
The address runners in other locations dial (shown in the UI's command; on Docker the LAN IP by default) |
--version V |
Pull another release from Docker Hub |
--image IMAGE |
Use a specific ready-made image |
--offline FILE |
Install or update from an offline bundle, without internet (Updating without internet) |
--admin-email E --admin-password P |
Create the first admin during installation (password at least 8 characters) |
--no-backup |
Do not back up the database when updating |
--rollback [DUMP] |
Roll the last update back |
--support-bundle |
Installs nothing; prepares a support bundle (zip) |
-y, --yes |
Do not ask for confirmation |
First sign-in and setup code
On the first visit the UI shows the Create the first admin screen. It appears only once, while there are no users at all.
The Create the first admin screen: Setup code, Name, Email, Password and Confirm password fields
- Paste the
XXXX-XXXX-XXXXcode printed at the end of the installation into Setup code. - Enter Name, Email and Password (at least 8 characters), and repeat it in Confirm password.
- Click Create admin and sign in.
The new account is the installation admin: installation settings such as the license, users, SSO and system events belong to this role only. The code stops working once the first admin exists.
Lost the code? (It stays valid until the first admin exists and is never regenerated.)
Docker:
grep SPITFIRE_SETUP_CODE ~/spitfire/deploy/docker/.envKubernetes:
kubectl -n spitfire get secret spitfire-secrets -o jsonpath='{.data.SPITFIRE_SETUP_CODE}' | base64 -dThe
setup_code=line in the controller log:docker compose -p spitfire -f deploy/docker/docker-compose.yml logs controller | grep -i setup kubectl -n spitfire logs deploy/spitfire-controller | grep -i setupRunning the install command again shows it again (the code does not change).
To create the first admin during installation without the code: install.sh docker --admin-email admin@example.com --admin-password '...' (Windows: -AdminEmail/-AdminPassword).
Do not share the setup code with anyone but the person installing. Whoever uses it first becomes the installation's admin.
After signing in, the next step is usually the license: License.
Updating
How you notice a new release
Once a day the controller asks spitfire.tr whether there is a newer release (it sends only its own version). If there is, admins see the Spitfire X is out banner at the top of every page:
- What's new and how to update opens the release notes and the update command for this installation (separately for Linux/macOS Docker, Windows PowerShell, Kubernetes and runner servers in other locations).
- Hide for this release closes the banner for that release. A Security update banner (red) cannot be hidden; it shows again on every visit.
The Version and updates card on the License page shows the installed version, the installation type, the last check's result, the Outbound proxy and the Update command; Check now checks without waiting.
The Version and updates card on the License page: installed version, installation type, update check and the Check now button
Updating step by step
Make sure no run is in progress (the update restarts the controller).
On the server the controller is installed on, run the same command as at installation:
curl -fsSL https://spitfire.sh | bash -s -- docker curl -fsSL https://spitfire.sh | bash -s -- kubernetes --context C --namespace NSOn Windows:
irm https://spitfire.sh/ps | iex.The script unpacks the latest bundle into the same folder and pulls the image. Generated keys (
deploy/docker/.envor thespitfire-secretsSecret) and data are kept.If the version changes, the database is first dumped to
~/spitfire/backups/*.dump(the newest 5 are kept;--no-backupskips it).Update the runners in other locations too: Runners → Updating runners. The Runners page marks runners on another release than the controller with a yellow older release tag.
Rolling back
If an update goes wrong:
~/spitfire/install.sh docker --rollback # Kubernetes: install.sh kubernetes --rollbackOn Windows .\install.ps1 -Rollback. The script first dumps the current database, restores the newest dump under backups/ (or the file you give: --rollback backups/<file>.dump) and returns to the release that took that dump. Changes made after the dump are lost. Without a terminal it needs --yes.
Updating without internet
On installations that cannot reach the internet, such as banks or closed networks, the controller cannot learn about a new release itself. When the update check is off (SPITFIRE_UPDATE_CHECK=off) or its last attempt failed, and the installed release is more than 60 days old, admins see a This release is N days old note at the top of the page. The installed release's date is also on the License → Version and updates card. There are three ways; pick the one that fits your network.
a) Temporary internet access
- For the time of the update, allow HTTPS (443) from the controller server (on Kubernetes, the nodes too) to:
spitfire.sh,spitfire.tr: the install script, bundles,releases.json, the Helm chart- Docker Hub:
registry-1.docker.io,auth.docker.io,production.cloudflare.docker.com,docker-images-prod.6aa30f8b08e16409b46e0173d6de2f56.r2.cloudflarestorage.com(image layers),hub.docker.com(the release check of a Kubernetes install)
- In the UI, press Check now on the License → Version and updates card; the new release and its notes show up. With the check off (
SPITFIRE_UPDATE_CHECK=off) skip this step; the latest release is on usespitfire.com/release-notes. - Run the update command the card shows (Updating step by step); the database is dumped first.
- Close the access again.
If you go out through a proxy, run the command with HTTPS_PROXY set: Installing behind a proxy.
b) Internal registry mirror (Harbor, Artifactory, Nexus)
Pull the image on a machine with internet access and push it to your internal registry (or use your registry's Docker Hub proxy cache):
docker pull usespitfire/spitfire:0.33.0
docker tag usespitfire/spitfire:0.33.0 harbor.example.local/spitfire/spitfire:0.33.0
docker push harbor.example.local/spitfire/spitfire:0.33.0Then unpack the installer bundle (spitfire-0.33.0.tar.gz, listed at usespitfire.com/install) into the install folder and run the installer with that image:
tar -xzf spitfire-0.33.0.tar.gz -C ~/spitfire
~/spitfire/install.sh docker --image harbor.example.local/spitfire/spitfire:0.33.0
~/spitfire/install.sh kubernetes --image harbor.example.local/spitfire/spitfire:0.33.0With Helm: helm upgrade spitfire ./spitfire-0.33.0.tgz -n spitfire --reuse-values --set image.repository=harbor.example.local/spitfire/spitfire --set image.tag=0.33.0. A new installation also needs the Postgres image in the mirror: push postgres:16 the same way and give --postgres-image harbor.example.local/spitfire/postgres:16 (Helm: --set postgres.image=…); the image given is kept on later runs.
c) Offline bundle (--offline)
Every release publishes one bundle per platform: spitfire-offline-<version>-linux-amd64.tar.gz and …-linux-arm64.tar.gz (listed with their .sha256 at usespitfire.com/install). It holds the Spitfire image and the Postgres image installations use (postgres:16; docker save for that platform), the installer bundle, the Helm chart, the signed releases.json and a signed SHA256SUMS of all of it. A first installation works from the bundle alone; nothing is downloaded.
Download the bundle on a machine with internet access and copy it to the controller server the way your organisation moves files in (approved USB media, a file transfer server,
scpthrough a jump host). A bundle is a few hundred MB; keep three times its size free on the install folder's disk.For a first installation, unpack the bundle's installer files first (an update skips this):
mkdir -p ~/spitfire && tar -xzf spitfire-offline-0.33.0-linux-amd64.tar.gz -C ~/spitfire spitfire-0.33.0.tar.gz tar -xzf ~/spitfire/spitfire-0.33.0.tar.gz -C ~/spitfire && rm ~/spitfire/spitfire-0.33.0.tar.gzRun it with the installer in the install folder:
~/spitfire/install.sh docker --offline spitfire-offline-0.33.0-linux-amd64.tar.gz ~/spitfire/install.sh kubernetes --offline spitfire-offline-0.33.0-linux-amd64.tar.gz --registry harbor.example.local/spitfire
The script never reaches the internet. It first checks the SHA256SUMS signature with the Spitfire release key written into it (OpenSSL 3, Ed25519) and every file against the list; if the signature does not hold, it stops without touching anything. Then it loads the image with docker load, unpacks the new release's installer files into the folder (deploy/*/.env is kept) and installs with --image. If the Postgres image is not on this machine it loads that too (if it is, it is left alone: an update does not change the running database's image): if the version changes the database is dumped first, and --rollback works as usual. On Kubernetes --registry is required, because the cluster cannot see images on this machine: the image is pushed to your internal registry as REG/spitfire:<version> (docker login first); the Postgres image is pushed as REG/postgres:16 and the installation uses it. With Helm, the chart is the bundle's helm/spitfire-<version>.tgz: --set image.repository=REG/spitfire --set image.tag=<version> --set postgres.image=REG/postgres:16. Runner servers update from the same bundle: install.sh runner --offline <bundle>.
Without OpenSSL 3 on the server (e.g. RHEL 8) the script cannot check the signature and refuses the bundle. Then give the bundle's .sha256 value listed at usespitfire.com/install: --offline-sha256 <value>. You can also verify the bundle by hand (on a machine with OpenSSL 3):
mkdir bundle && tar -xzf spitfire-offline-0.33.0-linux-amd64.tar.gz -C bundle && cd bundle
{ printf 'spitfire-offline-v1\n'; cat SHA256SUMS; } > signed.txt
openssl base64 -d -A -in SHA256SUMS.sig -out SHA256SUMS.bin
openssl pkeyutl -verify -rawin -pubin -inkey spitfire-release.pub.pem -in signed.txt -sigfile SHA256SUMS.bin
sha256sum -c SHA256SUMSspitfire-release.pub.pem is Spitfire's release key (also in the bundle's README.txt):
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAE7pSZPzN3PgBdeCyFwRvfMP2EWlKt87gSgRu84qu5Io=
-----END PUBLIC KEY-------offline comes with this release's install.sh. On an older installation, the first time, unpack the bundle's spitfire-<version>.tar.gz into the install folder (tar -xzf … -C ~/spitfire), then run the command above. Windows (install.ps1) has no --offline: verify the bundle on a Linux machine, load it with docker load -i spitfire-image.tar and run .\install.ps1 docker -Image usespitfire/spitfire:<version>. For Postgres, also run docker load -i postgres-image.tar.
Serving the release list on your network
So that the controller still shows new releases in the UI, put the bundle's releases.json and releases.json.sig on an internal web server and give the controller SPITFIRE_UPDATE_FEED=https://intranet.example.local/spitfire/releases.json (the .sig must be at the same address plus .sig). The list is signed; if it is altered on the mirror the controller refuses it. Replace both files with every new bundle.
Installing behind a proxy
Behind a corporate HTTPS proxy, run the install command with HTTPS_PROXY (and if needed HTTP_PROXY, NO_PROXY) set:
export HTTPS_PROXY=http://proxy.example.com:3128
export NO_PROXY=.example.local
curl -fsSL https://spitfire.sh | bash -s -- dockerThe script passes these values on to the controller (Docker: deploy/docker/.env; Kubernetes: the spitfire-proxy Secret). The controller makes the update check, webhooks and SSO calls through this proxy. The installation's own names (localhost, postgres, controller…) are added to NO_PROXY automatically. Add the names of an SSO provider or webhook targets on your internal network to NO_PROXY yourself. If a later run gives no proxy, the earlier setting is kept.
On an installation without internet access, turn the update check off: SPITFIRE_UPDATE_CHECK=off on the controller, and follow the release notes on usespitfire.com/release-notes. If you use your own mirror, give it with SPITFIRE_UPDATE_FEED; the mirror must also carry releases.json.sig. Ways to update: Updating without internet.
Backups and SPITFIRE_SECRET_KEY
Spitfire's data lives in Postgres; connection secrets are encrypted with SPITFIRE_SECRET_KEY. Back up both.
A full backup in one file (the database and the installation's keys, encrypted with a passphrase you choose):
cd ~/spitfire
./install.sh docker --backup spitfire-2026-10-10.sfbak # Kubernetes: ./install.sh kubernetes --backup …It asks for a passphrase (at least 12 characters), or takes it from SPITFIRE_BACKUP_PASSPHRASE for a scheduled job. Keep the
passphrase apart from the file: without it the backup cannot be opened. To restore, on this machine or a new one, install
Spitfire first and then:
./install.sh docker --restore spitfire-2026-10-10.sfbakThe current database is backed up first (backups/), then the backup's data and keys replace it and the services restart;
sign in with the backup's users. A backup of a newer Spitfire is refused on an older installation (update it first). The
target keeps its own Postgres password.
Or by hand:
- Keys: on Docker the
~/spitfire/deploy/docker/.envfile, on Kubernetes thespitfire-secretsSecret. Copy it to a safe place (such as a password vault). - Database: updates take
~/spitfire/backups/*.dumpautomatically when the version changes; restore them withpg_restore. For regular backups, add this folder or the Postgres volume to your own backup system.
If SPITFIRE_SECRET_KEY is lost, stored connection secrets (database, Kafka… passwords) cannot be read; you have to enter them all again. The TLS identity the controller presents to runners is also stored encrypted with this key.
Ports and network
| Port | For | Who must reach it |
|---|---|---|
| 8470 (Docker; NodePort 30470 on Kubernetes) | Web UI and REST API | Users, CI pipelines |
| 8471 | gRPC endpoint remote runners connect to, TLS | Runner servers in other locations (inbound TCP) |
| 8475 | Plaintext gRPC for runners on the same machine/cluster | Only the installation's own runners; never published |
| 8472 (optional) | Mock services (turned on with SPITFIRE_MOCK_ADDR) |
The system under test |
Runners connect out to the controller; no inbound port is needed on a runner server.
With a reverse proxy (ingress, nginx) in front of the UI, the client address is taken from X-Forwarded-For only when the request comes from loopback or a private network. Declare a proxy on a public address (a cloud load balancer, Cloudflare) in SPITFIRE_TRUSTED_PROXIES (comma-separated CIDRs); otherwise the audit log and the sign-in limits see the proxy's address. When users reach Spitfire directly from the same private network (no proxy in between), set SPITFIRE_TRUST_PRIVATE_NETWORKS=false: then only loopback and the proxies in SPITFIRE_TRUSTED_PROXIES may set the client address, so a user cannot choose the address they appear from in the audit log and the sign-in limits.
Sign-in is throttled: one client address may try 30 times at once, then once every 2 s; an account accepts 10 wrong passwords, then one a minute. When many users sit behind one proxy/NAT, raise the first limit with SPITFIRE_SIGNIN_BURST.
Link-local and cloud metadata addresses (169.254.0.0/16 such as 169.254.169.254, fe80::/10, fd00:ec2::254, 100.100.100.200) are never reached: not by the controller's own requests (webhooks, imports from a URL, observability and connection tests, Try it, GraphQL schema reading) and not by test steps (HTTP, GraphQL, SSE, WebSocket, MQTT 5, browser, a custom SQS endpoint). Such a request fails with link-local and cloud metadata addresses are not allowed. localhost and private addresses (10.x, 172.16–31.x, 192.168.x) are not affected. If a system you test really lives on a link-local address, set SPITFIRE_ALLOW_LINK_LOCAL=1 on the runners (and on the controller for Try it); the controller's own requests stay protected. Test steps also open only http/https addresses (ws/wss for WebSocket), even when the address comes from a variable; a browser step never opens file:// pages.
TLS and runner pin basics
- Port 8471 is always TLS. The controller creates its own identity on first start and stores it in the database (encrypted with
SPITFIRE_SECRET_KEY). - A remote runner verifies the controller by the pin of that key (
sha256:…). The pin and the ready-made install command are in the key dialog on the Runners page. The pin is required because connection secrets travel to runners over this channel. - To use your own certificate, give the controller
SPITFIRE_GRPC_TLS_CERTandSPITFIRE_GRPC_TLS_KEY; runners then verify with--tls(system CAs) or--ca <PEM file>instead of a pin. - The controller address remote runners see is set at installation with
--grpc-public-addr HOST:PORTor withSPITFIRE_GRPC_PUBLIC_ADDR.
Details: Runners → TLS pin and certificates.
Uninstalling
~/spitfire/uninstall.sh docker
~/spitfire/uninstall.sh kubernetes --context C --namespace NS
~/spitfire/uninstall.sh runner # remote runners installed with install.sh runnerWindows: .\uninstall.ps1 docker or .\uninstall.ps1 runner.
By default the data (Postgres volume / PVC) and the generated keys (.env / spitfire-secrets) are kept; a later install.sh brings everything back. To delete everything, add --purge (Windows: -Purge).
--purge deletes the database and SPITFIRE_SECRET_KEY; it cannot be undone. Take a backup first.
Common problems
Symptom: ERROR: Cannot reach the Docker daemon.
Cause: Docker is not running, or your user is not in the docker group.
Fix: Start Docker Desktop or sudo systemctl start docker; add your user to the docker group and log in again.
Symptom: ERROR: Docker Compose not found.
Cause: The Compose v2 plugin is not installed.
Fix: Install the Docker Compose v2 plugin (https://docs.docker.com/compose/install/).
Symptom: ERROR: Port 8470 is used by another application.
Cause: Another service on the machine uses that port.
Fix: Pick another port: install.sh docker --port 8480 --grpc-port 8481.
Symptom: ERROR: Could not pull the image: usespitfire/spitfire:...
Cause: The machine cannot reach Docker Hub, or the version is wrong.
Fix: Try internet access with docker pull; behind a proxy, configure the Docker daemon's proxy; check the version number.
Symptom: The setup screen says "Wrong setup code".
Cause: The code was copied incompletely, or it belongs to another installation.
Fix: Copy it again from the .env file or the controller log (First sign-in and setup code).
Symptom: The sign-in page opens instead of the setup screen, saying "Setup is already complete".
Cause: The first admin already exists (perhaps through --admin-email).
Fix: Sign in with that account. If its password is unknown, use spitfire-controller reset-password on the server: Users and access.
Symptom: ERROR: The controller was not ready within 180 s.
Cause: The controller failed while starting (database, missing setting…).
Fix: Read the log with docker compose -p spitfire -f deploy/docker/docker-compose.yml logs -f controller; if you cannot solve it, prepare a support bundle with ./install.sh --support-bundle (Troubleshooting).
Symptom: The Version and updates card shows Check failed.
Cause: The controller cannot reach spitfire.tr.
Fix: Behind a proxy, run the install command again with HTTPS_PROXY set; without internet access set SPITFIRE_UPDATE_CHECK=off.
Symptom: After a reinstall, saved connections no longer work; secrets cannot be read or Test fails.
Cause: SPITFIRE_SECRET_KEY changed (e.g. .env was deleted and the installation redone).
Fix: Put back the .env you backed up and run the installer again; without a backup, enter the secrets again.