Skip to content

Deployment

Networked Compose

Every Ryph service in its own container on one machine, in the shape of the Kubernetes deployment.

Best for
Trying the split services before Kubernetes
Setup
One compose file, a dozen services and their secrets
Runs
Containers on the machine’s Docker, started by a launcher
Isolation
Runs confined behind a gate. Services share one flat network.
Data
Postgres with one database per service, and object storage
Scale
One machine
View Compose file

This mode runs each Ryph service in its own container, with its own secrets and its own database, on one machine. It’s the closest shape to Kubernetes that runs without a cluster.

Use it to run the services separately on your own hardware, or as a staging step before you move to Kubernetes. To self-host on a single server, Docker Compose is simpler, with four containers.

The files are in deploy/compose-networked/ in the Ryph repository.

What you need

  • Docker Engine 28 or later, so runs can be confined to their own network.
  • Docker Compose 2.23.1 or later. Some NAS vendors ship an older one, so check with docker compose version.
  • An x86_64 or arm64 machine.

What runs

One machine

The ryph network

Flat: every service reaches every other, and the internet

  • app
  • control-plane
  • launcher
  • forge
  • historian
  • model-proxy
  • mcp-gateway
  • memory
  • chat
  • automations
  • web
  • egress-proxy
  • previews
  • postgres
  • objects

Run network

Internal, and only the run gate leads out. Runs share it.

  • Run
  • Run
  • Run gate

Build network

One per build, behind the same gate

  • BuildKit

Tool network

Hosted MCP servers, on a network runs don’t join

  • Tool guard
  • MCP server
The launcher is the only service with the Docker socket, and starts every run, build and tool server. Runs are confined behind the gate; between the services, a token is the only guard.
ServiceWhat it does
appThe web app and its backend. It’s the only container a browser talks to, and it passes requests on to the services behind it.
control-planeRuns, agents, policy and sign-in. It decides what each run may use, and publishes the signing keys every other service checks tokens against.
launcherThe only container with the Docker socket. It starts runs, checks, environment builds and hosted MCP servers when the control plane or the MCP gateway asks.
forgeThe git gateway agents clone through and hand their patches to. With a GitHub App or a GitLab bot, it also receives their webhooks.
historianStores each run’s transcript as it streams in, in a separate database.
model-proxyHolds the model provider keys. Agents call models through it with a short-lived key, and it swaps in the real one and meters usage.
mcp-gatewayHolds tool server credentials and hosts MCP servers. Agents call tools through it, and it checks each call against the agent’s tool policy.
memoryWhat agents remember across runs: the curated canon they’re briefed from, and the inbox where their new observations wait to be screened.
chatConnects Ryph to chat rooms on Slack, Discord, Zulip and Telegram, and turns mentions into work.
automationsSchedules and inbound webhooks you set up. Each one starts a run when it fires.
webWeb search and page fetching for agents. It keeps no data.
egress-proxyRuns reach the internet through it. It lets requests through only to hosts on the run’s allowlist, and refuses private addresses.
previewsWhen an agent exposes a port in its sandbox, such as a dev server, the run opens a tunnel to this service so you can open it in your browser. Ryph checks that you can see the run first.
sanitizerCleans the pages agents publish as artifacts. It runs as a separate user in a separate process namespace, and holds no secrets.
artifact-thumbnailsRenders the previews in artifact lists. It has no network, and only the control plane can reach its socket.
postgresOne PostgreSQL server, with a separate database for each service that keeps data.
objectsS3-compatible object storage (SeaweedFS) for patch bundles and run images.

Secrets are split by service. Model provider keys are sealed inside the model proxy, tool credentials inside the MCP gateway, and chat credentials inside the chat service. Each link between two services has its own token, held by those two services alone.

Start it

All secrets are required settings, and Compose refuses to start until each one is set in .env. From a checkout of the repository, bun run compose:init writes a random value for each missing one. Otherwise, generate each with openssl rand -hex 32.

On Linux, the launcher needs the Docker socket’s group. compose:init writes it to .env as DOCKER_GID. Without it, set it yourself: stat -c %g /var/run/docker.sock prints it. Docker Desktop works with the default.

cd deploy/compose-networked
docker compose pull
docker compose up -d --wait

Images come from quay.io/ryph at RYPH_VERSION, which defaults to edge. docker compose pull also fetches the images runs start from, so the first run doesn’t wait on a large download.

Claim it

Open http://localhost:3000. The first account you register becomes the owner, and it needs the setup token the control plane prints at boot:

docker compose logs control-plane | grep "Setup token"

Then add a model provider key under Model keys, connect a repository, and create an agent.

Route webhooks

Every service listens on 127.0.0.1 only, and nothing routes by path. GitHub, GitLab and your automations need one address that reaches the right service, so put a reverse proxy in front:

PathService
/v1/hooks/github, /v1/hooks/gitlab, exactforge :7412
the rest of /v1/hooks/automations :7414
everything elseapp :3000

The two exact paths go first. Without them, GitLab’s deliveries reach the app, which answers 404, and GitLab soon turns the hook off. The README in deploy/compose-networked/ has a Caddy example, and covers a GitLab with a self-signed certificate.

How runs start

The control plane holds the secrets but no Docker socket. The launcher holds the socket but no secrets: no master key, no database, no provider key. When a run, a check suite or an environment build needs a container, the control plane asks the launcher for one over HTTP.

A request names only what to run. Everything that could reach past a container is the launcher’s own configuration: its networks, its mounts, its privileges and its runtime. So a control plane that’s been compromised can start an unprivileged run on an image of its choosing, which is no more than a run can do anyway.

Environment builds and checks are on by default. Builds run in a BuildKit container on a separate network, behind the same gate as runs.

The MCP gateway uses the launcher too, to host MCP servers from its catalogue. It has its own token, which can host servers but can’t touch runs. Hosted servers and their tool guard sit on a network runs don’t join.

How runs are kept apart

Each run joins an internal network with no outside access. It can only get out through the run gate, which relays only the ports a run needs, each to the service behind it:

ServiceWhat a run reaches it for
control-planeIts task, credentials, heartbeat and result.
forgeIts clone, and its work as a patch.
historianIts transcript.
model-proxyModel calls, with the run’s own key.
mcp-gatewayTool calls, with the run’s own token.
egress-proxyThe internet, through the agent’s allowlist.
previewsA port it exposes for you to open.

So the egress allowlist is a real boundary: a run that ignores the proxy reaches nothing. A run can’t reach the web app, the launcher, the gateways’ management endpoints, any other service by name, or the Docker host.

Runs share the run network, so a run can reach another run’s open ports. With RYPH_SERVICES=on, a repository’s service containers, such as the database its tests need, start beside each run on that run’s own network. A Docker network separates them by address, not by force: under OrbStack, another run can reach them.

What Compose can’t do

Between services, the network is flat. Every service can reach every other. On Kubernetes, network policies open each service’s ports only to its callers. Here, each internal endpoint is guarded by its service token alone. That’s why each token goes only to the two ends of its link.

The launcher is root on the machine. Whoever controls it controls the server. It’s built to hold nothing else of value, and environment builds still run in a privileged BuildKit container, as they do anywhere on Docker.

One of each. Every service is declared at one replica. Running more is expected to work, but nothing here checks it yet.

Runs are confined. Services aren't.

This mode keeps runs away from your services and the internet, but not your services away from each other. For both, use Kubernetes.

Stop and remove

docker compose stop pauses everything and keeps it all. docker compose up -d --wait resumes it.

The launcher starts the run gate and the run network itself, so Compose doesn’t know about them, and a plain docker compose down can’t remove the ryph network while the gate is on it. From a checkout, bun run compose:down removes them with everything else, by this deployment’s labels. Your data survives. bun run compose:reset removes the data too.