Quickstart
No install needed to look around. The live demo is a read-only instance of exactly what you get, seeded with made-up data. It runs open on purpose: every page and every API read works without signing in, and every change is refused. An install you run never serves open on a network address. Only loopback and --read-only may, and it warns when it does.
Requirements
An Ansible run needs ansible-core, which the binary does not carry. Use the ansible-playbook and
ansible-inventory already on your PATH, or, once the binary is installed, let
switchtender ansible install put a pinned, hash-checked ansible-core beside the database, as
install Ansible shows. That install needs a python3 from 3.10 to 3.14 with its
venv module, and it does not run on Windows.
The other tools come from the PATH: terraform, tofu, python3, pwsh (PowerShell 7), or go.
Bash runs use the system shell. Nothing else for the default SQLite setup. Building from source
instead of installing the release binary needs Go 1.26, and Docker Compose is an alternative, where
docker compose --profile stack up --build builds the image from this repository. That image
already carries Ansible, Python, Terraform, and OpenTofu.
Install
curl -fsSL https://switchtender.com/install.sh | sh
On a host with wget and no curl, wget -qO- https://switchtender.com/install.sh | sh does the
same. The script downloads the release binary for your platform, checks it against the published
checksums, and installs it. It writes to /usr/local/bin when it can and to ~/.local/bin
otherwise, which is what happens on a stock Mac and on any Linux install without root. That second
directory is often not on PATH, so the script says so and prints its closing commands with the full
path; add the line it gives you to run switchtender by name. Running in about a minute. Prefer to build it
yourself, or want to hack on it? go build -o switchtender . from a clone produces the same
binary; the commands below assume it is on your PATH, so prefix a locally built binary with ./.
Install Ansible
Skip this when ansible-playbook is already on your PATH, or when you only run the other tools.
Otherwise run the install in the directory you will start the server in, as the account that will
run it:
switchtender ansible install
It downloads ansible-core 2.21.4 from PyPI, checks every file against a hash the binary carries, and
installs it into ansible/ beside switchtender.db, where the server finds it with no restart.
That release needs Python 3.12 to 3.14. On Python 3.11 add --version 2.19, and on 3.10
--version 2.17. The python3 macOS ships is 3.9, so install a newer Python there first. Debian
and Ubuntu package the venv module separately, such as python3.12-venv, and the install names
the package when it is missing. The directory and every directory above it must be writable only by
that account or root, or the install refuses it. Managed Ansible runtime
covers offline installs, workers, and the desktop app.
Run the server
export SWITCHTENDER_ENCRYPTION_KEY=$(openssl rand -hex 32)
export SWITCHTENDER_ENCRYPTION_SALT=$(openssl rand -hex 16)
switchtender serve --addr :8080 --db switchtender.db
The key and salt together seal stored credentials at rest with argon2id and AES-256-GCM. Keep both with your other secrets: a server started with a different pair cannot open the credentials sealed under this one. Without them the server still runs, but credential features stay off.
The first start on an empty database mints an initial admin token and prints it once, so the API is authenticated from the first request. Export it for the commands below:
export ST_TOKEN=<the token serve printed>
Open http://localhost:8080 for the web UI and sign in with that token, or use the API directly.
A fresh install opens with an empty templates list. switchtender examples --db switchtender.db
seeds a handful of starter templates that run with no project, inventory, or credential, so a first
launch works on the spot. It skips a template whose name is already present, so it is safe to re-run.
On your own machine, switchtender desktop does all of this in one command. It picks a stable
loopback port, keeps its data in a per-user directory, and opens the UI. The
desktop guide covers it, including packaging.
Submit a run
The first one needs nothing on disk, so it succeeds on an install that is minutes old:
curl -X POST localhost:8080/v1/runs \
-H "Authorization: Bearer $ST_TOKEN" \
-d '{"tool": "bash", "command": "echo hello from switchtender"}'
An Ansible run takes a playbook and an inventory. The server resolves both relative to its own
working directory, so site.yml and hosts.ini have to exist there, or the run names a
project and they are resolved inside that checkout instead. Without either the run
is submitted, accepted, and then fails with "the playbook: site.yml could not be found":
curl -X POST localhost:8080/v1/runs \
-H "Authorization: Bearer $ST_TOKEN" \
-d '{"playbook": "site.yml", "inventory": "hosts.ini"}'
The response carries a run id. Fetch its status, its structured events, or its log:
curl -H "Authorization: Bearer $ST_TOKEN" localhost:8080/v1/runs/<id>
curl -H "Authorization: Bearer $ST_TOKEN" localhost:8080/v1/runs/<id>/events
curl -H "Authorization: Bearer $ST_TOKEN" localhost:8080/v1/runs/<id>/logs
Add "shards": 4 to the body to split the run across four slices of the inventory, balanced by
each host's measured duration in recent runs.
Add a worker (Team)
Distributed execution is a Team feature, so every switchtender worker needs a license and refuses
to start without one. Community runs everything on the server itself, which is the default and needs
no extra process: this section is for when one machine is no longer enough.
Point a worker at the same database, with the same key and salt set as the server's, and it competes for queued runs:
switchtender worker --db switchtender.db --name laptop
For more than one machine, use a PostgreSQL DSN as the --db value on every process.
Queues are part of the same feature: naming one on a run, a template, or an inventory source routes it to a worker serving that name, so it is refused on Community rather than accepted and left with nothing able to claim it.
Lock down the API
The initial admin token from the first start is yours to keep, but a shared install deserves an account for each person and each CI job, with tokens bound to them, so the audit trail says who did what and each token carries only its account's role. Create the accounts:
SWITCHTENDER_PASSWORD=secret switchtender user new operator-jane --role operator --db switchtender.db
SWITCHTENDER_PASSWORD=secret switchtender user new ci-deploy --role operator --db switchtender.db
Then mint a token for each, bound with --user:
switchtender token new --db switchtender.db --user ci-deploy --name ci
A token minted without --user is an admin token that names no account, like the initial one, so
keep those few.
A loopback bind, or --read-only, starts without minting an initial token: a loopback API is
reachable only from this machine, and a read-only one changes nothing. On a loopback bind the API
asks for a token from the moment the install holds any token or account.
Run with Docker
The compose file lives in the repository, so this one needs a checkout rather than the installed binary:
git clone https://github.com/kordloom/switchtender
cd switchtender
export SWITCHTENDER_ENCRYPTION_KEY=$(openssl rand -hex 32)
export SWITCHTENDER_ENCRYPTION_SALT=$(openssl rand -hex 16)
docker compose --profile stack up --build
This starts one server on SQLite, with its database and its signing key in a volume, so both
survive a restart and an upgrade. The server listens on port 8080. Set SWITCHTENDER_PORT to change
the host port. With no terminal to show it on, the initial admin token is written to
/data/initial-admin-token in the container, readable by the service user alone:
docker compose --profile stack exec server cat /data/initial-admin-token
On a Team license, the team profile runs PostgreSQL, a server, and a worker instead. Put the license
at ./switchtender-license.json, or name it with SWITCHTENDER_LICENSE_FILE, and give every process
the same signing key, since processes that share one database sign as one install:
export SWITCHTENDER_AUDIT_KEY=$(openssl rand -hex 32)
docker compose --profile team up --build
Keep that key where you can restore it, beside the encryption key and salt.
Set up a production server
For a real install, init generates the encryption key and salt, creates the first admin account, and
writes a config file in one step. It can also write a systemd unit:
sudo useradd --system --create-home --home-dir /var/lib/switchtender switchtender
sudo -u switchtender -H switchtender init --db /var/lib/switchtender/switchtender.db \
--config /var/lib/switchtender/switchtender.env \
--systemd /var/lib/switchtender/switchtender.service
The unit runs the server as the account that ran init, and every run the server executes without
an execution image runs as that account too, which is why init runs here as an account made for
the job rather than as you or as root. It prints the admin password once, so save it. Move the unit
into place and start it:
sudo cp /var/lib/switchtender/switchtender.service /etc/systemd/system/switchtender.service
sudo systemctl enable --now switchtender
The unit listens on loopback, which suits a reverse proxy on the same machine. To serve HTTPS
directly with nothing in front, give it a certificate and key and listen on the network: add
--addr :8443 --tls-cert /path/to/tls.crt --tls-key /path/to/tls.key to the unit's ExecStart
line, then sudo systemctl daemon-reload && sudo systemctl restart switchtender. Run by hand, the
server reads the same key and salt from the file init wrote:
set -a; . /var/lib/switchtender/switchtender.env; set +a
switchtender serve --db /var/lib/switchtender/switchtender.db --addr :8443 \
--tls-cert /path/to/tls.crt --tls-key /path/to/tls.key
Run on Kubernetes
The Helm chart runs the server as an ordinary Deployment, with no operator. By default it is the Community shape: one server on SQLite, with the whole install on a persistent volume.
The chart is in the repository, so clone it first if you installed the binary alone. Write the keys to a file before you install, so you keep them. The salt has to stay the same for the life of the install, because every stored credential is sealed against it, and a key or a salt generated afresh on an upgrade makes every stored secret unreadable:
git clone https://github.com/kordloom/switchtender
cd switchtender
printf 'encryptionKey: "%s"\nencryptionSalt: "%s"\nauditKey: "%s"\n' \
"$(openssl rand -hex 32)" "$(openssl rand -hex 16)" "$(openssl rand -hex 32)" \
> switchtender-secrets.yaml
chmod 600 switchtender-secrets.yaml
helm install switchtender ./deploy/helm/switchtender -f switchtender-secrets.yaml \
--set fullnameOverride=switchtender
Keep that file in a secret manager, and pass the same -f switchtender-secrets.yaml to every
helm upgrade. Or put the three values in a Secret of your own under SWITCHTENDER_ENCRYPTION_KEY,
SWITCHTENDER_ENCRYPTION_SALT, and SWITCHTENDER_AUDIT_KEY and pass --set existingSecret=<name>.
Copy the values across before you switch an install over: the chart keeps its own Secret rather than
deleting it, but the pods read only the one you name.
auditKey is the install's signing seed. On SQLite the server mints one itself when it is empty,
and setting it means you hold the copy to restore. On PostgreSQL it is required, since every server
and worker has to sign as the same install and none of them mints the key.
fullnameOverride names the resources switchtender and switchtender-server rather than repeating
the name. Set it on a new install only: renaming an existing install's resources orphans its data
volume and its keys.
The install prints how to reach the interface. With the default ClusterIP service:
kubectl port-forward svc/switchtender 8080:8080
The first server to start writes an initial admin token to /data in its pod, readable by the
service user alone:
kubectl exec deploy/switchtender-server -- cat /data/initial-admin-token
Sign in with it, create named accounts and tokens, and delete it.
On a Team license, point database.dsn at PostgreSQL to run more than one server, and set
worker.enabled=true for dedicated workers. Pass the license with
--set-file license=switchtender-license.json: --set parses commas and braces and changes it. The
DSN reaches the pods from a Secret rather than as an argument, so the password is not in the pod
spec.
helm uninstall leaves the data volume and the Secret holding the keys in place. Delete them
yourself once you are sure. extraEnv, extraEnvFrom, extraVolumes, and extraVolumeMounts
carry anything the chart has no key for, such as a secret source's token or a CA bundle.
Try the demo
To look around without setting anything up, run the seeded demo. It fills a fresh database with sample projects, templates, inventories, and real runs, including a flaky host, a split, and a pipeline, then serves it read-only so it is safe to expose:
switchtender demo --addr :8080
Or with Docker, from a checkout: docker compose --profile demo up --build, which serves on port
8081 so it can run beside the stack. SWITCHTENDER_DEMO_PORT moves it.
