Skip to content

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.

ComponentWhy
PostgreSQLThe sole cold storage for a Keeper cluster: agent and operator registries, the service catalog, logs.
RedisThe heartbeat cache, leases on agent identifiers, coordination between Keeper instances.
VaultPKI for issuing agents’ mTLS identity and secret storage (DSN, the Redis password, the token-signing key).
The keeper, soul, soul-lint binariesSee 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:

PortPurposeProtocolRequired
8080Operator API (HTTP), the /readyz health check, the /ui web-UIHTTPrequired
9090Metrics (/metrics, Prometheus scrape/pull)HTTPrequired listener
8081MCPHTTPoptional listener
9442gRPC bootstrap (agent onboarding: soul init)server-only TLSrequired
9443gRPC EventStream (the agent’s long-lived stream: soul run)mTLSrequired

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).

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: 30

This 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:

Terminal window
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 role
vault secrets enable -path=pki pki
vault secrets tune -max-lease-ttl=87600h pki
vault write pki/root/generate/internal common_name="Soul Stack Demo Root" ttl=87600h
vault 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.json
jq -r .data.certificate keeper-issue.json > keeper.crt
jq -r .data.private_key keeper-issue.json > keeper.key
jq -r .data.issuing_ca keeper-issue.json > pki-ca.crt

keeper.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).

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 config
kid: 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:

Terminal window
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:

Terminal window
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:

Terminal window
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.

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).

On the Keeper side (through the Operator API):

Terminal window
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.

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.com
paths:
modules: /var/lib/soul-stack/modules
seed: /var/lib/soul-stack/seed
keeper:
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.crt

soul 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_TOKEN environment variable (the flag takes priority over the variable).

The flag equivalent (handy for one-off manual debugging):

Terminal window
soul init --token='<bootstrap_token from 4.1>' --config /etc/soul/soul.yml

On success the agent record moves pending → connected once the daemon starts. Start the daemon (it holds the EventStream to Keeper over mTLS):

Terminal window
soul run --config /etc/soul/soul.yml

Check that the host shows up as connected:

Terminal window
curl -s http://keeper.example.com:8080/v1/souls/host-01.example.com \
-H "Authorization: Bearer $TOKEN"
# the response has status: connected

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:

service.yml
name: demo
state_schema_version: 1 # no migrations/ directory needed at v1

scenario/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:

scenario/create/main.yml
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):

Terminal window
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):

Terminal window
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.

Query the incarnation’s status (applyingready on success, error_locked on failure):

Terminal window
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:

Terminal window
which htop && cat /etc/motd

The run history (state snapshots):

Terminal window
curl -s http://keeper.example.com:8080/v1/incarnations/demo-app/history \
-H "Authorization: Bearer $TOKEN"
  • 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.