Docket is not a service. It is a program you wrap around an agent, and everything it produces is a file. That makes deployment unusually simple — and unusually easy to get wrong in the one way that matters, which is scope: a receipt says what the boundary saw, and a deployment that quietly moves the agent outside the boundary produces evidence of a run that did not happen.
This page covers the recipes in deploy/, what they are checked against, and where the
honesty stops. The short version: the images and manifests have been read by a checker and by
tests; none of them has been run here, because this repository is built and tested in an
environment with no container runtime and no cluster (which docker podman buildah finds nothing;
there is no kubectl).
| shape | what runs where | use it when |
|---|---|---|
| one process, one host | docket run -- <agent> on your machine or in CI |
the default. Nothing to deploy. |
| one container | the agent and Docket in the same container (deploy/Dockerfile) |
you already build images and want the run's workspace, snapshots and evidence to be the image's business |
| one pod, two containers | your agent under Docket, plus something for it to call (deploy/kubernetes/deployment.yaml, deploy/helm/docket/) |
you are running agents on Kubernetes and want caps and a receipt per run |
The rule that makes the third shape work is that the agent and the boundary must share a filesystem
view (Docket snapshots the tree the agent edits, and writes evidence beside it) and must reach each
other over loopback. Pods get both for free: containers in a pod share a network namespace, so
127.0.0.1 in one container is 127.0.0.1 in the other.
The boundary binds loopback only (src/docket/proxy.py), and that is a design decision, not an
oversight:
-
no Service, and no published ports. Nothing outside the pod can address the boundary, so nothing outside the pod can post events into a live run or read its ledger through the instrumentation endpoints;
-
no scraping from outside.
/docket/metricsis Prometheus text and it is real, but it is on a loopback socket. Read it from inside the pod:$ kubectl exec deploy/docket-agent -c docket -- python -c \ "import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:8787/docket/metrics').read().decode())"
If you want the numbers in your monitoring system, add a sidecar that curls loopback and exposes them on the pod network — and think about what else that sidecar exposes, because the boundary's write paths live on the same socket.
-
no
--portbeyond loopback.docket run --port 8787fixes the port inside the network namespace, which is what a probe needs; it does not change the interface it binds.
make deploy-check runs tools/deploy_check.py, which reads the
artefacts and asserts the invariants that hold regardless of which cluster they land on:
| check | what it establishes |
|---|---|
chart.metadata, chart.renders |
the chart is well-formed and renders under its own values |
chart.values-are-an-interface |
every .Values reference is declared and every declared value is used |
documents.are-declarations |
every Kubernetes document has an apiVersion and a kind |
images.are-pinned |
no :latest, no untagged images |
pod.is-hardened |
non-root, no privilege escalation, all capabilities dropped, read-only root filesystem, seccomp, no hostPath, no host namespaces |
containers.have-resources |
requests and limits on every container |
probes.use-a-declared-port |
every probed port is a port the container declares |
deployment.one-writer |
Recreate strategy and an evidence mount at /work/.docket |
configmap.policy-parses |
the embedded policy is read by Docket's own policy loader, and every key the policy documents as an integer arrives as one — a value rendered by a template as a float (6.7108864e+07, which TOML reads as a float) fails the check by name, with the | int64 fix in the message |
networkpolicy.default-deny |
empty ingress, declared policyTypes, DNS and 443 egress |
compose.waits-and-is-read-only |
the verifier waits for the run, mounts evidence read-only, and no ports are published |
kustomization.paths-exist, references.resolve |
every referenced path exists |
dockerfile.is-buildable-by-inspection |
the Dockerfile's sources exist, it installs as a non-root user, it sets a stop signal, and it bakes in no credential |
The checker is itself tested, which is the part that makes it more than decoration:
tests/test_deploy_check.py breaks each invariant on purpose — a privileged pod, a missing probe, an
unpinned image, an undeclared value, a range construct the renderer does not implement, a policy
number that arrives as a float — and asserts that the checker fails with a message naming the
problem.
The renderer is Helm-faithful, and that was verified against Helm. Helm loads values.yaml
through YAML → JSON → map[string]any, so a template receives a float64 for every number, printed
with Go's shortest representation (67108864 → 6.7108864e+07). tools/deploy_check.py reproduces
that conversion and that formatting — pinned against a table produced by running helm template
(Helm v3.16.3) — because a renderer that is almost Helm produces confident text about a document no
cluster would see. Rendering this chart with both (helm template docket deploy/helm/docket and
tools/deploy_check.py --print-rendered — or make helm-fidelity, which is the same comparison as
a command with a named exit code) gives the same five documents, identical field for field; that
is how the integer defect below was found. helm install has still never run — there is no
cluster here — and the chart is still checked by this renderer, because this is the one that refuses
what it does not implement by name.
It needs a YAML reader, and it is built so that not having one is never a pass: pip install pyyaml
(or pip install -e '.[deploy]') if the command reports it is missing. The exit codes separate the
two failures an operator cares about — 1 when a check failed, 2 when the check could not
run at all — and a test pins that distinction by shadowing the import rather than uninstalling
anything. make deploy-check deliberately does not install it for you.
Stated as plainly as the list above, because the difference between "checked" and "ran" is the whole argument of this project:
- Nothing is executed. No image is built, no container starts, no manifest is applied, no cluster
sees these objects.
docker buildandkubectl apply -k deploy/kuberneteshave never run here. - No admission control. Whether your cluster accepts these objects (Pod Security Admission level, a policy engine, a mutating webhook, a missing StorageClass, a NetworkPolicy plugin that is not installed) is unknown to this repository. A cluster without a NetworkPolicy implementation ignores the file silently.
- No cluster, and no
helm install.helm lintandhelm templatehave been run — v3.16.3, 2026-10-15, with the two renders compared document by document (above). Installing or upgrading the chart, watching a scheduler place the pod, and the parts of a Helm release that only exist in a cluster (hooks, ordering,--wait) have not, and cannot here. To keep the rest honest rather than merely unstated, the chart uses a declared subset of the template language (values substitution, single conditionals,rangeover declared lists — see the annotation indeploy/helm/docket/Chart.yaml) so that the checker can render it deterministically and refuse anything outside that subset by name instead of skipping it. If you use a Helm construct the checker does not know, it fails; that is deliberate. - No cluster-scale behaviour. One replica is the only topology that is correct (a run is one
writer to one evidence directory) and the manifests say so; whether your StorageClass gives you
the durability the ledger's
fsyncdeserves is a question about your storage, not about Docket. - No provider is contacted. The example pod answers itself with
docket.fakeupstream, a scripted provider, so that the manifest produces a real receipt offline. Point it at a real provider and the receipt will show that provider's real numbers — and no CI here will ever have called it.
Evidence is the artefact. Put the evidence directory on a volume you keep, back up, and can read
after the pod is gone. latest.receipt.json is written next to the per-run receipts and is the file
to collect; a receipt is self-contained (it embeds the price table and the policy digest), so it
verifies years later without this repository's state.
Readiness is the signal that matters. /docket/readyz turns red when the ledger can no longer be
written or the volume is below min_free_bytes. A run that keeps going with unusable evidence is
precisely the failure this tool exists to prevent, so wire that probe to something that pages a
human, not just to a load balancer.
Stop with SIGTERM, never SIGKILL. docket run forwards the signal to the agent's process group,
reaps the child, and then writes the receipt. SIGKILL leaves a readable prefix and no receipt,
which is recoverable (docket doctor) but is not the same thing as a clean end.
One writer per evidence directory. Two runs pointed at one directory is an evidence defect you can see: the ledger takes an exclusive lock and the second run is refused by name. Do not defeat that by sharing a volume between replicas.
Costs. max_usd is the cap people actually defend against; for subscription-backed agents set
--budget-mode quota and use token, call and wall-clock caps, then read cost_basis in the receipt
to see which basis was in force.
The compose file is a smoke test of the image and the two-container wiring, not a production topology:
$ docker compose -f deploy/compose.yaml up --build --abort-on-container-exitIt starts a scripted provider, runs the reference agent under the real boundary with real caps, and then verifies the receipt from a separate container that mounts the evidence volume read-only. If that sequence completes, the image runs, the wiring works, and the receipt is valid — which is exactly as much as a container can prove, and no more.