# Docker Compose

> The Ryph binary and a real Postgres on one server you keep running.

- **Best for:** One server, for you or a small team
- **Setup:** One compose file: Ryph, a launcher and Postgres
- **Runs:** Containers on the server’s Docker
- **Isolation:** Runs on a confined network. One process holds every key; a launcher holds the Docker socket.
- **Data:** Postgres in a container
- **Scale:** One machine

[View Compose file](https://github.com/ryph-hq/ryph/blob/main/deploy/compose/compose.yaml)

This mode runs the same `ryph` binary as a laptop, in a container beside a real Postgres. Agents run as containers on the same machine's Docker. The host only needs Docker.

It suits one server you keep running, such as a NAS, a homelab box or a VPS, for one person or a small team. It's the simplest way to self-host Ryph.

The files are in `deploy/compose/` in the Ryph repository.

## What you need

- Docker Engine 28 or later, which confining runs needs, with Compose 2.20 or later and buildx. Docker Desktop has all three.
- 10 to 15 GB of disk for the images. Most of it is the agent image, which carries every mainstream toolchain. Environment builds need several GB more for each repository.
- An x86_64 or arm64 machine.
- The `compose.yaml` from `deploy/compose/`, and its `.env.example` if you want to change settings. Every image is pulled from `quay.io/ryph`.

## Start it

```sh
cd deploy/compose
cp .env.example .env      # optional; every setting has a default
docker compose pull
docker compose up -d --wait
```

`docker compose pull` also fetches the images runs and environment builds start from, so the first run doesn't wait on a large download.

On Linux, set `DOCKER_GID` in `.env` to the group of the Docker socket first. `stat -c %g /var/run/docker.sock` prints it. The launcher starts runs through that socket, and the group is what lets it. Docker Desktop works with the default.

`RYPH_VERSION` in `.env` picks the version of every image at once. The default, `edge`, is the newest build of the main branch, and a `sha-` tag pins one commit. There are no numbered releases yet.

## Claim it

The first account needs the setup token Ryph prints at boot:

```sh
docker compose logs ryph | grep "Setup token"
```

The line holds a full link. Open it and register, and you're the owner. Everyone after you registers or is invited from the web app.

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

For one person, set `RYPH_MULTIUSER=off` in `.env` before the first start. Ryph is then single-user like the laptop binary, and you sign in with a link. `docker compose exec ryph ryph login --no-open` prints a fresh one.

## What runs

What runs where, on your server:

- **The ryph container**: One process, holding the master key and the database, and no Docker socket. Holds Web app, Sign-in, Control plane, Gateways, Run history, Memory, Chat, Automations, Forge.
- **The launcher**: The Docker socket, and none of Ryph’s secrets. Holds Launcher.
- **postgres** (locked down by default): On no network at all, reached through a socket. Holds PostgreSQL 17.
- **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.

Ryph asks the launcher for every run, build and tool server, and the launcher alone holds the Docker socket. A run can only get out through the run gate, which reaches only Ryph’s run-facing ports.

| Service    | What it does                                                                                                                                                                |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ryph`     | The binary: the web app, sign-in, control plane, gateways, run history, memory, chat, automations and the forge, in one process. It holds the secrets and no Docker socket. |
| `launcher` | The only container with the Docker socket. It starts the containers below when Ryph asks, and holds none of Ryph's secrets.                                                 |
| `postgres` | PostgreSQL 17. It's on no network at all. Ryph reaches it through a socket in a shared volume.                                                                              |
| `artifact-thumbnails` | Renders the previews in artifact lists. It has no network, credentials or Docker socket, and only Ryph can reach its socket.                                      |

The launcher starts the rest:

- **One container per run**, on an internal network with no outside access.
- **The run gate**, the only way out of a run's network. It relays only Ryph's run-facing ports.
- **A browser** beside a run whose agent uses one.
- **Hosted MCP servers**, behind a tool guard on a network runs don't join.
- **One BuildKit container per environment build**, on its own network behind the same gate.

State lives in named volumes: the master key and files, the database, repositories kept on the server, and the launcher's build cache.

Compose doesn't know about the containers the launcher starts, so they outlive `docker compose down`. To remove everything, take them away by the labels the launcher puts on them. The README in `deploy/compose/` has the commands.

## Connect your code

**GitHub, as you.** Create a fine-grained personal access token for the repositories agents should work on, and set it as `RYPH_GITHUB_TOKEN` in `.env`. It needs read and write access to Contents, Pull requests and Issues, and read access to Metadata. Commit statuses (read) is optional and lets agents see CI. Leave Workflows off, so no agent can change `.github/workflows`. Everything Ryph does on GitHub appears as you. This mode gets no webhooks, so `/ryph` comments do nothing and merges are noticed about once a minute.

**Repositories on the server.** Without a git host, agents can work on repositories in the `repos` volume. Clone one there with `docker compose exec ryph git clone`, then add it under Repos → Connect repository → On this machine.

**Radicle.** The `radicle` profile adds a Radicle node of Ryph's own, with its own identity. Ryph can also act through a node that already runs on the server.

**GitHub as an App.** An App acts as itself and gets webhooks, so `/ryph` commands and merges arrive straight away. It needs GitHub to reach your server, so a NAS on a home network usually can't use one.

**GitLab, as you.** Works with gitlab.com or your own GitLab, 17.0 or newer. Create a personal access token with the `api`, `read_repository` and `write_repository` scopes, and set `RYPH_GITLAB_MODE=user` and `RYPH_GITLAB_TOKEN` in `.env`, plus `RYPH_GITLAB_URL` for your own instance. As with GitHub, everything appears as you, `/ryph` comments do nothing and merges are noticed by polling.

**GitLab with a bot.** A bot is a GitLab account of Ryph's own, and it gets webhooks. Give it a token with the same scopes, and add it to your projects or a group at the **Developer** role, so protected branches keep it off your default branch. Set `RYPH_GITLAB_TOKEN` to its token and `RYPH_GITLAB_WEBHOOK_SECRET` to a random secret. When you connect a project, Ryph shows a webhook URL and token for a Maintainer to paste into the project's **Settings → Webhooks**. For sign-in with GitLab, add an OAuth application with the `read_user` scope and set `RYPH_GITLAB_CLIENT_ID` and `RYPH_GITLAB_CLIENT_SECRET`.

A branch Ryph pushes to GitLab runs the project's `.gitlab-ci.yml`, an agent's edits to it included, with the project's unprotected CI variables. Protect the variables a pipeline shouldn't hand to such a branch.

## Reach it from other machines

The web app listens on `127.0.0.1:3000`, so only the server reaches it. To open it to others:

1. **Put a proxy with TLS in front**

   Ryph's session cookie only works over HTTPS, so plain HTTP on a LAN address signs you out straight away.

2. **Set the app's address**

   Set `RYPH_APP_URL` to the HTTPS address people use.

3. **Pass on the visitor's address**

   Set `RYPH_ADDRESS_HEADER=x-forwarded-for`, and `RYPH_XFF_DEPTH=1` behind one proxy. Sign-in is rate limited per address, and without this every visitor arrives from Docker's gateway and shares one limit.

4. **Keep the app on localhost**

   Leave `RYPH_UI_BIND=127.0.0.1` when the proxy runs on the same server.

With Caddy:

```
ryph.example.lan {
  reverse_proxy 127.0.0.1:3000
}
```

Webhooks for automations arrive on port 7411 under `/v1/hooks/`. Route that path from your proxy if you use them, but leave `/v1/hooks/github` and `/v1/hooks/gitlab` on the app's port, ahead of it. The app hands those two on to the forge.

## Environments

Each repository gets an environment: an image with its checkout and setup already done, so its runs start ready instead of installing dependencies every time. Checks run on the same images. Ryph builds them by itself, and again as the default branch moves. Both are on by default. `RYPH_ENVIRONMENTS=off` and `RYPH_CHECKS=off` in `.env` turn them off.

## How it's kept safe

**Ryph holds the secrets, and the launcher holds the socket.** The `ryph` container has the master key and the database, but no Docker socket. It asks the launcher over HTTP to start runs, checks, environment builds and hosted MCP servers. A request names only what to run. Everything that could reach past a container is the launcher's own configuration: its networks, the run gate, the runtime, mounts and privileges.

So if Ryph is compromised, the attacker has every secret Ryph holds, but can only ask for what Ryph may ask for: an unprivileged run on the run network, or a hosted MCP server. It can't ask for this machine's files, a privileged container or a published port, so it doesn't get root on the server through the launcher.

**Runs are confined.** Each run joins an internal Docker network with no default route, no DNS for outside names, and no address on the host. The launcher refuses to launch onto a network that isn't. A run can only get out through the run gate, which reaches the control plane, the forge, the model and tool gateways and the egress proxy. It can't reach the web app, previews or Postgres. The egress proxy lets a run reach only the hosts its agent's network policy allows, and refuses private addresses.

Runs share that network, so a run can reach another run's open ports. Runs use Docker's default runtime, so an escape reaches the Docker daemon. On Linux with gVisor installed, `RYPH_DOCKER_RUNTIME=runsc` runs them under it. Builds don't get that runtime, so with it set Ryph starts with environments on only if you also set `RYPH_ENV_TRUST_DEFAULT_BRANCH=on`, which builds just the default branch. The other choice is `RYPH_ENVIRONMENTS=off`.

**Environment builds run privileged.** A repository's `.ryph/Dockerfile` and setup steps run as root in a BuildKit container. It's confined like a run, but an escape from it is root on the server. Connect only repositories whose `.ryph/` you'd run as root here, or turn environments off.

> [!WARNING]
> **The launcher is root on the server**
>
> Whoever controls the `launcher` container controls the machine, so it holds no other secrets and listens only for Ryph. Keep it patched. To keep runs and services apart from the machine and from each other, use [Kubernetes](https://ryph.ai/docs/kubernetes.md).

## Upgrade

```sh
docker compose pull
docker compose up -d --wait
```

If `RYPH_VERSION` is pinned, change it first. Ryph applies new migrations at boot and refuses to start on a database a newer version has migrated, so take a backup before you upgrade. Repos and workspaces with environment builds need a Rebuild afterwards to pick up the new agent tools.

## Back up

Keep the database dump and the master key in different places. Together they hold your secrets in the clear.

```sh
# The database. Safe while Ryph runs.
docker compose exec -T postgres pg_dump -U ryph ryph | gzip > ryph-$(date +%F).sql.gz

# The master key, which opens every provider key and credential in the database.
docker compose exec ryph cat /var/lib/ryph/secrets.key > secrets.key
```

The `ryph-home` and `repos` volumes hold run files and repositories kept on the server. Back them up too if you use them. The launcher's volumes hold only build cache and working files.

`docker compose down` keeps every volume. `docker compose down -v` deletes them, the database and the key included.
