Quick Start
This guide walks you from zero to an applied Destiny in about 5 minutes: bring up Keeper, create the first operator, connect a single agent, and apply a simple Destiny that installs a package and creates a file on the host.
This is a demo walkthrough to get you oriented, not a production install. For a production rollout (HA, multiple Keepers, managed infrastructure, persistent Vault, TLS material) see Operations and Installing from packages.
What you’ll need
Section titled “What you’ll need”| Component | Why |
|---|---|
| PostgreSQL | The sole cold storage for a Keeper cluster: agent and operator registries, the service catalog, logs. |
| Redis | The heartbeat cache, leases on agent identifiers, coordination between Keeper instances. |
| Vault | PKI for issuing agents’ mTLS identity and secret storage (DSN, the Redis password, the token-signing key). |
The keeper, soul, soul-lint binaries | See Installation. |
All three components must be reachable over the network from the host where Keeper runs.
Keeper opens several listeners. The values below are an example from the demo config; in production they’re configurable:
| Port | Purpose | Protocol | Required |
|---|---|---|---|
8080 | Operator API (HTTP), the /readyz health check, the /ui web-UI | HTTP | required |
9090 | Metrics (/metrics, Prometheus scrape/pull) | HTTP | required listener |
8081 | MCP | HTTP | optional listener |
9442 | gRPC bootstrap (agent onboarding: soul init) | server-only TLS | required |
9443 | gRPC EventStream (the agent’s long-lived stream: soul run) | mTLS | required |
The soul agent initiates the connection to Keeper itself — managed hosts need no inbound ports open (the only thing listening externally is the local metrics listener).
Step 1. Bring up the infrastructure
Section titled “Step 1. Bring up the infrastructure”Bring up PostgreSQL, Redis, and Vault. Keeper reads the PostgreSQL DSN, the Redis password, and the operator-token signing key from Vault, and it issues agents’ mTLS certificates through Vault PKI. Provisioning comes down to:
- writing the KV secrets: the PostgreSQL DSN, the Redis password, the token-signing key;
- enabling the PKI engine and creating a role for issuing agent certificates;
- issuing Keeper’s server TLS certificate from the same PKI root.
Detailed Vault provisioning commands (KV, PKI, AppRole, issuing the server certificate) are in Installing from packages. Version requirements, the mandatory parameters, and the PostgreSQL / Redis / Vault modes (what to configure before starting Keeper) are in Preparing the infrastructure.
Local trial: bring up all three with Docker Compose
Section titled “Local trial: bring up all three with Docker Compose”For a quick local trial you don’t have to install anything by hand: one compose.yml brings up PostgreSQL, Redis, and Vault with settings that line up with the demo keeper.yml below (Redis with no password, Vault in dev mode). Save it and run docker compose up -d:
# compose.yml — local trial infrastructure (not for production)services: postgres: image: postgres:16-alpine environment: POSTGRES_USER: keeper POSTGRES_PASSWORD: keeper POSTGRES_DB: keeper ports: ["127.0.0.1:5432:5432"] healthcheck: test: ["CMD-SHELL", "pg_isready -U keeper -d keeper"] interval: 2s timeout: 3s retries: 30
redis: image: redis:7-alpine ports: ["127.0.0.1:6379:6379"] # no password — matches redis.password_ref: "" below healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 2s timeout: 3s retries: 30
vault: image: hashicorp/vault:1.18 cap_add: ["IPC_LOCK"] environment: VAULT_DEV_ROOT_TOKEN_ID: root # dev-mode root token — matches vault.token: root below VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200 ports: ["127.0.0.1:8200:8200"] healthcheck: test: ["CMD", "vault", "status", "-address=http://127.0.0.1:8200"] interval: 2s timeout: 3s retries: 30This brings up the three services. Now provision the dev Vault — one KV secret plus the PKI that issues the TLS material (Keeper’s server certificate and the CA you put on each host). The vault CLI talks to the container above:
export VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN=root
# KV secret the demo keeper.yml reads (postgres.dsn_ref)vault kv put secret/keeper/postgres dsn="postgres://keeper:keeper@127.0.0.1:5432/keeper"
# PKI: engine → root → issue rolevault secrets enable -path=pki pkivault secrets tune -max-lease-ttl=87600h pkivault write pki/root/generate/internal common_name="Soul Stack Demo Root" ttl=87600hvault write pki/roles/soul-seed allow_any_name=true max_ttl=720h
# Issue Keeper's server cert from that root (the same root the agents chain to)vault write -format=json pki/issue/soul-seed \ common_name=127.0.0.1 alt_names=localhost ip_sans=127.0.0.1 ttl=720h > keeper-issue.jsonjq -r .data.certificate keeper-issue.json > keeper.crtjq -r .data.private_key keeper-issue.json > keeper.keyjq -r .data.issuing_ca keeper-issue.json > pki-ca.crtkeeper.crt / keeper.key / pki-ca.crt are exactly the three files the demo keeper.yml below points at. pki-ca.crt is the CA — copy it to each host (soul.yml → keeper.tls.ca) so the agent can verify Keeper during the bootstrap phase (step 4.2).
Step 2. Start Keeper
Section titled “Step 2. Start Keeper”Fill in Keeper’s config (the PostgreSQL/Redis/Vault addresses via vault: refs, the listeners, the paths to TLS material) and start the binary. The minimal demo config, like the rest of this guide, is for orientation only:
# keeper.yml — minimal demo configkid: keeper-demo-01
listen: grpc: bootstrap: # agent onboarding (server-only TLS) addr: "127.0.0.1:9442" tls: cert: /etc/keeper/tls/keeper.crt key: /etc/keeper/tls/keeper.key event_stream: # the agent's long-lived stream (mTLS) addr: "127.0.0.1:9443" tls: cert: /etc/keeper/tls/keeper.crt key: /etc/keeper/tls/keeper.key ca: /etc/keeper/tls/pki-ca.crt # the same PKI root as the agent certificates openapi: { addr: "127.0.0.1:8080" } # Operator API + /readyz + /ui metrics: { addr: "127.0.0.1:9090" } # Prometheus scrape mcp: { addr: "127.0.0.1:8081" } # optional; drop the block — MCP is off
postgres: dsn_ref: vault:secret/keeper/postgres # reads the dsn field from Vault KV
redis: addr: "127.0.0.1:6379" password_ref: "" # empty = Redis with no password (demo)
vault: addr: "http://127.0.0.1:8200" token: "root" # dev-mode root token; AppRole in production pki_mount: "pki" # PKI engine for issuing agents' mTLS identity pki_role: "soul-seed"Secrets (the PostgreSQL DSN, the Redis password) don’t sit in the config — they’re pulled from Vault via vault: refs (postgres.dsn_ref, redis.password_ref). vault.token: root is a dev shortcut for Vault in dev mode; in production Keeper authenticates to Vault via AppRole (see Installing from packages). The other blocks (auth / otel / logging / reaper / …) are omitted — their defaults work as-is.
Check readiness:
curl -fsS http://127.0.0.1:8080/readyz && echo OK/readyz returns 200 when the instance is ready to take traffic (PostgreSQL and Redis are reachable).
Step 3. Create the first operator (Archon)
Section titled “Step 3. Create the first operator (Archon)”A Soul Stack operator is called an Archon, and its identifier is the AID. An AID is any string matching the pattern ^[a-z0-9][a-z0-9._@-]{1,127}$: it starts with a letter or a digit, followed by a-z0-9 and the characters ._@-, for a total length of 2–128 characters. For example, archon-alice, alice@corp.com, uid-4815, and ops-team all qualify. The first Archon is created by an administrative subcommand of the keeper binary itself — under a lock it verifies that the operator registry is empty, creates the first Archon with the cluster-admin role, and issues a JWT for it:
keeper init \ --archon=archon-alice \ --config=/etc/keeper/keeper.yml \ --credential-out=/etc/keeper/archon-alice.jwt--archon=alice@corp.com or --archon=ops-team are valid too: any AID matching the pattern above works.
The JWT is written to the --credential-out file with 0400 permissions. Save the token in a variable for the next steps:
TOKEN=$(cat /etc/keeper/archon-alice.jwt)A handy way to browse the API is GET /docs — a built-in OpenAPI-spec viewer. Open http://127.0.0.1:8080/docs, paste the JWT into the input field, and the page loads the full spec with endpoint search and a “Try It” button.
Step 4. Onboard an agent (Soul)
Section titled “Step 4. Onboard an agent (Soul)”A Soul is an agent on a managed host. The agent’s identifier (the SID) is the host’s FQDN. Onboarding goes through a CSR: the private key is generated on the host and never leaves it. The flow is two moves — the operator registers the host and gets a one-time bootstrap token, then soul init on the host exchanges the token for an mTLS identity (the SoulSeed).
4.1. Register the host
Section titled “4.1. Register the host”On the Keeper side (through the Operator API):
curl -s -X POST http://keeper.example.com:8080/v1/souls \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"sid": "host-01.example.com", "transport": "agent", "covens": ["demo"]}'The response carries a bootstrap_token (returned once, with a default TTL of 24 hours). The agent record appears in pending status. The covens: ["demo"] label is a stable host tag that scenarios target later. A lost token can’t be recovered — only reissued via POST /v1/souls/{sid}/issue-token.
4.2. Apply the token on the host
Section titled “4.2. Apply the token on the host”Before soul init, place the PKI root on the host (the same CA as Keeper’s server certificate) at the path from the soul.yml config (keeper.tls.ca) — the agent uses this file to verify Keeper’s server certificate during the bootstrap phase.
A minimal soul.yml:
sid: host-01.example.compaths: modules: /var/lib/soul-stack/modules seed: /var/lib/soul-stack/seedkeeper: endpoints: - host: keeper.example.com bootstrap_port: 9442 # server-only TLS, the `soul init` phase event_stream_port: 9443 # mTLS, the `soul run` phase priority: 1 tls: ca: /var/lib/soul-stack/seed/ca.crtsoul init generates the private key and a CSR, connects to Keeper’s bootstrap listener, and lays down the SoulSeed it gets back. The bootstrap token from 4.1 is passed in one of two ways:
- with the
--token=<bootstrap_token>flag; - via the
SOUL_BOOTSTRAP_TOKENenvironment variable (the flag takes priority over the variable).
The flag equivalent (handy for one-off manual debugging):
soul init --token='<bootstrap_token from 4.1>' --config /etc/soul/soul.ymlOn success the agent record moves pending → connected once the daemon starts. Start the daemon (it holds the EventStream to Keeper over mTLS):
soul run --config /etc/soul/soul.ymlCheck that the host shows up as connected:
curl -s http://keeper.example.com:8080/v1/souls/host-01.example.com \ -H "Authorization: Bearer $TOKEN"# the response has status: connectedStep 5. Apply the first Destiny
Section titled “Step 5. Apply the first Destiny”The state you apply lives in a service repository — an ordinary git repo. This is where the demo service registered below comes from; you author it and push it to git. For this walkthrough it’s a one-line manifest plus a single create scenario that installs a package and drops a file. The minimal layout:
demo-service/├── service.yml # manifest: name + state-schema version└── scenario/ └── create/ └── main.yml # the create operation (runs when an incarnation is created)service.yml — the manifest. A service’s version is a git ref (ADR-007), so there is no version: field here:
name: demostate_schema_version: 1 # no migrations/ directory needed at v1scenario/create/main.yml — the create scenario. With no orchestration delta (on: / serial: / where:) its steps run on all of the incarnation’s hosts. Each step is a desired state of the form core.<module>.<state> — core.pkg.installed = “the package is installed”, core.file.present = “a file with this content exists” — not an imperative command, and each is idempotent:
input: banner_text: type: string default: "Managed by Soul Stack"
state_changes: sets: banner: "${ input.banner_text }"
tasks: - name: Install the htop package module: core.pkg.installed params: name: htop
- name: Write the managed banner to motd module: core.file.present params: path: /etc/motd content: "${ input.banner_text }\n" mode: "0644"The task grammar (control blocks, register:, requisites) is in DSL → Destiny; the orchestration delta in DSL → Scenario.
Commit and push the repo, and note the ref (a tag or branch) you’ll reference. Then register the service in Keeper’s catalog (a git source + ref):
curl -s -X POST http://keeper.example.com:8080/v1/services \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "demo", "git": "https://git.example.com/svc/demo.git", "ref": "main"}'Create an incarnation — a runtime instance of the service; this runs the create scenario on the hosts. Let’s bind it to the demo label (where our agent from step 4 lives):
curl -s -X POST http://keeper.example.com:8080/v1/incarnations \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "demo-app", "service": "demo", "covens": ["demo"] }'The response is 202 Accepted with an apply_id: the operation is asynchronous.
Step 6. See the result
Section titled “Step 6. See the result”Query the incarnation’s status (applying → ready on success, error_locked on failure):
curl -s http://keeper.example.com:8080/v1/incarnations/demo-app \ -H "Authorization: Bearer $TOKEN"When status: ready, check on the host that the state was applied:
which htop && cat /etc/motdThe run history (state snapshots):
curl -s http://keeper.example.com:8080/v1/incarnations/demo-app/history \ -H "Authorization: Bearer $TOKEN"What’s next
Section titled “What’s next”- Concepts — the vocabulary and the mental model: what exactly gets applied to a host.
- DSL — how to write your own Destinies and scenarios.
- Modules — the catalog of built-in core modules.
- Operations — operating the souls: upgrades, monitoring, recovery.