Skip to main content
What has and has not been run. The Dockerfile, the Compose file and the Helm chart were written and reviewed, but not built, started or installed in the project’s own test pass. In particular the Helm chart has not been installed on a real cluster, and helm was not available on the machine that wrote it, so the chart has not even been helm linted or rendered. Treat all three as a starting point, run helm lint and helm template yourself, and try them in a non-production environment first.

Docker

The repository’s Dockerfile builds one image with the server (default command), the gateway and the CLI. It is a two-stage build on node:22-slim (npm ci, tsc -b, then dev dependencies are pruned).
  • Runs as the unprivileged node user, keeps data in the /data volume (REVERSI_DATA=/data), and has a health check on /readyz.
  • It sets HOST=0.0.0.0 and PORT=7421. Because of that, REVERSI_CONSOLE=open is refused (open mode is loopback only); use REVERSI_CONSOLE=login.
  • A gateway that listens beyond loopback needs auth in its config, or it refuses to start.

Docker Compose

deploy/docker-compose.yml runs the server on PostgreSQL 16 with a Postgres health check, JSON logs, and the login console at http://127.0.0.1:7421/console (paste an API key). Follow the comments at its top:
The stack mounts deploy/keys.json, examples/gateway/manifests and examples/gateway/policy.yaml read-only, publishes only on 127.0.0.1, and requires POSTGRES_PASSWORD (it fails without it). Replace the example manifests and policy with your own.

Kubernetes (Helm)

The chart is at deploy/helm/reversi. It deploys the server, and optionally the gateway as a second container in the same pod.
Build and push the image yourself and set image.repository and image.tag. Nothing is created for secrets: you create the Secrets and ConfigMaps and the chart refers to them by name, so no credential ever sits in values.yaml.
my-values.yaml

Values

Fail-fast checks

templates/validate.yaml stops the render with a message for:
  • server.console=open: “has no login and is refused on a cluster”.
  • replicaCount above 1 with a store other than postgres.
  • registry.persistence.enabled with replicaCount above 1: “the volume is ReadWriteOnce”.
  • server.console=login with neither keys.existingSecret nor oidc.enabled.
The templates also require store.databaseUrlSecret.name when the store is postgres (the default), and gateway.existingConfigMap when the gateway is enabled.

What the chart does and does not do

  • Pod security: non-root user 1000, read-only root filesystem (writable /data and /tmp), all capabilities dropped, RuntimeDefault seccomp, no service account token mounted. Readiness probe /readyz, liveness /healthz.
  • The server refuses to start without a credential source. The chart does not check that you set keys.existingSecret or oidc.enabled unless the console is login.
  • The gateway container shares the pod’s environment (store, manifests, policy, keys) and /data. Your gateway.yaml must listen on 0.0.0.0 and the port set in gateway.port, and use auth.keys: /config/keys/keys.json (or OIDC), since a gateway beyond loopback needs authentication. The Service exposes the mcp port inside the cluster; the Ingress routes only to the server.
  • Read-only by default. With the ConfigMap sources, manifests and policy are read-only mounts, so the console’s editors and reversi apply --server have nothing writable to work on. Update the ConfigMaps (helm upgrade) and let hot reload pick them up. I did not verify how quickly Kubernetes propagates a ConfigMap change into the pod or that the file watch sees it, so run kill -HUP or restart if it does not.
  • Writable registry (registry.persistence.enabled: true). The console editors and reversi apply --server then work, because the server’s manifests and policy are folders on the volume. Set server.console: login and use an admin key for apply. An init container creates /state/manifests and /state/policy, and the server and gateway treat an empty folder as an editable registry, so the first publish (for example reversi apply <dir> --server <url> --token <admin key>) creates it. The /state mount is in both the server and the gateway container. Checked locally with an empty folder and apply --server; the init container and mounts were read from the templates, not run on a cluster.
  • There is no Terraform module and no operator. See GitOps.

Server environment

Everything can also be set in reversi.config.yaml; the variable wins. At least one credential source (keys or OIDC) is required: the server will not start unauthenticated. node packages/server/dist/main.js --help prints the list.

TLS and networking

Both programs speak plain HTTP. Terminate TLS in a reverse proxy (nginx, Caddy, a cloud load balancer) and set REVERSI_SECURE_COOKIES=1. Keep the server and gateway on a private network; the agent only needs to reach the gateway or the agent endpoints. Set allowed_hosts on the gateway to your public hostname.

High availability

One engine process per run is the supported model. With PostgreSQL you can run several server or gateway instances, each owning its own sessions and runs, behind a load balancer with session affinity for MCP sessions (the Mcp-Session-Id header). Ledger appends from all of them stay on one chain. Active/active drivers of the same run are not supported, and there is no clustering with run hand-off.

Upgrades

Deploy the new version: PostgreSQL migrations run at start. Back up first (storage). Manifests, the policy and the manifest public key reload while running (file watch, SIGHUP, POST /v1/reload); other settings and environment variables need a restart. Key files reload live.

Incident runbook