Sidecar HTTP Service
The sidecar pattern runs chio as its own process and image. The host application talks to the kernel over local HTTP at localhost:9090, and the signing key never enters the host's address space. This is the canonical production posture: it gives you an independent trust boundary, independent rollout cadence, and language portability for the host.
When to read this page
Why Sidecar
- Independent trust boundary. The Ed25519 signing key is mounted into the sidecar container and is unreachable from the host container. A compromised host cannot forge receipts.
- Independent scaling. Roll the kernel forward without redeploying the host. Useful when kernel and host are owned by different teams.
- Language portability. The host can be any language that speaks HTTP. The same sidecar image serves Rust, Python, Go, Node, .NET, and Java hosts.
- First-class platform support. Cloud Run, ECS Fargate, and Azure Container Apps all support multi-container deployments with localhost networking and startup ordering.
The cost is a localhost-HTTP hop on every kernel call (about 100 microseconds per call). Negligible for tool-call workloads, noticeable on hot inner loops.
The Sidecar Image
Two reference Dockerfiles ship in the chio repository:
| Path | Base | Use |
|---|---|---|
Dockerfile.sidecar | Alpine 3.22 + tini | Default sidecar image. Small, musl-linked, runs as uid/gid 10001. |
deploy/sidecar/Dockerfile | distroless cc-debian12 nonroot (uid 65532) | Distroless variant with a baked-in curl for HEALTHCHECK and a multi-arch shared-library layout. |
Both build the same chio binary from chio-cli; the difference is the runtime layer. For a deeper Dockerfile-by-Dockerfile breakdown, see Container Images.
Build
From the chio repository root:
docker build -f Dockerfile.sidecar -t chio-sidecar:local .The builder stage installs protoc because chio-envoy-ext-authz (reachable transitively through the workspace) invokes tonic-build at compile time. CI installs protobuf-compiler for the same reason. Keeping the Docker build consistent with CI prevents a future dependency change from silently breaking the image.
The build copies the full workspace so path dependencies resolve. Specifically:
wit/is consumed bychio-wasm-guardsviawasmtime::component::bindgen!(reached through thechio-cli -> chio-wasm-guardspath dependency).examples/,formal/, andtests/are workspace members; their absence breaksCargo.lockresolution.sdks/is copied for forward-compatibility with non-workspace SDKs that may join the root workspace.
For the distroless variant:
docker build -f deploy/sidecar/Dockerfile -t ghcr.io/backbay-labs/chio-sidecar:latest .That command is the canonical build invocation in the sidecar deploy manifests. deploy/sidecar/Dockerfile adds a stage that bakes curl plus its shared libraries into the runtime image so the distroless layer can run a HEALTHCHECK probe without a shell.
Run
The image's zero-argument default is --help. Every chio subcommand needs operator input (policy path, wrapped server command, etc.), so a bare docker run prints usage rather than crashing. Operators always override CMD at deploy time.
docker run --rm -p 8939:8939 chio-sidecar:local <subcommand> [args...]The two production-grade subcommands are chio api protect (reverse proxy in front of an OpenAPI host) and chio mcp serve-http (HTTP-fronted MCP tool server). A typical api protect invocation:
chio mcp serve-http \
--policy /etc/chio/policy.yaml \
--server-id my-toolNo useful zero-arg default
chio run and chio mcp serve-http require --policy plus additional positional input. The image falls through to --help on a bare invocation rather than exiting non-zero before the health endpoint opens.Listen Address
The sidecar listens on 0.0.0.0:9090 by default and exposes GET /chio/health. Both values are baked into deploy/sidecar/Dockerfile as defaults:
ENV CHIO_LISTEN_ADDR=0.0.0.0:9090 \
CHIO_HEALTH_PATH=/chio/health \
CHIO_LOG_LEVEL=info \
CHIO_KERNEL_CONFIG_PATH=/etc/chio/kernel.yamlOverride at deploy time by setting the env vars in the orchestrator manifest. The application container reaches the kernel through http://localhost:9090 because pod / task / revision containers share the network namespace.
Health Endpoints
The sidecar exposes a single health path, GET /chio/health by default. The path is governed by CHIO_HEALTH_PATH.
- Liveness: 200 means the process is up and the kernel is constructed.
- Readiness: 200 means the kernel has loaded its policy and the receipt sink is reachable. A failing readiness check causes the orchestrator to keep traffic off this replica.
- Fail closed: if the kernel cannot load
CHIO_KERNEL_CONFIG_PATHor the policy bundle at startup, the process exits non-zero. The orchestrator marks the container unhealthy (ECS, Azure) or fails the revision (Cloud Run).
The distroless sidecar bakes in a HEALTHCHECK that hits the same path:
HEALTHCHECK --interval=10s --timeout=5s --start-period=15s --retries=3 \
CMD ["/usr/bin/curl", "-fsS", "http://localhost:9090/chio/health"]Environment Variables
The sidecar reads a small, fixed set of environment variables. Two of them have defaults baked into the image; the rest must be provided at deploy time.
| Variable | Default | Purpose |
|---|---|---|
CHIO_LISTEN_ADDR | 0.0.0.0:9090 | Bind address for the sidecar HTTP listener. |
CHIO_HEALTH_PATH | /chio/health | Path the orchestrator probes for liveness and readiness. |
CHIO_KERNEL_CONFIG_PATH | /etc/chio/kernel.yaml | Path to the kernel YAML config inside the container. |
CHIO_POLICY_SOURCE | (none) | Policy bundle URL (gs://, s3://, https://, or a bundled path). |
CHIO_RECEIPT_SINK | (none) | Receipt destination (BigQuery, DynamoDB, Cosmos DB, stdout, etc.). |
CHIO_SIGNING_KEY | (none) | Ed25519 signing seed for receipts. MUST come from a managed secret backend. |
CHIO_CAPABILITY_AUTHORITY_URL | (none) | URL of the capability authority issuing tokens. |
CHIO_LOG_LEVEL | info | Tracing log level (info / warn / error). |
CHIO_TRUSTED_ISSUER_KEY | (none) | Single trusted issuer public key (hex-encoded Ed25519). Read by chio api protect. |
CHIO_TRUSTED_ISSUER_KEYS | (none) | Multiple trusted issuer keys, comma-separated. Read alongside CHIO_TRUSTED_ISSUER_KEY; the union is used. |
CHIO_SIDECAR_CONTROL_TOKEN | (none) | Bearer token for the sidecar admin surface. Falls back to CHIO_API_PROTECT_CONTROL_TOKEN. |
Verified against crates/chio-cli/src/cli/runtime.rs
CHIO_TRUSTED_ISSUER_KEY, CHIO_TRUSTED_ISSUER_KEYS, and CHIO_SIDECAR_CONTROL_TOKEN are read directly by chio api protect at startup. CHIO_API_PROTECT_CONTROL_TOKEN is the legacy alias.Wire Protocol
The sidecar exposes the kernel surface over HTTP. Hosts call endpoints for capability validation, tool dispatch, and receipt retrieval; the wire format is documented in Wire Protocol. Every request is serialized as JSON; binary payloads are base64-encoded.
For the chio api protect mode the sidecar is itself the front door: it terminates the inbound request, validates capabilities, runs guards, forwards to the upstream host, and signs a receipt on the way out. The host never sees the original capability token.
Graceful Shutdown
On SIGTERM the sidecar runs a drain sequence:
- Stop accepting new connections on the listen address.
- Let in-flight requests finish. A request that has already passed the guard pipeline is allowed to complete and emit a receipt.
- Flush the receipt log to the configured sink (
CHIO_RECEIPT_SINK). - Drain the signing task and emit a final checkpoint if a checkpoint boundary is crossed.
- Exit 0.
tini is PID 1 in both Dockerfiles, so signals reach the chio binary correctly. If the drain takes longer than the orchestrator's grace period, the platform escalates to SIGKILL; in-flight receipts buffered in memory at that point may be lost.
Set a grace period that fits the receipt sink
Process Supervision
systemd
For bare-metal or VM hosts, run the sidecar under systemd with a unit that pins the binary, mounts the secret directory, and restarts on failure.
[Unit]
Description=Chio sidecar HTTP service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=chio
Group=chio
ExecStart=/usr/local/bin/chio mcp serve-http \
--policy /etc/chio/policy.yaml \
--server-id my-tool \
--listen 0.0.0.0:9090
Environment=CHIO_KERNEL_CONFIG_PATH=/etc/chio/kernel.yaml
Environment=CHIO_LOG_LEVEL=info
EnvironmentFile=/run/secrets/chio.env
Restart=on-failure
RestartSec=2s
TimeoutStopSec=30s
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/chio
[Install]
WantedBy=multi-user.targetContainer orchestrators
For multi-container platforms, the sidecar ships next to the host and the platform handles startup ordering and health gating. The orchestrator-specific manifests live under deploy/:
- Cloud Run uses
run.googleapis.com/container-dependenciesplus anhttpGetstartup probe. - ECS Fargate uses
dependsOnwithcondition: HEALTHYand a curl-basedhealthCheck. - Azure Container Apps uses Bicep multi-container probe sequencing.
Worked Example
Run the sidecar locally, send a request, observe the receipt.
# 1. Build the image from the chio repo root.
$ docker build -f Dockerfile.sidecar -t chio-sidecar:local .
# 2. Run the sidecar with a local policy and a stdout receipt sink.
$ docker run --rm \
-p 9090:9090 \
-v "$(pwd)/policy.yaml:/etc/chio/policy.yaml:ro" \
-v "$(pwd)/kernel.yaml:/etc/chio/kernel.yaml:ro" \
-e CHIO_KERNEL_CONFIG_PATH=/etc/chio/kernel.yaml \
-e CHIO_RECEIPT_SINK=stdout \
-e CHIO_SIGNING_KEY="$(cat ./secrets/signing-seed.hex)" \
chio-sidecar:local \
mcp serve-http \
--policy /etc/chio/policy.yaml \
--server-id local-mock \
--server-name "Local Mock" \
--listen 0.0.0.0:9090
# 3. From another shell: hit the health endpoint.
$ curl -fsS http://localhost:9090/chio/health
{"status":"ok"}
# 4. Issue a tool call (shape depends on the wrapped server).
$ curl -fsS -X POST http://localhost:9090/invoke \
-H "Authorization: Bearer $CAPABILITY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"tool":"echo","arguments":{"message":"hello"}}'
# 5. Watch the receipt arrive in the stdout sink.
$ docker logs <container> 2>&1 | grep '"kind":"chio_receipt"'For an end-to-end example with a real upstream, see Hello Tool.
Sidecar vs In-Process
| Property | In-Process | Sidecar |
|---|---|---|
| Trust boundary | Host process holds the signing key | Sidecar container holds the signing key |
| Latency per kernel call | No IPC. Function call cost. | Localhost HTTP. About 100 microseconds. |
| Independent scaling | No. Kernel scales with the host. | Yes. Kernel and host scale separately. |
| Hot-reload policy | No. Restart the host. | Yes. Roll the sidecar revision. |
| Host language | Rust only | Any language with an HTTP client |
| Recommended for | Trusted single-tenant binaries | Untrusted code, multi-tenant, third-party agents |
Next Steps
- In-Process Library for the alternative when the host is fully trusted.
- Container Images for a Dockerfile-by-Dockerfile breakdown including the TEE variant.
- Cloud Run, ECS, and Azure for the platform-specific manifests.
- Deployment Topologies for the trust-boundary discussion.
- Config Reference for
kernel.yamlfield definitions.