Every run starts on a machine. Out of the box, that’s the universal image, which carries the mainstream toolchains. Add a few files under .ryph/, and Ryph builds your repo its own environment: the tools it needs, the code checked out, dependencies installed and the build already warm.
Runs then start where the last build left off. An agent on a big Rust or Node project gets to work without waiting for installs and a cold build, on the same setup every time.
- .nvmrc 22
- .ryph/Dockerfile
- .ryph/setup.yml
- .ryph/services.yml
- Machine Rust, Node, Python, Go…
- Dependencies each run
- Build cold
With no setup, a run starts on the universal image. The Node version acme/web pins in .nvmrc installs on first use.
Works with no setup
A repo with no .ryph/ runs on the universal image. It’s Ubuntu 24.04 with:
| Group | What’s included |
|---|---|
| Languages | Rust stable with clippy and rustfmt, Node 24 with pnpm and yarn, Deno 2, Bun, Python 3.12 with uv, Go |
| Build tools | build-essential, pkg-config, cmake, clang, the OpenSSL headers, SQLite |
| Everyday tools | git, ripgrep, fd, jq, just, shellcheck, curl |
| Version managers | rustup and mise |
The versions your repo pins install on first use. rust-toolchain.toml is read on the first cargo call, and .tool-versions, mise.toml, .nvmrc and .python-version are read the first time the tool runs.
This covers most repos, with two costs. A pinned version downloads again on every run, and every run installs your dependencies and builds from cold. On a large project, that’s minutes before the agent reads its task.
Your repo’s own image
Once a repo has a .ryph/ directory, Ryph builds it an environment image from the default branch. The image holds everything a run would otherwise do itself:
The run its own commit
Fetches the commit it was given and checks it out in place. setup.yml runs again to catch up on what changed.
Changes whenEvery run
Ryph’s harness /opt/ryph
The runner and the agent’s harness.
Changes whenRyph is upgraded. Only this layer is swapped, in seconds.
Your checkout /ryph/work
Your repository at the built commit, with setup.yml already run: node_modules, target/, .venv and the rest.
Changes whensetup.yml or the Dockerfile changes. Code changes alone rebuild it at most every 6 hours.
The machine .ryph/Dockerfile
The operating system, system packages and toolchains. Without a Dockerfile, the universal image.
Changes whenThe Dockerfile changes.
When a run starts on the image, it fetches its own commit into the checkout that’s already there and checks it out in place. Git only rewrites the files that differ, so the build output from the image stays valid for everything else, and a tool like cargo rebuilds only what changed. Then setup.yml runs again, to pick up a lockfile that moved since the build. On a warm tree, that takes moments.
The agent is told which image it’s on, when and from which commit it was built, and which toolchain versions it has.
Let an agent set it up
You don’t have to write any of this by hand. Open the repository under Repos, and in its Environment section click Set up this repo’s environment. Pick the Agent to do it, click Start, and it works through these steps. Once the repository has a .ryph/, the same button reads Revise environment.
-
The agent reads your repo
It looks at your CI configuration, contributor guides, toolchain pins and lockfiles.
-
It writes .ryph/ and tries it
It writes
setup.ymlandenvironment.md, plus aDockerfilefor anything setup steps can’t install, andservices.ymlwhen tests need a database. It tries what it can in its own shell. -
Ryph builds its branch
The agent ships the files as a patch. Ryph builds an image from the patch’s branch and sends the result to the agent, with the log of any step that failed.
-
It fixes what failed
The agent revises and pushes until the image builds. You review a patch that’s known to work.
Agents can’t run Docker themselves. Building the patch’s branch is how they find out whether a Dockerfile works.
A build is confined like a run: it reaches the internet only through Ryph’s egress proxy, so a change to setup.yml can’t take a build anywhere the allowlist doesn’t.
When Ryph builds
.ryph/changes on the default branch. Ryph builds right away. Changes tochecks.yml,services.ymlorenvironment.mddon’t need a new image, so they don’t start one.- Only code changes. Ryph rebuilds at most every 6 hours. The image only has to stay warm, since every run fetches its own commit anyway.
- Ryph is upgraded. When the deployment pins Ryph’s images by version or digest, only the harness layer is swapped on every image, which takes seconds. With the default
edgetag on Docker Compose, click Rebuild after an upgrade. - You ask. Rebuild, in the Environment section of the repository’s page, starts one.
Runs keep starting on the previous image while a new one builds. Builds are incremental too: when the Dockerfile hasn’t changed, a build starts from the last image and only redoes what moved.
Which image a run gets
| The run is on | It uses |
|---|---|
| The default branch | The newest image built from the default branch |
A branch with the same .ryph/Dockerfile as the default branch | The default branch’s image |
A branch that changes .ryph/Dockerfile | An image of that branch. The run waits while it builds |
| A branch that removes the Dockerfile | The universal image |
| A repo with no image built yet, and no Dockerfile | The universal image, straight away |
| A repo with no image built yet, and a Dockerfile | Its first image. The run waits while it builds |
A run waits only when the image it would get is the wrong machine. An image a few commits old is as good as a new one, because the run checks out its own commit on top. A branch that edits only setup.yml uses the default branch’s image too, since the run reads and runs its own copy of setup.yml anyway.
If a build the run is waiting for fails, the run stops with the build’s log. An agent on the wrong machine would only fail somewhere harder to read.
Write it yourself
Everything lives in .ryph/ at the root of the repo. Every file is optional.
| File | What it’s for |
|---|---|
.ryph/setup.yml | Commands that prepare the checkout: installing dependencies, a first build. |
.ryph/Dockerfile | The machine: system packages and toolchains the universal image doesn’t have. |
.ryph/environment.md | Notes for the agent about its environment, added to its prompt. |
.ryph/services.yml | Databases and other services each run gets its own copy of. |
setup.yml
A list of steps, each with a run command and an optional name:
steps:
- name: install dependencies
run: npm install --prefer-offline
- name: prepare the database
run: |
cp .env.example .env
npm run db:migrate
- run: npm run build Each step runs with bash -c in the repository’s root, in a fresh shell, so variables and cd don’t carry over to the next step. If a step fails, the rest of that repo’s steps are skipped, anything the failed step changed in the checkout is put back, and the run goes on with a warning. The agent is told which step failed, so it can finish the setup itself.
Two rules matter, because the steps run twice: once when the image is built, and again at the start of every run.
Make steps cheap when nothing changed. On a warm tree, most tools have nothing to do:
| Cheap on a warm tree | Throws the warm tree away |
|---|---|
npm install --prefer-offline, pnpm install | npm ci, which deletes node_modules |
cargo build, cargo fetch | cargo clean && cargo build |
uv sync, pip install -r requirements.txt | rm -rf .venv && python -m venv .venv |
deno install --frozen, go mod download |
For a step that should only run during the image build, check RYPH_SETUP_PHASE, which is image there and run at the start of a run:
- name: warm the build cache
run: '[ "$RYPH_SETUP_PHASE" = image ] && npm run build || true' Leave the tree clean. Whatever is in the checkout when the agent finishes becomes its work. If setup leaves a tracked file modified, or an untracked file that isn’t gitignored, Ryph puts the checkout back and warns. Keep setup output in .gitignore, and use your tools’ locked install so the lockfile doesn’t change.
Setup steps get the same internet access as the run. There’s no teardown: whatever a step starts ends with the run’s container.
Dockerfile
A .ryph/Dockerfile describes the machine: the packages and tools your repo needs that the universal image doesn’t have.
FROM quay.io/ryph/ryph-universal:1
USER root
RUN apt-get update
&& apt-get install -y --no-install-recommends libvips-dev
&& rm -rf /var/lib/apt/lists/*
USER runner
COPY rust-toolchain.toml /tmp/toolchain/
RUN cd /tmp/toolchain && rustup toolchain install
RUN cargo install --locked cargo-nextest - The build context is your repository at the commit being built, so you can
COPYa lockfile or a toolchain file..ryph/Dockerfile.dockerignoreis honoured. - Building
FROMthe universal image is the easiest start. On Ryph it resolves to the deployment’s own pinned copy, and on your laptop to the public image, so the same file builds in both places. Any other glibc-based image works too. - Leave the checkout to Ryph. The Dockerfile shouldn’t
COPY .the whole repo or build the project: Ryph clones it into the image and runssetup.ymlthere. - Switch back to
USER runnerat the end. Runs and setup steps run as that user.
environment.md
Plain prose for the agent, added to its system prompt: which services are running and where, how to run the tests, and anything it would otherwise find out by trial and error. Keep it under 32 KiB.
Ryph adds its own notes ahead of it, describing the image the run is on and the services it was given. The agent always sees those, whatever environment.md says.
services.yml
Services the run needs, such as Postgres or Redis. Ryph starts a copy for each run, beside its container:
services:
db:
uses: postgres@18
env:
POSTGRES_PASSWORD: password
POSTGRES_DB: app_test
port: 5432 A step or the agent reaches a service by its name, like db:5432. The run’s container has no Docker, so docker compose up in a setup step won’t work. Declare the service here. Running services needs RYPH_SERVICES=on on the deployment.
The same services back your dev server when an agent shares it as a preview, so the app you click through has its database behind it.
Workspaces
A workspace run is one container, so it gets one image. The machine comes from the Dockerfile of the workspace’s environment repo, which is its first repo unless you pick another in the Environment section of the workspace’s page. Every member’s checkout is baked into the image, with its own setup.yml already run.
Machine sizes
An admin picks a size for each repository under Machine size on its page in Repos, and for each workspace on its page in Workspaces. Sizes are set for the whole deployment with RYPH_RUN_SIZES, such as:
RYPH_RUN_SIZES='standard:cpu=4,memory=8Gi,ephemeral-storage=10Gi;large:cpu=8,memory=16Gi,ephemeral-storage=10Gi,work-volume=50Gi'
RYPH_RUN_SIZE_DEFAULT=standard On Kubernetes, work-volume gives a run’s checkout its own volume, for repos whose build output outgrows the pod’s disk.
Turn them on
The ryph binary on Docker, Docker Compose and Networked Compose build environments by default. Images take a few GB of disk per repo. On Kubernetes, set environments.enabled: true in the Helm values and give the builds a registry to push to. Anywhere else, set RYPH_ENVIRONMENTS=on.
| Variable | Default | What it does |
|---|---|---|
RYPH_ENVIRONMENTS | off | on builds environment images. On by default for the ryph binary on Docker and both Compose setups. |
RYPH_ENV_REBUILD_INTERVAL | 6h | How often a change to code alone rebuilds an image. |
RYPH_ENV_BUILD_CONCURRENCY | 2 | Builds running at once across the deployment. |
RYPH_ENV_BUILD_TIMEOUT | 60m | The longest a build may take. |
RYPH_ENV_WARM_CHAIN | 4 | Incremental builds in a row before the next one starts from scratch. 0 makes every build start from scratch. |
RYPH_RUN_SIZES | standard, 4 CPUs and 8 GiB | The machine sizes an admin can pick from. See above. |
RYPH_RUN_SIZE_DEFAULT | the first size | The size a repo gets until an admin picks one. |