Merge remote-tracking branch 'origin/main'

This commit is contained in:
Bryan Helmkamp 2026-05-01 09:43:39 -04:00
commit 155d0a5a28
No known key found for this signature in database
22 changed files with 254 additions and 476 deletions

View file

@ -1,12 +0,0 @@
# syntax=docker/dockerfile:1.9
#
# Thin deploy image for PaaS providers that build from a repo Dockerfile
# (Railway, Render, Fly.io source mode). Pulls the published multi-arch
# image from GHCR instead of rebuilding fabro from source on every
# deploy, which keeps cold starts short and avoids needing the release
# workflow's pre-built binary context on the PaaS runner.
#
# The upstream image already configures entrypoint, CMD (binds
# 0.0.0.0:${PORT:-32276}), volumes, and the unprivileged fabro user.
FROM ghcr.io/fabro-sh/fabro:nightly

View file

@ -144,15 +144,18 @@ For headless or scripted environments, `fabro install` runs the same setup as a
---
## Self-host the Fabro server
## Running Fabro
Running Fabro as an HTTP server with the web UI lets a team share one instance. The repository ships a `Dockerfile` that serves the API (with the embedded web UI) on `$PORT` (default `32276`) and persists state to `/storage`.
Fabro runs as a server. You choose where it runs:
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/new/template?template=https%3A%2F%2Fgithub.com%2Ffabro-sh%2Ffabro) [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/fabro-sh/fabro)
- **On your laptop** — install the CLI (above) and run `fabro server start`. Workflows pause when your laptop sleeps.
- **On a host (self-hosted)** — deploy the Docker image with `docker compose` or any cloud container service (ECS, Cloud Run, Kubernetes). See [Self-host with Docker](https://docs.fabro.sh/administration/self-host-docker).
Click a button to provision a Fabro service from this repository. On Railway, attach a Volume at `/storage` so your runs and checkpoints survive redeploys; on Render, the `render.yaml` blueprint provisions a 1 GB disk at `/storage` automatically. The [Railway](https://docs.fabro.sh/administration/deploy-railway) and [Render](https://docs.fabro.sh/administration/deploy-render) deploy guides walk through env vars, accessing the dev token, and pointing the CLI at your deployment.
One-click managed alternative for the same Docker image:
Prefer to run Fabro elsewhere? See [Running the Fabro Server](https://docs.fabro.sh/administration/deploy-server) for generic Docker guidance and the companion guides for [Fly.io](https://docs.fabro.sh/administration/deploy-fly-io) and [DigitalOcean](https://docs.fabro.sh/administration/deploy-digital-ocean).
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/UcEy5m?referralCode=E5TucU&utm_medium=integration&utm_source=template&utm_campaign=generic)
See the [deployment overview](https://docs.fabro.sh/administration/deployment) for the full picture.
---

View file

@ -7,6 +7,9 @@ services:
volumes:
- fabro-storage:/storage
- /var/run/docker.sock:/var/run/docker.sock
env_file:
- path: .env
required: false
volumes:
fabro-storage:

View file

@ -1,137 +0,0 @@
---
title: "DigitalOcean"
description: "Deploy Fabro to a DigitalOcean Droplet with docker compose and Caddy for automatic TLS"
---
<Warning>
The server interface is in private early access. Contact [bryan@qlty.sh](mailto:bryan@qlty.sh) if you're interested in trying it.
</Warning>
DigitalOcean is a good fit for self-hosting Fabro on a Droplet — a plain Linux VPS running `docker compose`. The repo ships everything you need:
- `docker-compose.yaml` pulls the pre-built image from GHCR and declares a named volume for `/storage`
- `docker-compose.prod.yaml` adds a [Caddy](https://caddyserver.com) sidecar that terminates TLS and auto-provisions Let's Encrypt certificates
- `docker/Caddyfile` reverse-proxies HTTPS traffic to the Fabro server
<Note>
**Why not App Platform?** DigitalOcean App Platform has no persistent volumes — it is designed for stateless workloads that offload state to managed Postgres or Spaces. Fabro writes runs, checkpoints, sessions, and JWT keys to `/storage`, so a Droplet (or any VPS) is the right fit. If you prefer Kubernetes, DOKS works too — that path is not documented here.
</Note>
## Prerequisites
- A DigitalOcean account and (optionally) the [`doctl`](https://docs.digitalocean.com/reference/doctl/how-to/install/) CLI
- A domain name with DNS you can edit (required for Caddy to issue a Let's Encrypt cert)
- LLM provider API keys (Anthropic, OpenAI, etc.) and any secrets you want set via `.env`
## 1. Create a Droplet
Pick the **Docker on Ubuntu** image from the DigitalOcean Marketplace — it ships with Docker Engine and the Compose plugin preinstalled, so there's no manual Docker install step.
From the control panel, or with `doctl`:
```bash
doctl compute droplet create fabro \
--image docker-20-04 \
--size s-2vcpu-2gb \
--region nyc3 \
--ssh-keys <your-ssh-key-fingerprint>
```
`s-2vcpu-2gb` is a reasonable starting size for light usage; grow later as your workflow load increases. Any region works — pick the one closest to you.
## 2. Point DNS at the Droplet
Caddy needs the domain to resolve to the Droplet's public IP **before** you start the stack, or the first cert issuance will fail. Create an `A` record for your chosen hostname (e.g. `fabro.example.com`) pointing at the Droplet's IPv4 address. Wait for propagation before continuing.
## 3. Configure and launch
SSH into the Droplet and pull the repo:
```bash
ssh root@<droplet-ip>
git clone https://github.com/fabro-sh/fabro
cd fabro
cp .env.example .env
```
Edit `.env`. At minimum:
```
FABRO_DOMAIN=fabro.example.com
ANTHROPIC_API_KEY=...
SESSION_SECRET=...
```
The [Server Configuration](/administration/server-configuration) reference has the full list of variables; the minimum useful set is:
| Variable | Purpose |
|---|---|
| `FABRO_DOMAIN` | Public hostname Caddy serves. Must resolve to this Droplet for Let's Encrypt to issue a cert. |
| `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GEMINI_API_KEY` / ... | At least one LLM provider key for the models you'll run |
| `FABRO_DEV_TOKEN` | Optional — pre-set the dev token instead of reading the one written to `/storage` on first boot |
| `SESSION_SECRET` | 64-character hex string; required when the web UI is enabled |
| `GITHUB_APP_CLIENT_SECRET`, `GITHUB_APP_WEBHOOK_SECRET`, `GITHUB_APP_PRIVATE_KEY` | Only if you enable GitHub OAuth or the GitHub App integration |
Bring up the stack:
```bash
docker compose -f docker-compose.yaml -f docker-compose.prod.yaml up -d
```
This starts two containers: `fabro` (the server, state in the `fabro-storage` named volume) and `caddy` (listening on ports 80/443, cert state in the `caddy_data` named volume). Caddy requests a Let's Encrypt cert on first boot; watch progress with `docker compose logs -f caddy`.
## Accessing your Fabro server
Once `https://<FABRO_DOMAIN>/health` returns `ok`, two things to grab:
1. **The dev token** — on first boot, Fabro writes one to `/var/fabro/dev-token` and logs it:
```bash
docker compose exec fabro cat /var/fabro/dev-token
```
2. **Point your local CLI at the server** — add the URL to `~/.fabro/settings.toml`:
```toml title="~/.fabro/settings.toml"
[cli.target]
type = "http"
url = "https://fabro.example.com/api/v1"
```
Then commands like `fabro model list --server <url>` will hit your Droplet.
See [Running the Fabro Server](/administration/deploy-server) for the full auth and CLI-pointing story.
## Updates
To pull the latest nightly image and restart:
```bash
cd /root/fabro
git pull
docker compose -f docker-compose.yaml -f docker-compose.prod.yaml pull
docker compose -f docker-compose.yaml -f docker-compose.prod.yaml up -d
```
The `fabro-storage` and `caddy_data` named volumes survive `pull` and `up`, so runs, checkpoints, and TLS certs persist.
To pin a specific version instead of `:nightly`, edit `docker-compose.yaml` and change `image: ghcr.io/fabro-sh/fabro:nightly` to the tag you want.
## Caveats
- **DNS must resolve before first `up`.** Caddy will retry Let's Encrypt failures, but an obviously bad DNS config will lock you into the staging CA's low rate limits. Verify with `dig +short fabro.example.com` before starting.
- **Firewall.** The Docker Marketplace image opens 22, 80, and 443 by default — good. Keep port 32276 **closed** on the public interface; Caddy fronts the Fabro server on the private Docker network. Use `ufw status` to verify.
- **State lives in named volumes.** `fabro-storage` and `caddy_data` are the load-bearing pieces. Back them up (for example, via `docker run --rm -v fabro_fabro-storage:/src -v $(pwd):/dst alpine tar czf /dst/backup.tgz -C /src .`) before destructive operations.
- **Single-host deploy.** This setup assumes one Droplet owns the data. For HA you'd need a different architecture (external block storage, managed DB, etc.) — Fabro's server currently assumes a single writer on `/storage`.
- **Architecture.** The compose file pins `platform: linux/amd64`; the `:nightly` tag is multi-arch but the arm64 variant is not currently usable. Use x86_64 Droplets.
## Next steps
<Columns cols={2}>
<Card title="Running the Fabro Server" icon="server" href="/administration/deploy-server">
Auth, dev tokens, submitting runs, and pointing the CLI at your deployment.
</Card>
<Card title="Server Configuration" icon="gear" href="/administration/server-configuration">
Full `settings.toml` reference — reverse-proxy TLS, auth methods, concurrency, and more.
</Card>
</Columns>

View file

@ -1,108 +0,0 @@
---
title: "Fly.io"
description: "Deploy Fabro to Fly.io from the prebuilt GHCR image, with a Fly Volume for state"
---
<Warning>
The server interface is in private early access. Contact [bryan@qlty.sh](mailto:bryan@qlty.sh) if you're interested in trying it.
</Warning>
[Fly.io](https://fly.io) can host the Fabro server by pulling the pre-built image published to GHCR. The repo ships a `fly.toml` that points Fly directly at the image — no build step on Fly's builders, no `Dockerfile` evaluation — and declares a Volume mount at `/storage` so your runs, checkpoints, and sessions survive redeploys.
Fly.io is CLI-first; there is no one-click deploy button. The workflow below uses [`flyctl`](https://fly.io/docs/flyctl/install/).
## First-deploy checklist
### 1. Adopt `fly.toml` in a new app
```bash
git clone https://github.com/fabro-sh/fabro
cd fabro
fly launch --copy-config --no-deploy
```
`fly launch --copy-config` keeps the repo's `fly.toml` instead of generating a new one; `--no-deploy` lets you finish wiring secrets and volumes before the first deploy. You'll be prompted for an **app name** (must be globally unique on Fly) and a **primary region** — update `fly.toml` in place if you change the defaults.
### 2. Create the persistent Volume
Fabro writes all persistent state — run history, checkpoints, sessions, the default token, and JWT keys — under `/storage`. Fly containers have ephemeral filesystems, so without a Volume that directory is wiped on every redeploy. `fly.toml` declares the mount but **cannot create the volume itself** — provision it with `flyctl`:
```bash
fly volumes create storage --size 1 --region <your-primary-region>
```
Grow later with `fly volumes extend` if needed. The volume name (`storage`) must match `[[mounts]].source` in `fly.toml`.
### 3. Set required environment variables
Fly stores env vars as encrypted secrets:
```bash
fly secrets set \
ANTHROPIC_API_KEY=... \
SESSION_SECRET=...
```
The [Server Configuration](/administration/server-configuration) reference has the full list; the minimum useful set:
| Variable | Purpose |
|---|---|
| `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GEMINI_API_KEY` / ... | At least one LLM provider key for the models you'll run |
| `FABRO_DEV_TOKEN` | Optional — pre-set the dev token instead of reading the one written to `/storage` on first boot |
| `SESSION_SECRET` | 64-character hex string; required when the web UI is enabled |
| `GITHUB_APP_CLIENT_SECRET`, `GITHUB_APP_WEBHOOK_SECRET`, `GITHUB_APP_PRIVATE_KEY` | Only if you enable GitHub OAuth or the GitHub App integration |
### 4. Deploy
```bash
fly deploy
```
Fly pulls `ghcr.io/fabro-sh/fabro:nightly`, attaches the volume, and starts the Machine. The health check on `/health` must pass before traffic is routed.
## Accessing your Fabro server
Once the deploy is healthy, Fly exposes a `<app>.fly.dev` URL (or your custom domain). Two things to grab:
1. **The dev token** — on first boot, Fabro writes one to `/var/fabro/dev-token` and logs it. Read it via the Machine's shell:
```bash
fly ssh console -C "cat /var/fabro/dev-token"
```
Or tail the startup logs with `fly logs`.
2. **Point your local CLI at the server** — add the Fly URL to `~/.fabro/settings.toml`:
```toml title="~/.fabro/settings.toml"
[cli.target]
type = "http"
url = "https://<your-app>.fly.dev/api/v1"
```
Then commands like `fabro model list --server <url>` will hit your Fly instance.
See [Running the Fabro Server](/administration/deploy-server) for the full auth and CLI-pointing story.
## Redeploys and updates
`fly deploy` re-pulls the GHCR image on every run. `fly.toml` references the `:nightly` tag by default, so redeploying picks up the latest nightly automatically. To pin a specific version, edit `fly.toml` to reference `ghcr.io/fabro-sh/fabro:<version>` and redeploy. The `/storage` Volume survives redeploys, so runs and checkpoints persist.
## Caveats
- **`$PORT` is not injected.** Unlike Railway and Render, Fly does not set a `PORT` environment variable. The Fabro image binds to `$PORT` if set, otherwise `32276` — `fly.toml` pins `internal_port = 32276` so the default works. If you change `internal_port`, also `fly secrets set PORT=<n>` to match.
- **Volume is load-bearing and not replicated.** Fly's docs recommend at least two Volumes per app for redundancy, but Fabro's server is single-replica by design — one Machine owns `/storage`. Treat this like a traditional VPS: hardware failure means restoring from [Fly's volume snapshots](https://fly.io/docs/volumes/snapshots/) or a backup you manage.
- **Single Machine.** Don't `fly scale count` above 1 — a second Machine can't mount the same Volume, and the server assumes a single writer.
- **Architecture.** Fly Machines run x86_64 (amd64) by default. The `:nightly` tag is multi-arch, but the arm64 variant is not currently usable — stay on amd64.
- **Autostop is disabled.** `fly.toml` sets `auto_stop_machines = "off"` so the Machine stays up for the run queue. Leaving autostop enabled would pause Fabro when there's no HTTP traffic, stalling any in-flight runs.
## Next steps
<Columns cols={2}>
<Card title="Running the Fabro Server" icon="server" href="/administration/deploy-server">
Auth, dev tokens, submitting runs, and pointing the CLI at your deployment.
</Card>
<Card title="Server Configuration" icon="gear" href="/administration/server-configuration">
Full `settings.toml` reference — TLS, auth methods, concurrency, and more.
</Card>
</Columns>

View file

@ -1,6 +1,6 @@
---
title: "Railway"
description: "Deploy Fabro to Railway from the Dockerfile, with a persistent Volume for state"
description: "Deploy Fabro to Railway from the prebuilt GHCR image, with a persistent Volume for state"
---
<Warning>
@ -11,9 +11,9 @@ description: "Deploy Fabro to Railway from the Dockerfile, with a persistent Vol
## One-click deploy
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/new/template?template=https%3A%2F%2Fgithub.com%2Ffabro-sh%2Ffabro)
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/UcEy5m?referralCode=E5TucU&utm_medium=integration&utm_source=template&utm_campaign=generic)
The button launches Railway's new-project flow pointed at this repo. `railway.toml` instructs Railway to build `Dockerfile.deploy`, which is a thin wrapper that pulls `ghcr.io/fabro-sh/fabro:nightly` — no Rust compilation on Railway's builder, so deploys complete in seconds.
The button launches Railway's template flow, which deploys the pre-built `ghcr.io/fabro-sh/fabro:nightly` image directly from GHCR — no Rust compilation on Railway's builder, so deploys complete in seconds.
## First-deploy checklist
@ -59,11 +59,11 @@ Once the deploy is healthy, Railway exposes a `*.up.railway.app` URL (or your cu
Then commands like `fabro model list --server <url>` will hit your Railway instance.
See [Running the Fabro Server](/administration/deploy-server) for the full auth and CLI-pointing story.
See [Server Operations](/reference/server-operations) for the full auth and CLI-pointing story.
## Redeploys and updates
Railway re-pulls the GHCR image on every deploy. `Dockerfile.deploy` references the `:nightly` tag by default, so redeploying picks up the latest nightly automatically. To pin a specific version, edit `Dockerfile.deploy` to reference `ghcr.io/fabro-sh/fabro:<version>` and redeploy. The `/storage` Volume survives redeploys, so runs and checkpoints persist.
Railway re-pulls the GHCR image on every deploy. The template uses the `:nightly` tag by default, so redeploying picks up the latest nightly automatically. To pin a specific version, change the image in **Service → Settings → Source** to `ghcr.io/fabro-sh/fabro:<version>` and redeploy. The `/storage` Volume survives redeploys, so runs and checkpoints persist.
## Caveats
@ -74,7 +74,7 @@ Railway re-pulls the GHCR image on every deploy. `Dockerfile.deploy` references
## Next steps
<Columns cols={2}>
<Card title="Running the Fabro Server" icon="server" href="/administration/deploy-server">
<Card title="Server Operations" icon="server" href="/reference/server-operations">
Auth, dev tokens, submitting runs, and pointing the CLI at your deployment.
</Card>
<Card title="Server Configuration" icon="gear" href="/administration/server-configuration">

View file

@ -1,89 +0,0 @@
---
title: "Render"
description: "Deploy Fabro to Render via the render.yaml blueprint, with a persistent disk for state"
---
<Warning>
The server interface is in private early access. Contact [bryan@qlty.sh](mailto:bryan@qlty.sh) if you're interested in trying it.
</Warning>
[Render](https://render.com) can host the Fabro server by pulling the pre-built image published to GHCR. The repo ships a `render.yaml` blueprint that provisions the service and a persistent disk for `/storage` in one click, so your runs, checkpoints, and sessions survive redeploys.
## One-click deploy
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/fabro-sh/fabro)
The button opens Render's blueprint flow pointed at this repo. `render.yaml` instructs Render to build `Dockerfile.deploy`, which is a thin wrapper that pulls `ghcr.io/fabro-sh/fabro:nightly` — no Rust compilation on Render's builder, so deploys complete in seconds.
The blueprint declares:
- A `web` service (Docker runtime) on Render's `starter` plan
- A 1 GB persistent disk mounted at `/storage`
- A health check on `/health`
## First-deploy checklist
The blueprint covers the infrastructure; a few pieces still need to be wired up after the first deploy.
### 1. Confirm the disk is attached
Fabro writes all persistent state — run history, checkpoints, sessions, the default token, and JWT keys — under `/storage`. The blueprint provisions a 1 GB disk at that path automatically. Verify in **Service → Disks** that `fabro-storage` is present and mounted at `/storage`. Grow the size later from the same page if needed.
<Note>
Render persistent disks require a paid plan (`starter` or higher). The free tier does not support disks, so the blueprint selects `starter` by default.
</Note>
### 2. Confirm the target port
Render sets `$PORT` automatically and expects the container to bind to it. The Fabro image honors `$PORT` and falls back to `32276`, so Render's default HTTP routing works without any manual port configuration.
### 3. Set required environment variables
Add variables in **Service → Environment** as needed. The [Server Configuration](/administration/server-configuration) reference has the full list; the minimum useful set:
| Variable | Purpose |
|---|---|
| `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GEMINI_API_KEY` / ... | At least one LLM provider key for the models you'll run |
| `FABRO_DEV_TOKEN` | Optional — pre-set the dev token instead of reading the one written to `/storage` on first boot |
| `SESSION_SECRET` | 64-character hex string; required when the web UI is enabled |
| `GITHUB_APP_CLIENT_SECRET`, `GITHUB_APP_WEBHOOK_SECRET`, `GITHUB_APP_PRIVATE_KEY` | Only if you enable GitHub OAuth or the GitHub App integration |
No `.env` file is auto-loaded inside the container; everything comes from Render's environment.
## Accessing your Fabro server
Once the deploy is healthy, Render exposes a `*.onrender.com` URL (or your custom domain). Two things to grab:
1. **The dev token** — on first boot, Fabro writes one to `/var/fabro/dev-token` and logs it. Find it in **Service → Logs** in the Render dashboard, or open **Service → Shell** and run `cat /var/fabro/dev-token`.
2. **Point your local CLI at the server** — add the Render URL to `~/.fabro/settings.toml`:
```toml title="~/.fabro/settings.toml"
[cli.target]
type = "http"
url = "https://<your-service>.onrender.com/api/v1"
```
Then commands like `fabro model list --server <url>` will hit your Render instance.
See [Running the Fabro Server](/administration/deploy-server) for the full auth and CLI-pointing story.
## Redeploys and updates
Render re-pulls the GHCR image on every deploy. `Dockerfile.deploy` references the `:nightly` tag by default, so redeploying picks up the latest nightly automatically. To pin a specific version, edit `Dockerfile.deploy` to reference `ghcr.io/fabro-sh/fabro:<version>` and redeploy. The `/storage` disk survives redeploys, so runs and checkpoints persist.
## Caveats
- **Disk is load-bearing.** `/storage` holds the dev token and JWT signing keys in addition to runs and checkpoints. Do not detach or resize it destructively once the service is in use.
- **Single replica.** Fabro's server currently assumes one process owns `/storage`. Don't scale the service to multiple instances.
- **Architecture.** Render runs x86_64 (amd64) containers. The `:nightly` tag is multi-arch, but the arm64 variant is not currently usable — stay on amd64, which is Render's default.
## Next steps
<Columns cols={2}>
<Card title="Running the Fabro Server" icon="server" href="/administration/deploy-server">
Auth, dev tokens, submitting runs, and pointing the CLI at your deployment.
</Card>
<Card title="Server Configuration" icon="gear" href="/administration/server-configuration">
Full `settings.toml` reference — reverse-proxy TLS, auth methods, concurrency, and more.
</Card>
</Columns>

View file

@ -0,0 +1,42 @@
---
title: "Deployment"
description: "Choose where the Fabro server runs: on your laptop or self-hosted on a server"
---
<Warning>
The server interface is in private early access. Contact [bryan@qlty.sh](mailto:bryan@qlty.sh) if you're interested in trying it.
</Warning>
Fabro runs as a server. The CLI authenticates and communicates with that server, whether the server is running on your laptop or on a remote host. Choose where the server runs based on how you use Fabro.
## Two deployment modes
| | Local | Self-hosted |
|---|---|---|
| **Where the server runs** | Your laptop | A host you operate (Docker container) |
| **How to start it** | `fabro server start` | `docker compose up -d` |
| **Best for** | Solo use, getting started, learning | Teams, production, 24/7 workflows |
| **Trade-off** | Workflows pause when your laptop sleeps or shuts off | You operate the host |
Both modes use the same image, the same workflow engine, and the same CLI. The only difference is where the server process lives.
## Local
Run `fabro server start` on your machine. State persists under `~/.fabro/`. The CLI talks to it over a Unix socket by default. This is the right mode for solo use and getting started — no deployment required.
When your laptop sleeps or shuts off, in-flight workflows pause until the laptop wakes again. For workflows that need to run 24/7 or for teams sharing a single instance, self-host.
See [Server Operations](/reference/server-operations) for starting the server, the install wizard, authentication, and pointing the CLI at it.
## Self-hosted
For team use, production workflows, or running 24/7, self-host the server as a Docker container. The recommended approach is `docker compose` for a single host, or any cloud container service (ECS, Cloud Run, Kubernetes) using the same image with the same requirements.
<Columns cols={2}>
<Card title="Self-host with Docker" icon="docker" href="/administration/self-host-docker">
Compose-first walkthrough. Same image works on ECS, Cloud Run, and Kubernetes.
</Card>
<Card title="Deploy to Railway" icon="train" href="/administration/deploy-railway">
One-click managed shortcut for the same Docker image.
</Card>
</Columns>

View file

@ -0,0 +1,143 @@
---
title: "Self-host with Docker"
description: "Run the Fabro server as a Docker container with docker compose, ECS, or any cloud container service"
---
<Warning>
The server interface is in private early access. Contact [bryan@qlty.sh](mailto:bryan@qlty.sh) if you're interested in trying it.
</Warning>
The supported deployment artifact is the official Fabro image at `ghcr.io/fabro-sh/fabro`. Everything else — `docker compose`, ECS, Cloud Run, Kubernetes, Railway — is just running this image somewhere with the right requirements.
## Requirements
| Requirement | Value |
|---|---|
| **Image** | `ghcr.io/fabro-sh/fabro:nightly` (multi-arch; pin a version for production) |
| **Persistent volume** | Mount at `/storage`. Stores run history, checkpoints, sessions, the dev token, and JWT keys. |
| **Port** | The container binds to `$PORT` (default `32276`). Expose it. |
| **LLM provider key** | At least one of `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, ... |
| **Replicas** | One. The server expects exclusive ownership of `/storage`. |
## Quickstart with docker compose
The repo ships a `docker-compose.yaml` at the root. Clone the repo (or copy the file), create a `.env` with at least one provider key, and start it:
```bash
git clone https://github.com/fabro-sh/fabro.git
cd fabro
cp .env.example .env
# edit .env and set at least ANTHROPIC_API_KEY (or another provider key)
docker compose up -d
```
The compose file:
- Pulls `ghcr.io/fabro-sh/fabro:nightly`
- Creates a named volume `fabro-storage` mounted at `/storage`
- Mounts `/var/run/docker.sock` so Fabro can spawn sandbox containers on the host daemon
- Exposes port `32276`
- Loads environment from `.env` if present
<Warning>
Mounting `/var/run/docker.sock` gives the container host-root-equivalent access. Only use the bundled compose service in trusted, single-tenant deployments. See [Sandboxing](/administration/sandboxing) for the threat model.
</Warning>
After the container is healthy, finish setup in your browser following the [install wizard](/reference/server-operations#first-run-web-install-wizard).
### Adding a reverse proxy with TLS
For a production deployment exposed to the internet, layer the `docker-compose.prod.yaml` overlay on top. It adds a [Caddy](https://caddyserver.com) reverse proxy that terminates TLS (auto-provisioning a Let's Encrypt certificate) and forwards to Fabro:
```bash
# In .env
FABRO_DOMAIN=fabro.example.com
# Then bring the stack up
docker compose -f docker-compose.yaml -f docker-compose.prod.yaml up -d
```
Leave `FABRO_DOMAIN` unset to serve plain HTTP on `localhost`.
## Required environment variables
At minimum, set one LLM provider key in `.env`:
```bash title=".env"
ANTHROPIC_API_KEY=sk-ant-...
```
For the web UI you also need a session secret:
```bash
SESSION_SECRET=<64-character hex string>
```
Generate one with `openssl rand -hex 32`.
Optional:
| Variable | Purpose |
|---|---|
| `FABRO_DEV_TOKEN` | Pre-set the dev token instead of reading the one written to `/storage` on first boot |
| `GITHUB_APP_CLIENT_SECRET`, `GITHUB_APP_WEBHOOK_SECRET`, `GITHUB_APP_PRIVATE_KEY` | Only if you enable GitHub OAuth or the GitHub App integration |
| `FABRO_DOMAIN` | Public hostname when using the Caddy reverse-proxy overlay |
See [Server Configuration](/administration/server-configuration) for the full settings reference, and [`.env.example`](https://github.com/fabro-sh/fabro/blob/main/.env.example) for the complete list.
## Cloud container services
The same image works on any container orchestrator that supports the requirements above. Common patterns:
- **AWS ECS / Fargate** — Task definition referencing `ghcr.io/fabro-sh/fabro:nightly`, EFS volume mounted at `/storage`, port `32276` published, environment variables for keys.
- **Google Cloud Run** — Cloud Run with a backed volume mount at `/storage`. Pin minimum instances to 1; scale-to-zero interrupts running workflows.
- **Kubernetes** — One-replica `StatefulSet` (not Deployment) with a `PersistentVolumeClaim` mounted at `/storage`. Expose via Service + Ingress.
In all cases: single replica, persistent `/storage`, expose `$PORT`, set provider keys.
## Pinning a version
`docker-compose.yaml` uses `:nightly` by default, so `docker compose pull && docker compose up -d` picks up the latest nightly. To pin a specific version, change the `image:` line to `ghcr.io/fabro-sh/fabro:<version>`.
Release artifacts ship with [SLSA Build Provenance](/reference/verifying-releases) attestations you can verify with `gh attestation verify`.
## Pointing the CLI at your server
Once the container is running, install the CLI on your local machine and point it at the server:
```toml title="~/.fabro/settings.toml"
[cli.target]
type = "http"
url = "https://fabro.example.com/api/v1"
```
For dev-token auth, save the token in the CLI auth store:
```bash
fabro auth login --server https://fabro.example.com/api/v1 --dev-token fabro_dev_...
```
See [Server Operations](/reference/server-operations#pointing-the-cli-at-a-server) for the full CLI-target options.
## Caveats
- **Volume is load-bearing.** Without a persistent volume at `/storage`, redeploys silently wipe all state — including the dev token and JWT signing keys.
- **Single replica.** The server expects exclusive ownership of `/storage`. Don't scale to multiple replicas.
- **Architecture.** The `:nightly` tag is multi-arch. The amd64 variant is the most heavily tested.
## Next steps
<Columns cols={2}>
<Card title="Server Operations" icon="server" href="/reference/server-operations">
Install wizard, web UI, authentication, demo mode, and pointing the CLI at the server.
</Card>
<Card title="Server Configuration" icon="gear" href="/administration/server-configuration">
Full settings.toml reference — auth, reverse-proxy TLS, run defaults, and more.
</Card>
<Card title="Deploy to Railway" icon="train" href="/administration/deploy-railway">
One-click managed shortcut for the same Docker image.
</Card>
<Card title="Sandboxing" icon="shield" href="/administration/sandboxing">
The Docker sandbox provider's security model and trust assumptions.
</Card>
</Columns>

View file

@ -33,7 +33,7 @@ It checks:
**Port already in use** — Change the port with `fabro server start --port 3001` or stop the conflicting process.
**SSE streams disconnecting** — If using a reverse proxy, ensure buffering is disabled and the connection timeout is long enough for workflow runs. See the [DigitalOcean reverse-proxy example](/administration/deploy-digital-ocean).
**SSE streams disconnecting** — If using a reverse proxy, ensure buffering is disabled and the connection timeout is long enough for workflow runs.
**Run config validation errors** — Use `fabro preflight` to validate without executing:

View file

@ -16,7 +16,7 @@ Fabro has two interfaces, both backed by the same workflow engine:
- **Direct CLI runs** (`fabro run`) — Run a single workflow synchronously in your terminal. Best for local development, one-off runs, and CI/CD.
- **Server interface** (`fabro server start`) — Start an HTTP API server with a web UI, concurrent run scheduling, and team access. Best for production use and running at scale.
Both interfaces parse the same Graphviz files, use the same execution engine, and support the same sandbox providers. See [Running the Fabro Server](/administration/deploy-server) for a detailed comparison and setup guide, or [Architecture](/reference/architecture) for internals.
Both interfaces parse the same Graphviz files, use the same execution engine, and support the same sandbox providers. See [Deployment](/administration/deployment) for where the server runs and [Server Operations](/reference/server-operations) for operating it, or [Architecture](/reference/architecture) for internals.
## Author time

View file

@ -108,6 +108,7 @@
"reference/run-directory",
"reference/sdk",
"reference/architecture",
"reference/server-operations",
"reference/verifying-releases",
"administration/server-configuration",
"administration/troubleshooting",
@ -156,11 +157,9 @@
"group": "Deployment",
"icon": "server",
"pages": [
"administration/deploy-server",
"administration/deploy-railway",
"administration/deploy-render",
"administration/deploy-fly-io",
"administration/deploy-digital-ocean"
"administration/deployment",
"administration/self-host-docker",
"administration/deploy-railway"
]
}
]

View file

@ -4,10 +4,7 @@ description: "Get up and running with Fabro"
---
<Note>
Fabro has two modes:
- **Standalone mode** — Run workflows directly from the CLI. This is what the quick start covers below.
- **Server mode** — An API server with a web UI for launching and managing workflow runs at scale. See [Server Mode](/administration/deploy-server) for details.
Fabro runs as a server. This quickstart runs everything locally on your laptop — no deployment required. To self-host the server for a team or 24/7 workflows, see [Deployment](/administration/deployment).
</Note>
## Supported platforms
@ -54,7 +51,7 @@ Release binaries and the multi-arch Docker image ship with [SLSA Build Provenanc
</Tip>
<Note>
Setting up server mode instead? Run `fabro server start` to finish setup in a browser-based wizard. See [Running the Fabro Server](/administration/deploy-server) for the full flow.
Self-hosting the server? See [Self-host with Docker](/administration/self-host-docker) and [Server Operations](/reference/server-operations) for the install wizard, auth, and CLI-pointing.
</Note>
## Initialize your project

View file

@ -89,4 +89,4 @@ The UI provides:
## Comparison
See [Server Mode](/administration/deploy-server#standalone-vs-server-mode) for a full feature comparison between standalone and server mode.
See [Deployment](/administration/deployment) for where the Fabro server runs and the trade-offs between local and self-hosted modes.

View file

@ -1,28 +1,13 @@
---
title: "Running the Fabro Server"
description: "Run Fabro as an API server with a web UI, concurrent runs, and team access"
title: "Server Operations"
description: "Operate the Fabro server: starting, install wizard, auth, web UI, and pointing the CLI at it"
---
<Warning>
The server interface is in private early access. Contact [bryan@qlty.sh](mailto:bryan@qlty.sh) if you're interested in trying it.
</Warning>
Fabro has two interfaces to the same workflow engine. You can run a workflow directly in the CLI with `fabro run`, or start the HTTP server with `fabro server start` to queue runs, stream events, and serve the web UI.
Both interfaces use the same workflow engine, the same Graphviz files, and the same sandbox providers. The difference is how you interact with them.
## Direct CLI Runs vs. Server Interface
| | Direct CLI runs | Server interface |
|---|---|---|
| **Command** | `fabro run workflow.fabro` | `fabro server start` |
| **Best for** | Local development, one-off runs, CI/CD | Production, team use, running at scale |
| **Execution** | Synchronous, one run per process | Asynchronous, queued with configurable concurrency |
| **Human-in-the-loop** | Terminal prompts | Web UI or HTTP endpoints |
| **Events** | Printed to stderr | Streamed via SSE |
| **Persistence** | Checkpoint files only | Persistent run store + checkpoint files |
| **Web UI** | Not available | Full React interface |
| **Authentication** | None | Dev token and/or GitHub OAuth |
This page covers operating the Fabro server once it's running, whether locally on your laptop or self-hosted in a container. For where to run it, see [Deployment](/administration/deployment).
## Starting the server
@ -68,7 +53,7 @@ See [Server Configuration](/administration/server-configuration) for the full `s
## Submitting runs
In the server interface, workflows are submitted via the REST API and executed in the background. The exact request body is documented in the API reference:
Workflows are submitted via the REST API and executed in the background. The exact request body is documented in the API reference:
```bash
curl -X POST http://localhost:3000/api/v1/runs
@ -110,7 +95,7 @@ The API streams run events via [Server-Sent Events (SSE)](/api-reference/runs/st
## Human-in-the-loop
In the server interface, human-in-the-loop questions are served over HTTP instead of terminal prompts. The engine blocks the current stage until an answer is submitted, then continues execution. See the [list questions](/api-reference/human-in-the-loop/list-run-questions) and [submit answer](/api-reference/human-in-the-loop/submit-run-answer) API reference pages.
Human-in-the-loop questions are served over HTTP. The engine blocks the current stage until an answer is submitted, then continues execution. See the [list questions](/api-reference/human-in-the-loop/list-run-questions) and [submit answer](/api-reference/human-in-the-loop/submit-run-answer) API reference pages.
## Authentication
@ -162,16 +147,16 @@ See [User Configuration](/reference/user-configuration#cli-target-section) for t
## Next steps
<Columns cols={2}>
<Card title="Deployment" icon="server" href="/administration/deployment">
Choose where the server runs: laptop or self-hosted Docker container.
</Card>
<Card title="Server Configuration" icon="gear" href="/administration/server-configuration">
Full settings.toml reference — authentication, reverse-proxy TLS, run defaults, and more.
</Card>
<Card title="Deploy to Railway" icon="train" href="/administration/deploy-railway">
Step-by-step guide for deploying Fabro on Railway.
</Card>
<Card title="API Reference" icon="code" href="/api-reference/overview">
REST API for submitting runs, streaming events, and managing resources.
</Card>
<Card title="How Fabro Works" icon="lightbulb" href="/core-concepts/how-fabro-works">
The workflow engine that powers both interfaces.
The workflow engine and architecture.
</Card>
</Columns>

View file

@ -1,35 +0,0 @@
# Fly.io deployment config for the Fabro server.
#
# Pulls the multi-arch image published to GHCR instead of building from
# source on every deploy. Persists state to a Fly Volume mounted at
# /storage. The Fabro binary binds to $PORT or 32276, so matching
# internal_port to 32276 avoids needing to wire PORT as a secret.
#
# First-time setup:
# fly launch --copy-config --no-deploy # adopts this file; sets app name/region
# fly volumes create storage --size 1 # required — fly.toml cannot create volumes
# fly secrets set ANTHROPIC_API_KEY=... SESSION_SECRET=...
# fly deploy
app = "fabro"
primary_region = "ord"
[build]
image = "ghcr.io/fabro-sh/fabro:nightly"
[[mounts]]
source = "storage"
destination = "/storage"
[http_service]
internal_port = 32276
force_https = true
auto_stop_machines = "off"
auto_start_machines = false
[[http_service.checks]]
path = "/health"
method = "GET"
interval = "30s"
timeout = "5s"
grace_period = "10s"

View file

@ -36,9 +36,10 @@ pub use error::{Error, Result};
pub use local::LocalSandbox;
pub use read_guard::ReadBeforeWriteSandbox;
pub use sandbox::{
CommandOutputCallback, DirEntry, ExecResult, ExecStreamingResult, GitRunInfo, GitSetupIntent,
GrepOptions, Sandbox, SandboxEvent, SandboxEventCallback, format_lines_numbered,
git_push_via_exec, setup_git_via_exec, shell_quote,
CommandOutputCallback, DEFAULT_EXEC_OUTPUT_TAIL_BYTES, DirEntry, ExecResult,
ExecStreamingResult, GitRunInfo, GitSetupIntent, GrepOptions, Sandbox, SandboxEvent,
SandboxEventCallback, format_lines_numbered, git_push_via_exec, redacted_output_tail,
setup_git_via_exec, shell_quote,
};
pub use sandbox_provider::SandboxProvider;
pub use sandbox_record::SandboxRecord;

View file

@ -451,15 +451,7 @@ impl ExecResult {
&self,
max_bytes_per_stream: usize,
) -> Option<fabro_types::ExecOutputTail> {
let (stdout, stdout_truncated) = redacted_tail(&self.stdout, max_bytes_per_stream);
let (stderr, stderr_truncated) = redacted_tail(&self.stderr, max_bytes_per_stream);
let tail = fabro_types::ExecOutputTail {
stdout,
stderr,
stdout_truncated,
stderr_truncated,
};
(!tail.is_empty()).then_some(tail)
redacted_output_tail(&self.stdout, &self.stderr, max_bytes_per_stream)
}
pub fn default_redacted_output_tail(&self) -> Option<fabro_types::ExecOutputTail> {
@ -488,6 +480,26 @@ impl ExecResult {
}
}
/// Build a redacted `ExecOutputTail` from raw stdout/stderr without
/// fabricating a synthetic `ExecResult`. Pass `""` for either stream that
/// isn't relevant. Returns `None` when both streams are empty.
#[must_use]
pub fn redacted_output_tail(
stdout: &str,
stderr: &str,
max_bytes_per_stream: usize,
) -> Option<fabro_types::ExecOutputTail> {
let (stdout, stdout_truncated) = redacted_tail(stdout, max_bytes_per_stream);
let (stderr, stderr_truncated) = redacted_tail(stderr, max_bytes_per_stream);
let tail = fabro_types::ExecOutputTail {
stdout,
stderr,
stdout_truncated,
stderr_truncated,
};
(!tail.is_empty()).then_some(tail)
}
fn redacted_tail(text: &str, max_bytes: usize) -> (Option<String>, bool) {
if text.is_empty() || max_bytes == 0 {
return (None, !text.is_empty());

View file

@ -33,11 +33,11 @@ fn security_doc_does_not_require_jwt_keys_for_the_current_web_flow() {
}
#[test]
fn deploy_server_doc_links_to_the_cli_target_section_slug() {
let deploy_server = read_doc("docs/public/administration/deploy-server.mdx");
fn server_operations_doc_links_to_the_cli_target_section_slug() {
let server_operations = read_doc("docs/public/reference/server-operations.mdx");
assert!(
deploy_server.contains("/reference/user-configuration#cli-target-section"),
"deploy-server doc should link to the Mintlify slug for the [cli.target] section"
server_operations.contains("/reference/user-configuration#cli-target-section"),
"server-operations doc should link to the Mintlify slug for the [cli.target] section"
);
}

View file

@ -326,21 +326,14 @@ fn parse_fast_import_mark(stdout: &str) -> Result<String, SandboxMetadataError>
"git fast-import did not report imported commit mark (stdout_bytes={})",
stdout.len()
),
exec_output_tail: stdout_output_tail(stdout),
exec_output_tail: fabro_sandbox::redacted_output_tail(
stdout,
"",
fabro_sandbox::DEFAULT_EXEC_OUTPUT_TAIL_BYTES,
),
})
}
fn stdout_output_tail(stdout: &str) -> Option<fabro_types::ExecOutputTail> {
fabro_sandbox::ExecResult {
stdout: stdout.to_string(),
stderr: String::new(),
exit_code: Some(0),
termination: fabro_types::CommandTermination::Exited,
duration_ms: 0,
}
.default_redacted_output_tail()
}
fn fast_import_ident(author: &GitAuthor) -> String {
let name = author
.name

View file

@ -1,8 +0,0 @@
[build]
builder = "dockerfile"
dockerfilePath = "Dockerfile.deploy"
[deploy]
healthcheckPath = "/health"
restartPolicyType = "ALWAYS"
restartPolicyMaxRetries = 5

View file

@ -1,11 +0,0 @@
services:
- type: web
name: fabro
runtime: docker
dockerfilePath: ./Dockerfile.deploy
plan: starter
healthCheckPath: /health
disk:
name: fabro-storage
mountPath: /storage
sizeGB: 1