Preview Environments
Preview environments give every pull request its own short-lived deployment, so reviewers can open the proposed change in a browser instead of building it locally. They are provided by roost, deployed when a pull request opens and torn down again when it closes or merges.
Preview kinds
Section titled “Preview kinds”A preview is either a directory of files or a running container:
- Static uploads a built directory, which the edge serves as-is. This is the simplest path and needs nothing but a build step that emits a folder.
- Container runs your application and reverse-proxies to it, for anything that has to actually execute: a server-rendered app, an API, a backend the frontend talks to.
A container preview gets its image in one of two ways:
- From a registry, by reference (
--image/image), which means CI has to build the image and push it somewhere first. - From an uploaded archive (
--image-archive/image_archive), where CI builds the image to a.tarfile and hands that file to roost directly.
Prefer the archive on CodeFloe. A preview image is built once, serves a single pull request, and is then thrown away, so pushing it to the container registry spends package quota on an artifact nobody keeps. Uploading the archive skips the registry entirely, which also removes the registry login and its credentials from the pipeline.
Preview URL
Section titled “Preview URL”Each preview is served at its own subdomain of preview.codefloe.com:
For example, pull request 27 in codefloe/docs is served at:
The parts are:
<owner>-<repo>identifies the repository.pr-<number>is the pull request number.<hash>is a short hash of the repository, so two repositories that would otherwise slugify to the same name never collide on a shared daemon.
The name is deterministic: the same pull request always maps to the same URL, so redeploys reuse it and the teardown removes the right one.
Daemon URL
Section titled “Daemon URL”CodeFloe runs one shared roost daemon, so there is nothing to install or operate. Its control API lives at:
The address is internal: CodeFloe’s CI runners can reach it, the public internet cannot. It is also not a credential, because every deploy is authenticated separately, so it does not need to be kept secret.
The roost client reads the daemon URL from ROOST_SERVER and falls back to http://127.0.0.1:7420 when that variable is empty.
A job that never sets it therefore fails with:
This is the signature of an unset or misspelled ROOST_SERVER, not of a daemon outage.
Lifecycle
Section titled “Lifecycle”- On a pull request the preview is deployed (or updated on every new push).
- On close or merge the preview is destroyed.
When the preview is deployed, a bot comment posts the link on the pull request and keeps it up to date. On teardown the comment is struck through and suffixed with “→ torn down”, leaving a record that a preview existed.
Enabling previews
Section titled “Enabling previews”Previews work on both CI systems CodeFloe offers. Full, working examples for each (static site and container) live in codefloe/roost-preview-examples.
The two examples below deploy a static site. For an app that has to run, keep the same wiring and swap the payload as described under container previews.
Crow CI
Section titled “Crow CI”Add a roost step to your Crow CI pipeline that runs on the pull request lifecycle events.
The roost_server and roost_token secrets already exist as global secrets in Crow CI, so you do not need to create them yourself: just reference them with from_secret.
The single step covers the whole lifecycle: it deploys on pull_request and destroys on pull_request_closed and pull_request_merged.
The ttl is a safety net so the daemon reclaims a preview even if the teardown event never fires.
Forgejo Actions
Section titled “Forgejo Actions”Forgejo Actions has no dedicated roost action, so the jobs run the roost CLI directly.
They authenticate without a shared secret: each job mints a short-lived OIDC token (enable-openid-connect: true) and exchanges it for the roost bearer token.
roost verifies the token and scopes the caller to its own repository, so deploys are named from the repository and pull request (--repo/--pr).
The daemon URL is available instance-wide as the Forgejo Actions variable ROOST_SERVER, so ${{ vars.ROOST_SERVER }} resolves in every repository on CodeFloe without any per-repository setup.
No token secret is needed either: the OIDC exchange replaces it.
The URL is a variable rather than a secret on purpose. Forgejo supports instance-wide (global) Actions variables, but deliberately does not support instance-wide secrets, so a shared value can only be distributed to every repository as a variable. That is a good fit here because the daemon URL is not a credential.
To point a single repository at a different daemon, define ROOST_SERVER as a repository or organization variable under Settings → Actions → Variables; the more specific definition wins over the instance-wide one.
Container previews
Section titled “Container previews”A container preview replaces the uploaded directory with an image and a port. Everything else stays as it is above: the same lifecycle events, the same derived name, the same URL, the same teardown.
The application has to listen on 0.0.0.0:<port> inside the container, not on 127.0.0.1, or the reverse proxy in front of it cannot reach it.
Building the image without a registry
Section titled “Building the image without a registry”Build the image to a .tar file in the same workspace and hand that file to roost.
Nothing is pushed, so the pipeline needs no registry credentials and the throwaway image never touches your package quota.
On Crow CI, the buildx plugin writes the archive and the roost step uploads it:
On Forgejo Actions, build and deploy have to live in the same job, so the archive stays on disk instead of travelling through upload-artifact, which would be slow and spend artifact storage:
The build must be single-platform.
type=docker rejects a multi-arch build, and a preview only ever runs on the daemon’s architecture anyway.
An archive may be at most 2 GiB. roost loads it, runs it, and deletes the image again when the preview is replaced or destroyed.
Passing configuration
Section titled “Passing configuration”Container previews take environment variables, so an app can be pointed at a staging API, put in debug mode, or given the credentials it needs to boot.
In Crow CI, env is a mapping and its values may come from_secret, so nothing sensitive is written into the pipeline file:
In Forgejo Actions, pass them on the command line.
A bare name takes its value from the step’s own environment, which keeps a secret out of the process list where --env KEY=VALUE would leave it readable:
Files are read first, in the order given, then the individual variables, so an explicit variable overrides the same name coming from a file. Environment variables apply to container previews only; a static preview has no process to give them to.
roost does not store these values, does not log them, and does not return them when listing previews. They are, however, part of the container’s configuration on the daemon host.
Failed deploys
Section titled “Failed deploys”roost waits for a container preview to actually start serving before it calls the deploy a success. Once the container is up, it polls the published port until the app answers an HTTP request, and only then publishes the URL.
If nothing answers within 30 seconds, the deploy fails, the container is removed, and the error carries the last lines the container wrote:
That output is usually the whole diagnosis: a missing environment variable, a crash on boot, or an app bound to 127.0.0.1 instead of 0.0.0.0.
Any HTTP response counts as serving, whatever its status, so a preview answering 404 is considered up.
A job that fails here means no preview was published, rather than a green pipeline pointing reviewers at a URL that only ever returns 502.
Limits
Section titled “Limits”Each container preview runs with 512 MiB of memory, one CPU, and at most 256 processes. It drops all capabilities and cannot gain new privileges.
An app that needs materially more than that is not a good fit for a shared preview host: build it as a static site if you can, or run your own roost daemon.
Static previews are capped at 512 MiB compressed on upload and 1 GiB once extracted.