BuildContainer Platformsnew
Container Images
The chio repository includes three reference Dockerfiles: the default kernel image, the sidecar HTTP service, and the TEE shadow runner.
Source
deploy/docker/Dockerfile, deploy/docker/Dockerfile.sidecar, deploy/docker/Dockerfile.tee, deploy/sidecar/Dockerfile, and .github/workflows/sidecar-image.yml.Image Overview
| Image | Dockerfile | Binary | Base | Exposed |
|---|---|---|---|---|
| Default kernel image | deploy/docker/Dockerfile | chio | Alpine 3.22 | 8940 (trust-demo target), 8931 (mcp-demo target) |
| Sidecar HTTP service | deploy/docker/Dockerfile.sidecar | chio | Alpine 3.22 + tini | 9090 (--listen default) |
| TEE shadow runner | deploy/docker/Dockerfile.tee | chio-tee | Alpine 3.22 + tini | none (verdict-only by default) |
A fourth Dockerfile, deploy/sidecar/Dockerfile, is the distroless variant of the sidecar image used by the published GHCR image. It shares the same build pattern but ships on gcr.io/distroless/cc-debian12:nonroot (uid 65532) and bakes a multi-arch curl for the HEALTHCHECK probe. See Sidecar HTTP Service for that path.
Default Kernel Image
deploy/docker/Dockerfile builds a Rust binary plus an optional dashboard SPA, packaged as three compose-friendly targets: the bare CLI, a trust-control demo, and an MCP-server demo. The dashboard stage builds the chio-cli/dashboard Vite SPA so the trust-demo target can serve it.
Build
# Build the bare CLI image (default target).
docker build -f deploy/docker/Dockerfile -t chio:cli .
# Build the trust-control demo image (dashboard included).
docker build -f deploy/docker/Dockerfile --target chio-trust-demo -t chio:trust-demo .
# Build the MCP-server demo image.
docker build -f deploy/docker/Dockerfile --target chio-mcp-demo -t chio:mcp-demo .Stages
- rust-builder:
rust:1.93-alpine3.22withbuild-base,cmake,openssl-dev, andpkgconf. Buildscargo build --profile docker-release --locked -p chio-cli --bin chioagainst a dedicateddocker-releasecargo profile. - dashboard-builder:
node:22-alpine. Builds the Vite dashboard for the trust-demo target. - Chio: Alpine 3.22 runtime with
ca-certificates,libgcc, andlibstdc++. DefaultCMDis--help. - chio-trust-demo: extends
chio, copies the dashboarddist/, exposes 8940, and startschio trust servewith a demo service token. - chio-mcp-demo: extends
chio, adds python3 and a mock MCP server, exposes 8931, runschio mcp serve-http.
Env vars (demo targets only)
| Variable | Default | Used by |
|---|---|---|
CHIO_SERVICE_TOKEN | demo-token | trust-demo |
CHIO_CONTROL_URL | http://chio-trust-demo:8940 | mcp-demo |
CHIO_CONTROL_TOKEN | demo-token | mcp-demo |
CHIO_AUTH_TOKEN | demo-token | mcp-demo |
Demo targets are for development
demo-token defaults so a bare docker compose up brings a working demo online. Keep them inside a development network. Deploy sidecars with Dockerfile.sidecar with secrets-backed env vars.Sidecar Image
deploy/docker/Dockerfile.sidecar builds the standard sidecar image. It has two stages: an Alpine Rust builder produces a stripped, musl-linked chio binary; a minimal Alpine runtime ships only that binary plus tini and CA roots. Runs as a non-root chio:chio user (uid/gid 10001) with CHIO_HOME=/var/lib/chio.
Build
docker build -f deploy/docker/Dockerfile.sidecar -t chio-sidecar:local .Stages
- builder:
rust:1.93-alpine3.22(digest-pinned) withbuild-base,cmake,perl,pkgconf,musl-dev,protoc,openssl-dev, andopenssl-libs-static(built withOPENSSL_STATIC=1so the runtime layer gains nolibssl.sodependency — the static OpenSSL comes in viachio-custody-hw→webauthn-rs-core→openssl-sys). Runscargo build --release --locked --package chio-cli --bin chio, strips the binary, copies it to/chio. - runtime:
alpine:3.22(digest-pinned) withca-certificatesandtini. Adds thechiouser, copies the binary to/usr/local/bin/chio, and setsENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/chio"].
Why protoc
chio-envoy-ext-authz, reachable transitively through the workspace, invokes tonic-build at compile time. CI installs protobuf-compiler for the same reason. The vendored Envoy protos consumed by tonic-build live under crates/protocol/chio-envoy-ext-authz/proto, so the existing COPY crates ./crates line covers them; no separate top-level COPY proto is required.
Default CMD
CMD ["--help"]. Both chio run and chio mcp serve-http require --policy plus positional input, so a bare docker run prints usage and exits successfully after printing usage. Override it at deploy time.
Image size
Alpine + musl-linked binary keeps the runtime layer small. The runtime image carries only ca-certificates, tini, and the chio binary itself. Expect a compressed image around 20 to 40 MB depending on the build profile and feature set.
TEE Image
deploy/docker/Dockerfile.tee mirrors the sidecar build pattern but builds the chio-tee binary, the confidential-compute shadow replay runner. It exposes no public port and runs in a verdict-only mode by default.
Build
docker build -f deploy/docker/Dockerfile.tee -t chio-tee:local .Default env vars
| Variable | Default | Purpose |
|---|---|---|
CHIO_HOME | /var/lib/chio | Working directory for spool and persisted state. |
CHIO_TEE_CONFIG | /etc/chio/tee.toml | Path to the TEE config file inside the image. |
CHIO_TEE_MODE | verdict-only | Replay mode: emit only the allow/deny verdict, not the inputs. |
RUST_LOG | info | Tracing log level for the TEE runner. |
The TEE runner reads /run/chio-tee as a runtime socket directory and writes spool data to /var/lib/chio/tee. Both are created and chown'd to chio:chio in the image. See TEE Deployment for the attestation flow and the verdict-only vs full-replay modes.
Build Strategy
Workspace-aware caching
The two builds differ in how much of the tree they copy. The default deploy/docker/Dockerfile uses a pruned manifest: its rust-builder stage copies a separate deploy/docker/chio-workspace/Cargo.toml and Cargo.lock pair plus .cargo/, crates/, one example fixture file, fixtures/proof-room, spec/, and wit/. It does not copy examples/, formal/, tests/, sdks/, bench/, contracts/, integrations/, or xtask/.
Dockerfile.sidecar and Dockerfile.tee instead copy the repository root Cargo.toml / Cargo.lock and the full workspace tree, because those builds resolve every workspace member against the committed lock. The copy list there covers crates/, wit/ (consumed at compile time by chio-wasm-guards through wasmtime::component::bindgen!), contracts/ (embedded by chio-web3-bindings via Alloy's sol! macro), and the workspace-member trees bench/, examples/, formal/, tests/, sdks/, integrations/editors, integrations/ (the aws-bedrock control plane and mcp-adapter members), and xtask/. Their absence breaks manifest loading or Cargo.lock resolution.
BuildKit cache mounts
The distroless variant under deploy/sidecar/Dockerfile uses --mount=type=cache for the cargo registry and the build target directory:
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/build/target \
cargo build --release --locked -p chio-cli --bin chioCombined with cache-from: type=gha in the GitHub Actions workflow, this pulls warm cache layers from previous runs. CI builds drop from the eight-minute cold cargo compile to under two minutes on a warm cache.
--locked is mandatory
Every build invocation passes --locked. The image must compile against the committed Cargo.lock; an image that picks up newer transitive deps at build time defeats reproducibility and the SBOM contract.
Multi-Arch
The published sidecar image is built for linux/amd64 and linux/arm64. The CI workflow uses docker/setup-qemu-action plus docker/setup-buildx-action and passes platforms: linux/amd64,linux/arm64 to the build step.
The distroless variant's healthcheck stage handles multi-arch explicitly: it derives the Debian multiarch directory from dpkg-architecture -qDEB_HOST_MULTIARCH so the curl shared-library copy works unmodified on both x86_64-linux-gnu and aarch64-linux-gnu.
Image Tagging
The published image lives at ghcr.io/backbay-labs/chio-sidecar. The CI workflow emits four tag families through docker/metadata-action:
| Tag | Trigger | Example |
|---|---|---|
| Branch | Push to a branch | main |
| Semver full | Tag push vX.Y.Z | 0.4.2 |
| Semver minor | Tag push vX.Y.Z | 0.4 |
| Short SHA | Every push | sha-1a2b3c4 |
latest | Default branch only | latest |
latest does not track tag pushes
:latest on tag pushes. Tagging an older patch (e.g. v0.1.1) after a newer minor (e.g. v0.2.0) would otherwise regress :latest. Versioned tags still ship via the semver patterns; pin to those for production.Supply-Chain Signing
Every published sidecar manifest is keyless-signed with cosign through Sigstore Fulcio. The workflow exchanges its OIDC token for a short-lived Fulcio cert and signs the manifest by digest, which covers every tag pointing at the same content-addressed manifest.
# Workflow excerpt: sign the digest, not the tag.
ref="${IMAGE_NAME}@${IMAGE_DIGEST}"
cosign sign --yes "${ref}"Verification on the consumer side:
cosign verify ghcr.io/backbay-labs/chio-sidecar:latest \
--certificate-identity-regexp 'https://github\.com/backbay/chio/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.comThe build also emits a SLSA provenance attestation (provenance: mode=max) and an SBOM (sbom: true), both attached to the image manifest by docker/build-push-action. Workspace advisory hygiene is enforced separately at CI time through cargo-deny; see deny.toml for the active advisories and the migration plan for any current ignores.
Runtime Restrictions
The published images are designed to slot into hardened orchestrator manifests without surprises.
Non-root by default
- Alpine sidecar:
USER chio:chio(uid / gid 10001). - Alpine TEE: same
chio:chiouser. - Distroless sidecar:
USER 65532:65532(distroless nonroot).
Read-only root filesystem
The reference ECS task definition sets readonlyRootFilesystem: true and the non-root user user: "65532:65532". Reverting either knob is a regression. The kernel writes only to /var/lib/chio and a tmpfs for in-flight buffers.
Linux capability drops
The chio binary needs none of CAP_SYS_ADMIN, CAP_NET_RAW, or CAP_SYS_PTRACE. Drop all capabilities and re-add only CAP_NET_BIND_SERVICE when binding below port 1024. The default sidecar listen address is 9090, so even that is unneeded in the standard manifest.
Distroless considerations
deploy/sidecar/Dockerfile ships on gcr.io/distroless/cc-debian12:nonroot because the kernel binary is dynamically linked against libgcc_s on glibc. Distroless has no shell, so the Dockerfile bakes a static-relocated curl for the HEALTHCHECK probe; an exec-form CMD is used because there is no /bin/sh to run shell-form directives.
No exec into a distroless container
docker exec -it ... sh. Debug by reproducing on the Alpine variant, or by attaching a tracing sidecar that reads the kernel's structured logs.Next Steps
- Sidecar HTTP Service for the runtime config (env vars, listen address, health endpoints, supervision).
- TEE Deployment for the chio-tee image specifics (verdict-only mode, attestation flow, spool directories).
- Cloud Run for the Knative-style multi-container manifest.
- ECS Fargate for the task-definition manifest with
dependsOnhealth gating. - Azure Container Apps for the Bicep template with startup and readiness probes.