Deployment topologies
Six ways to run the same thing. What each shape is for, what it hands you by default, and which one to pick.
On this page
TokenTriage deploys as one static binary with one durable directory beside it. Everything below is a different wrapper around that fact, and none of them changes the topology: one process, one data volume, two listeners.
That is worth saying plainly because it sets expectations. There is no control plane to install, no database to provision, no queue, no sidecar. Choosing a deployment shape is choosing how you would rather manage a process, not choosing an architecture.
Choose one
| Shape | Reach for it when | What you manage |
|---|---|---|
| the binary | a host you already configure, or a laptop | a service unit and a directory |
| a container | an existing container host or PaaS | one image, one volume, two ports |
| Docker Compose | a single host you want durable and locked down | a compose file and an env file |
| Helm | Kubernetes — the canonical artifact | a values file |
| Kustomize | Kubernetes under GitOps that cannot run Helm at apply | an overlay |
| Terraform | a single VM on AWS with the storage and load balancer alongside | a module invocation |
The Helm chart is canonical in a specific sense: the Kustomize base is rendered from it, with a CI gate keeping the two identical, and the Compose overlays and the Terraform module express the same single-node shape through their own primitives. Pick the one that fits how you already run things.
What every shape gives you
Two listeners, always, and they are not interchangeable.
The data plane takes proxy traffic on 8787. It is what your applications point at, and it
is the one you may expose once TLS terminates in front of it.
The admin plane serves 9090: the dashboard, the admin API, /metrics and /health. It
carries ledger metadata and the config surface, and it is not for the public internet. On
Kubernetes its Service type is a constant the chart will not let you override; on Compose it
binds loopback until you widen it yourself.
One data directory holds everything durable: the ledger, the governance store, the auth
stores and the derived index. /data in the images and on Kubernetes, ~/.tokentriage for a
host binary.
Kubernetes, with Helm
One StatefulSet at every topology, where a single node is replicas: 1, with one image, zero
chart dependencies, and one volume claim per pod.
$ helm install tt deploy/helm/tokentriage \
--namespace tokentriage --create-namespace \
-f my-values.yaml
The post-install notes resolve, for your release, how to reach both planes, the credential
one-liner, and the storage disclosure. Reprint them any time with helm get notes tt -n tokentriage.
You supply two things: the config body under config:, passed to the binary verbatim, and a
reference to a Secret holding your provider keys. The chart renders no Secret of its own.
config:
mode: ledger
upstreams:
anthropic:
base_url: https://api.anthropic.com
dialect: anthropic
api_key_env: ANTHROPIC_API_KEY
ledger:
decision_log:
path: /data/.tokentriage/decisions.jsonl
providerSecrets:
existingSecret: tt-provider-keys
Confirm the install with the bundled test, which asserts both halves of the secured-by-default state without needing a credential:
$ helm test tt -n tokentriage
TEST SUITE: tt-tokentriage-test-connection
Phase: Succeeded
Kubernetes, without Helm
deploy/k8s/base is a Kustomize base rendered from the chart, with airgap-mirror and
prod-pinned overlays, for shops that cannot run Helm at apply time:
$ kubectl apply -k deploy/k8s/base
The overlay image digests are invalid all-zero placeholders. They fail to pull until you replace them from the release’s image list. That is a loud failure in preference to a silently mutable tag.
Docker Compose
Overlays layer onto the root compose file, and they compose with each other. Four are worth knowing:
| Overlay | What it adds |
|---|---|
compose.prod.yaml |
a pinned image, a restart policy, resource limits, the env_file contract |
compose.hardened.yaml |
read-only rootfs, all capabilities dropped, no-new-privileges, a tmpfs /tmp |
compose.airgap.yaml |
a gateway-less internal network and an in-network mock upstream |
compose.backends.yaml |
development-grade Redis and Postgres, each behind its own profile |
Hardened production is the two stacked:
$ docker compose -f docker-compose.yml -f deploy/compose/compose.prod.yaml \
-f deploy/compose/compose.hardened.yaml up -d
Each of the first three carries a test profile that gates on a real exit code rather than on
up succeeding, because a proxy killed by a signal exits 0 and that must not read as a
pass. All three were run for this page; the air-gap gate additionally asserts a costed row comes
back with no route off the host.
Terraform on AWS
A single-VM example under deploy/terraform/aws/examples/single-vm builds on the tokentriage
module: an EC2 instance, a dedicated EBS volume at /data, and a TLS-terminated load balancer
in front of the data plane. The admin plane stays inside the security group and is reached
through Session Manager.
Credentials are ARNs resolved on the instance into the canonical environment variable names, so no plaintext is passed to the module or written to Terraform state. A separate external-state example expresses the shared-state tier on RDS and ElastiCache; it is a preview root that CI validates and does not apply.
How the shapes differ on the things that bite
| binary | Compose | Kubernetes | |
|---|---|---|---|
| config file | writable | writable if you mount it so | read-only projection |
| change config | edit, or the admin API | edit and restart | edit values, helm upgrade |
| dashboard default | off unless you enable it | off in the baked config | on, secured |
| admin bind | 127.0.0.1 |
loopback publish | 0.0.0.0, ClusterIP Service |
| credential bootstrap | run the CLI | docker compose exec |
kubectl exec |
| upgrade | replace the binary | pull and recreate | helm upgrade, rolls the pod |
| roll back | keep the old binary | keep the old tag | helm rollback |
The read-only config on Kubernetes is the difference that surprises people most: configuration in production covers what still works there and what the dashboard refuses.
What runs the same everywhere
Three behaviours are properties of the binary, not of the wrapper, so they hold in all six shapes.
Each instance keeps its own ledger. There is no shared ledger file at any topology. A fleet view is a read-time merge over per-instance ledgers, typed by how much of the fleet it covered.
Nothing dials out unless you consent. External state, an L3 cache, an external embeddings
API, OTLP export and the guardrail judge are each refused unless their config block carries
external: "allow", a string no deployment layer types for you.
Prices come from a compiled-in table. There is no pricing service to reach, which is why an air-gapped install still produces costed rows.