# Kubernetes

> The production deployment, a Helm chart with separate services behind network policies and every run in its own sandboxed pod.

- **Best for:** Larger teams and organisations
- **Setup:** A cluster, Postgres and object storage, then one helm install
- **Runs:** One pod per run, ideally in a gVisor sandbox
- **Isolation:** Network policies around every service and every run
- **Data:** Your Postgres and object storage
- **Scale:** Your cluster, with room for several deployments

[View Helm chart](https://github.com/ryph-hq/ryph/tree/main/deploy/helm/ryph)

On Kubernetes, each Ryph service runs separately, and every agent run gets its own pod in a locked-down namespace. It's the deployment for teams and organisations running Ryph for real.

Ryph installs with the Helm chart at `deploy/helm/ryph` in the Ryph repository. Its `README.md` is the full reference: requirements, the values, the namespace model, the network rules and the Secrets each service reads.

## What you get

What runs where, on your cluster:

- **Ryph services** (locked down by default): Each port open only to its callers, and each service holds only its own secrets. Holds Web app ×2, Control plane, Forge, Historian, Model proxy, MCP gateway, Memory, Chat, Automations, Egress proxy.
- **Runs** (locked down by default): One pod per run, in gVisor where the cluster offers it. Holds Run, Run, Run.
- **Environment builds** (locked down by default): Jobs in a separate namespace. Holds Build, Build.
- **Tool servers** (locked down by default): Hosted MCP servers, behind a tool guard. Holds Tool guard, MCP server.
- **Alongside:** PostgreSQL, Object storage

Every service and every run sits behind network policies. A run can reach only DNS, the five Ryph endpoints it needs and the egress proxy.

**Runs kept apart.** Each run is a Job in a namespace used only for runs, and its pod has no Kubernetes token and no cloud identity. With a sandboxing runtime class such as gVisor, each run gets its own kernel. A run can reach the cluster's DNS, the five Ryph endpoints it needs, the previews tunnel when previews are on, and the egress proxy, which lets it through only to the hostnames it was allowed. The database, the nodes, the cloud metadata server, the Kubernetes API and every other service are out of reach.

**Services kept apart.** Network policies close every service port to everything but its callers. The model and tool gateways each split into two endpoints, one for runs and one for managing keys, and runs can reach only the first. Services that fetch addresses from the outside world can't use them to reach anything inside the cluster, and the artifact thumbnail renderer can reach nothing at all.

**Secrets split by service.** Each service reads a Secret holding only what it uses. Model provider keys are sealed inside the model proxy and tool credentials inside the MCP gateway. The control plane can open neither. Bring your own Secrets, from External Secrets or Sealed Secrets, or let the chart create them.

**Work on the cluster.** Environment builds run as Jobs in a separate namespace, and the checks Ryph runs on pull requests run on the cluster too. Hosted MCP servers run in their own namespace behind a tool guard.

**Room to grow.** Runs are pods, so they scale with the cluster's nodes. The web app runs two replicas and rolls out without dropping requests. Every namespace name comes from one prefix, so several releases can share a cluster, each with its own namespaces, databases and hostname.

## What you need

- Kubernetes 1.29 or newer, with a network plugin that enforces NetworkPolicy, such as Calico, Cilium, GKE Dataplane V2 or kind 0.24 and newer.
- Ideally a sandboxing runtime class for runs, such as gVisor.
- PostgreSQL 15 or newer.
- An S3-compatible bucket for patches.
- A hostname with TLS for the web app, behind an Ingress or a Gateway API route.
- A GitHub App for each deployment, if you connect GitHub repositories.
- A GitLab bot account and its token, if you connect GitLab projects.
- A container registry, if you turn on [environment builds](#environment-builds).

> [!WARNING]
> **NetworkPolicy isn't optional**
>
> The boundary between a run and everything else is drawn by network policies. A plugin that ignores them, such as Flannel alone or Docker Desktop's built-in Kubernetes, leaves every run able to reach the database, and nothing will tell you.

## Install

The chart README walks through it:

1. **Create Postgres and a bucket**

   Set up the Postgres server and the object storage bucket. The chart creates the databases it can on that server.

2. **Write a values file**

   Give it the app's address, the database, the bucket, the runtime class and the front door.

3. **Install**

   Run `helm install`. Each service applies its own migrations before it starts, so there's no separate schema step.

4. **Check**

   Run `k8s:smoke`. It checks the app and the services behind it, then probes what a pod with a run's labels can and can't reach.

On GKE Autopilot, the chart README lists the extra values: private Service ranges, the node-local DNS cache, GKE Sandbox for gVisor and the load balancer's policies.

To try it before a real cluster, the dev values install on kind in about 3 GiB, with Postgres and object storage inside the cluster. Use kind 0.26 or later, whose node image is Kubernetes 1.32. On an older one, set `previews.capSocketBuffers=false` or the previews pods are refused.

## Claim it

The first account registered becomes the only owner, and it needs the setup token from the control plane's log. Claim it through a `kubectl port-forward` to the web app before the hostname goes live, so nobody who finds the new hostname can claim it first.

> [!NOTE]
> **Add a forge**
>
> Without a GitHub App, the deployment runs but can't connect GitHub repositories. Each deployment needs its own App, because an App has one webhook address. GitLab, on gitlab.com or your own instance, is set up under `gitlab` in the values, with a bot account at the Developer role. Radicle is an optional component. You can use any of them, or several.

## Environment builds

A plain install pulls Ryph's images from `quay.io/ryph` and needs no registry. Every run uses the stock agent image.

With [environments](https://ryph.ai/docs/environments.md) turned on (`environments.enabled: true`), Ryph builds each repo's image on the cluster, pushes it to your registry and starts that repo's runs from it. Set two values:

- `environments.registry` for default-branch builds and their layer caches.
- `environments.branchRegistry` for builds of other branches. Branch builds get read-only access to the default-branch registry.

The chart doesn't come with a registry, and the control plane won't start with builds on and no registry set.

> [!WARNING]
> **Registry login works on GKE only**
>
> On GKE, builds push to Artifact Registry with their Workload Identity. Ryph does this for any `*.pkg.dev` or `gcr.io` address. On other clusters, builds push without credentials, so the registry has to accept anonymous pushes. A registry reachable only from inside the cluster works.
>
> `image.pullSecrets` apply to the Ryph services only. Run and build pods get no pull secret, so the nodes need pull access to the registry themselves.
>
> Ryph cleans up old environment images on Artifact Registry. On other registries, set up the registry's garbage collection to clear them out.

## Upgrades

Run `helm upgrade` with the same values file. Each new pod applies only the migrations its database hasn't had yet, one replica at a time, while the old pods keep serving. Automations, Radicle and previews each run as one pod that's replaced, so they're down for the moment between the old pod stopping and the new one starting. To back out, `helm rollback` starts the previous images again; it leaves applied migrations in place.

Keep the keys that seal provider keys, tool credentials and chat bot tokens backed up, ideally in an external secret store. A new key can't read anything sealed with the old one, and the chart refuses to generate one over a deployment that already has it.
