Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Containerized hub

The wrkspc hub role ships as a container image built from source by the repository’s Dockerfile. The hub is a passive mTLS acceptor with no harness control, so the image is minimal and non-root.

Build

docker build -t wrkspc-hub:local .

The build compiles the workspace inside a rust:1.99.1-slim-bookworm stage (matching rust-toolchain.toml, --locked, path-remapped) and copies the single binary into a distroless runtime (cc-debian12, digest-pinned): the binary links glibc from the runtime image — no shell, no package manager, no utilities — running as the distroless nonroot user (uid 65532).

Published image (GHCR)

Release builds publish a prebuilt-binary image to the GitHub Container Registry as ghcr.io/<owner>/wrkspc-hub — the owner segment follows the repository, so an org transfer moves the published package with it. Tags: vX.Y.Z (and latest for the most recent release), multi-arch (linux/amd64, linux/arm64), cosign keyless-signed against the GitHub Actions OIDC identity.

docker pull ghcr.io/beardedeagle/wrkspc-hub:latest

The published image is assembled by .github/workflows/release.yml from the same just release-tarball binaries the signed release ships — the registry image never recompiles the workspace in CI. deploy/Dockerfile.publish is that thin wrapper (binary + the digest-pinned distroless runtime); the root Dockerfile (from source) remains the reproducibility definition and the local just docker-build path. Everything in this README — mounts, ports, TLS posture, observability — applies to the published image unchanged.

Run

The container expects four mounts, mirroring the wrkspc HLD’s D-6 hosting record:

MountPurpose
/data (writable)the hub database (wrkspc-hub.db + WAL + self-generated UI token)
/tls (read-only)server.crt, server.key — the hub’s serving chain + key
/ca (read-only)ca.pem — the CA root(s) client certificates must chain to
/ops (read-only, optional)revoke.txt — operator-managed serial deny-list

Minimal run (plain pilot mode, no TLS — the published port is plain HTTP, for loopback-scale testing only):

docker run --rm \
  -p 127.0.0.1:8082:8082 \
  -p 127.0.0.1:8083:8083 \
  -v wrkspc-hub-data:/data \
  wrkspc-hub:local \
  hub run --db /data/wrkspc-hub.db --a2a 0.0.0.0:8082 --ui 0.0.0.0:8083

Full mTLS run (the process serves A2A on container port 8082; the host port is the externally advertised 8443):

docker run --rm \
  -p 8443:8082 \
  -p 127.0.0.1:8083:8083 \
  -v wrkspc-hub-data:/data \
  -v ./tls:/tls:ro -v ./ca:/ca:ro -v ./ops:/ops:ro \
  wrkspc-hub:local \
  hub run --db /data/wrkspc-hub.db \
    --a2a 0.0.0.0:8082 --ui 0.0.0.0:8083 \
    --tls-cert /tls/server.crt --tls-key /tls/server.key --tls-ca /ca/ca.pem \
    --revoke-list /ops/revoke.txt \
    --card-url https://your-hub-fqdn:8443

The health probe below talks to container port 8083; both examples publish it on loopback so the probe works from the host.

The hub terminates TLS itself (hub-native rustls); the client certificate must reach the accept loop end-to-end, so do not place a terminating proxy in front of it.

deploy/docker-compose.example.yml shows the same shape declaratively, hardened: cap_drop: [ALL], no-new-privileges, read-only rootfs (only /data writable), tmpfs /tmp. The lab’s docker-lab deployment keeps its own compose overlay (lab mounts and PKI paths) and may prebuild the binary with cargo-zigbuild for faster iteration; the image this Dockerfile produces is identical in behavior.

Ports

All three ports are configurable; nothing is baked into the image:

VariableDefaultMeaning
HUB_A2A_PORT8082the container-internal A2A/TLS listener (--a2a); in TLS mode this listener serves the mTLS surface and the operator UI on the same port (path-routed)
HUB_UI_PORT8083the optional plaintext operator UI listener (--ui), for local use
HUB_PUBLIC_PORT8443the host port published for the TLS surface

In TLS mode one published port carries everything: /.well-known/*, /jsonrpc, /peers/*, /presence, /roster, and the delivery routes require a client certificate (the hub requests one but does not require it at the handshake; A2A routes reject connections that presented none), while / and /api/* serve the operator UI behind the bearer token for the browser. The separate --ui listener stays available for a loopback-only operator surface.

The hub CLI accepts the same values as flags (--a2a, --ui, and --card-url for the externally visible URL), so non-compose deployments configure them the same way. EXPOSE lines in the image are metadata only. The card URL must match the published port, or A2A clients that read the card will dial the wrong address.

Observability

The distroless runtime has no shell, so there is nothing to exec into. Inspect with docker logs, and run the liveness probe from the host:

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8083/api/health

The endpoint answers 401 without a token, which still proves the process is serving — treat any HTTP response as healthy and a connection failure as unhealthy. The lab’s compose overlay defines its healthcheck the same way from the host side.

Standard OS paths (non-container deploys)

The container mounts map onto standard OS paths for a bare-metal or VM deploy of wrkspc hub (systemd-managed on Linux):

Container mountStandard pathContent
/data/var/lib/wrkspc/wrkspc-hub.db + WAL + the self-generated <db>.ui-token (service-owned, 0700)
/tls/etc/wrkspc/tls/server.crt, server.key (root-owned, 0640/0600, readable by the service user)
/ca/etc/wrkspc/ca/ca.pem — the client-certificate trust root
/ops/etc/wrkspc/ops/revoke.txt — operator-managed serial deny-list

The service user must be able to read everything under /etc/wrkspc that the hub opens (tls/ca/ops) and write only /var/lib/wrkspc.

Reverse proxies

The hub terminates TLS itself and its A2A surface relies on the client-certificate identity stamp, so it must not sit behind a TLS-terminating proxy (Caddy, nginx, ALB) — header forwarding breaks cert-identity attribution. Where a load balancer is in front, use L4/TLS passthrough (e.g. AWS NLB with TCP listeners).

Cross-building release binaries

Release binaries for the hub image can be built from macOS with just build-matrix release linux — the recipe pins the toolchain and --locked itself. The raw equivalent:

RUSTUP_TOOLCHAIN=1.99.1 cargo zigbuild --locked -p wrkspc-cli --bin wrkspc \
  --target x86_64-unknown-linux-gnu --release

The explicit RUSTUP_TOOLCHAIN matters: mise’s rust shim can resolve a different toolchain than the repo’s rust-toolchain.toml in background shells, which fails with E0463. The repo’s just build-matrix <level> <target> recipes set it for you (see the Justfile).