Skip to main content
This guide deploys the ghcr.io/withcoral/coral and ghcr.io/withcoral/coral-ui images on Kubernetes with authentication enabled and TLS terminated at the Ingress. It assumes you have read Self-host Coral with Docker, which explains the listeners, auth modes, and image behavior that this guide builds on. TLS termination is mandatory: every Coral listener is cleartext, and with [auth] enabled bearer tokens would otherwise cross the wire unprotected. A Kubernetes deployment is remote by definition, so this guide does not use CORAL_UI_AUTH_MODE=disabled; if you need a login-less lab setup, use the local Docker Compose topology instead. The example uses the same three hostnames as the Compose guide: coral.example.com (Coral UI), auth.coral.example.com (OAuth issuer), and mcp.coral.example.com (MCP).

Prerequisites

  • A cluster with an Ingress controller (the manifests use ingress-nginx annotations) and cert-manager or another way to provision TLS certificates that chain to system roots.
  • DNS records for the three hostnames pointing at the Ingress.
  • An OIDC client registered with your identity provider, with redirect URI https://auth.coral.example.com/auth/oidc/callback.
  • A default StorageClass for the Coral state volume.
During login, the Coral server fetches https://coral.example.com/.well-known/oauth-client and Coral UI fetches the issuer’s discovery and token endpoints. From inside the cluster both coral.example.com and auth.coral.example.com must resolve to a public address (Coral rejects client-metadata hosts that resolve to private or loopback addresses), and the certificates must chain to system CA roots — Let’s Encrypt works, a private CA does not.

Namespace and secrets

Coral server

The server is stateful — SQLite, workspace and source state, and credential material all live on one volume — so it runs as a single replica with the Recreate strategy. This stays true with a Postgres backend, which moves the database but not the workspace files. The seed config below is applied on the first boot only; an existing config.toml on the volume is never rewritten. See changing the configuration. Save the following as coral.yaml, replacing hostnames and the [auth.provider] issuer and client ID with yours:
The image runs as user 65532 with group 0; fsGroup: 65532 makes the volume writable for it. The probes use the gRPC health service directly: service "" is liveness (always serving while the process runs) and coral.readiness reports whether the engine has resolved its catalog.

Coral UI

Coral UI is stateless — sessions live in encrypted cookies — so you can raise replicas beyond the example’s single replica as needed; no sticky sessions are required. It talks native gRPC to the coral Service inside the cluster; CORAL_UI_ALLOW_INSECURE_CORAL_ENDPOINT=1 is the explicit opt-in for that cleartext in-cluster leg. Save as coral-ui.yaml, replacing the two https:// hostnames with yours:

Ingress

One Ingress terminates TLS for all three hostnames. MCP Streamable HTTP holds long-lived streaming responses, so response buffering is disabled and the read timeout raised. Save as ingress.yaml, replacing the hostnames and the cert-manager.io/cluster-issuer annotation with your issuer’s name:
The OAuth listener has no health endpoint of its own; the Deployment’s gRPC probes cover the whole server process, and GET https://auth.coral.example.com/.well-known/oauth-authorization-server serves as an external check if you monitor each hostname.

Deploy and verify

Then verify the public surfaces exactly as in the Compose guide: https://coral.example.com/readyz returns ready, https://mcp.coral.example.com/mcp returns 401 with a WWW-Authenticate challenge, and opening https://coral.example.com signs you in through your identity provider — without Coral’s approval page, since Coral UI’s client ID is listed in trusted_clients.

Operations

Administration

Day-to-day administration happens in the Coral UI. For the rest, run the CLI inside the pod — it shares the server’s state safely:
The workstation CLI cannot target a remote server; CLI commands always operate on local state.

Changing the configuration later

The seed config applies only on first boot. To change configuration afterwards, edit the file on the volume and restart:
Unlike docker compose cp, kubectl cp writes through tar running inside the container as the container user, so file ownership stays correct. Updating the ConfigMap alone does nothing once config.toml exists.

Backup and upgrades

Back up the coral-data PersistentVolumeClaim; it holds everything the server owns. For upgrades, pin matching image versions for coral and coral-ui (release tags of the two images move together) and roll the Deployments: