# 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](https://github.com/ryph-hq/ryph/blob/main/deploy/compose-networked/compose.yaml)

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](https://ryph.ai/docs/kubernetes.md) 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](https://ryph.ai/docs/docker-compose.md) 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

What runs where, on one machine:

- **The ryph network**: Flat: every service reaches every other, and the internet. Holds app, control-plane, launcher, forge, historian, model-proxy, mcp-gateway, memory, chat, automations, web, egress-proxy, previews, postgres, objects.
- **Run network** (locked down by default): Internal, and only the run gate leads out. Runs share it. Holds Run, Run, Run gate.
- **Build network** (locked down by default): One per build, behind the same gate. Holds BuildKit.
- **Tool network** (locked down by default): Hosted MCP servers, on a network runs don’t join. Holds 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.

| Service               | What it does                                                                                                                                                                              |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `app`                 | The 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-plane`       | Runs, agents, policy and sign-in. It decides what each run may use, and publishes the signing keys every other service checks tokens against.                                             |
| `launcher`            | The 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.                              |
| `forge`               | The git gateway agents clone through and hand their patches to. With a GitHub App or a GitLab bot, it also receives their webhooks.                                                       |
| `historian`           | Stores each run's transcript as it streams in, in a separate database.                                                                                                                    |
| `model-proxy`         | Holds 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-gateway`         | Holds tool server credentials and hosts MCP servers. Agents call tools through it, and it checks each call against the agent's tool policy.                                               |
| `memory`              | What agents remember across runs: the curated canon they're briefed from, and the inbox where their new observations wait to be screened.                                                 |
| `chat`                | Connects Ryph to chat rooms on Slack, Discord, Zulip and Telegram, and turns mentions into work.                                                                                          |
| `automations`         | Schedules and inbound webhooks you set up. Each one starts a run when it fires.                                                                                                           |
| `web`                 | Web search and page fetching for agents. It keeps no data.                                                                                                                                |
| `egress-proxy`        | Runs reach the internet through it. It lets requests through only to hosts on the run's allowlist, and refuses private addresses.                                                         |
| `previews`            | When 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. |
| `sanitizer`           | Cleans the pages agents publish as artifacts. It runs as a separate user in a separate process namespace, and holds no secrets.                                                           |
| `artifact-thumbnails` | Renders the previews in artifact lists. It has no network, and only the control plane can reach its socket.                                                                               |
| `postgres`            | One PostgreSQL server, with a separate database for each service that keeps data.                                                                                                         |
| `objects`             | S3-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.

```sh
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:

```sh
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:

| Path                                          | Service             |
| --------------------------------------------- | ------------------- |
| `/v1/hooks/github`, `/v1/hooks/gitlab`, exact | `forge` :7412       |
| the rest of `/v1/hooks/`                      | `automations` :7414 |
| everything else                               | `app` :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:

| Service         | What a run reaches it for                    |
| --------------- | -------------------------------------------- |
| `control-plane` | Its task, credentials, heartbeat and result. |
| `forge`         | Its clone, and its work as a patch.          |
| `historian`     | Its transcript.                              |
| `model-proxy`   | Model calls, with the run's own key.         |
| `mcp-gateway`   | Tool calls, with the run's own token.        |
| `egress-proxy`  | The internet, through the agent's allowlist. |
| `previews`      | A 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.

> [!WARNING]
> **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](https://ryph.ai/docs/kubernetes.md).

## 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.
