diff --git a/Dockerfile.deploy b/Dockerfile.deploy deleted file mode 100644 index f75a00956..000000000 --- a/Dockerfile.deploy +++ /dev/null @@ -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 diff --git a/README.md b/README.md index d2d663b6e..0a3caeec9 100644 --- a/README.md +++ b/README.md @@ -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. --- diff --git a/docker-compose.yaml b/docker-compose.yaml index efd89d286..e3ee394c9 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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: diff --git a/docs/public/administration/deploy-digital-ocean.mdx b/docs/public/administration/deploy-digital-ocean.mdx deleted file mode 100644 index 6096bc30a..000000000 --- a/docs/public/administration/deploy-digital-ocean.mdx +++ /dev/null @@ -1,137 +0,0 @@ ---- -title: "DigitalOcean" -description: "Deploy Fabro to a DigitalOcean Droplet with docker compose and Caddy for automatic TLS" ---- - - - The server interface is in private early access. Contact [bryan@qlty.sh](mailto:bryan@qlty.sh) if you're interested in trying it. - - -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 - - - **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. - - -## 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 -``` - -`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@ -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:///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 ` 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 - - - - Auth, dev tokens, submitting runs, and pointing the CLI at your deployment. - - - Full `settings.toml` reference — reverse-proxy TLS, auth methods, concurrency, and more. - - diff --git a/docs/public/administration/deploy-fly-io.mdx b/docs/public/administration/deploy-fly-io.mdx deleted file mode 100644 index 51bc1d21f..000000000 --- a/docs/public/administration/deploy-fly-io.mdx +++ /dev/null @@ -1,108 +0,0 @@ ---- -title: "Fly.io" -description: "Deploy Fabro to Fly.io from the prebuilt GHCR image, with a Fly Volume for state" ---- - - - The server interface is in private early access. Contact [bryan@qlty.sh](mailto:bryan@qlty.sh) if you're interested in trying it. - - -[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 -``` - -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 `.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://.fly.dev/api/v1" - ``` - - Then commands like `fabro model list --server ` 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:` 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=` 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 - - - - Auth, dev tokens, submitting runs, and pointing the CLI at your deployment. - - - Full `settings.toml` reference — TLS, auth methods, concurrency, and more. - - diff --git a/docs/public/administration/deploy-railway.mdx b/docs/public/administration/deploy-railway.mdx index c932246fb..a6082f107 100644 --- a/docs/public/administration/deploy-railway.mdx +++ b/docs/public/administration/deploy-railway.mdx @@ -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" --- @@ -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 ` 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:` 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:` 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 - + Auth, dev tokens, submitting runs, and pointing the CLI at your deployment. diff --git a/docs/public/administration/deploy-render.mdx b/docs/public/administration/deploy-render.mdx deleted file mode 100644 index ff8837365..000000000 --- a/docs/public/administration/deploy-render.mdx +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: "Render" -description: "Deploy Fabro to Render via the render.yaml blueprint, with a persistent disk for state" ---- - - - The server interface is in private early access. Contact [bryan@qlty.sh](mailto:bryan@qlty.sh) if you're interested in trying it. - - -[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. - - - Render persistent disks require a paid plan (`starter` or higher). The free tier does not support disks, so the blueprint selects `starter` by default. - - -### 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://.onrender.com/api/v1" - ``` - - Then commands like `fabro model list --server ` 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:` 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 - - - - Auth, dev tokens, submitting runs, and pointing the CLI at your deployment. - - - Full `settings.toml` reference — reverse-proxy TLS, auth methods, concurrency, and more. - - diff --git a/docs/public/administration/deployment.mdx b/docs/public/administration/deployment.mdx new file mode 100644 index 000000000..fd88c037d --- /dev/null +++ b/docs/public/administration/deployment.mdx @@ -0,0 +1,42 @@ +--- +title: "Deployment" +description: "Choose where the Fabro server runs: on your laptop or self-hosted on a server" +--- + + + The server interface is in private early access. Contact [bryan@qlty.sh](mailto:bryan@qlty.sh) if you're interested in trying it. + + +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. + + + + Compose-first walkthrough. Same image works on ECS, Cloud Run, and Kubernetes. + + + One-click managed shortcut for the same Docker image. + + diff --git a/docs/public/administration/self-host-docker.mdx b/docs/public/administration/self-host-docker.mdx new file mode 100644 index 000000000..ebdc8c180 --- /dev/null +++ b/docs/public/administration/self-host-docker.mdx @@ -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" +--- + + + The server interface is in private early access. Contact [bryan@qlty.sh](mailto:bryan@qlty.sh) if you're interested in trying it. + + +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 + + + 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. + + +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:`. + +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 + + + + Install wizard, web UI, authentication, demo mode, and pointing the CLI at the server. + + + Full settings.toml reference — auth, reverse-proxy TLS, run defaults, and more. + + + One-click managed shortcut for the same Docker image. + + + The Docker sandbox provider's security model and trust assumptions. + + diff --git a/docs/public/administration/troubleshooting.mdx b/docs/public/administration/troubleshooting.mdx index adae24e83..1bd5d9b61 100644 --- a/docs/public/administration/troubleshooting.mdx +++ b/docs/public/administration/troubleshooting.mdx @@ -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: diff --git a/docs/public/core-concepts/how-fabro-works.mdx b/docs/public/core-concepts/how-fabro-works.mdx index 6b724d182..de33adec5 100644 --- a/docs/public/core-concepts/how-fabro-works.mdx +++ b/docs/public/core-concepts/how-fabro-works.mdx @@ -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 diff --git a/docs/public/docs.json b/docs/public/docs.json index 869f98d63..f69811618 100644 --- a/docs/public/docs.json +++ b/docs/public/docs.json @@ -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" ] } ] diff --git a/docs/public/getting-started/quick-start.mdx b/docs/public/getting-started/quick-start.mdx index 6445e64b2..9de4f2f06 100644 --- a/docs/public/getting-started/quick-start.mdx +++ b/docs/public/getting-started/quick-start.mdx @@ -4,10 +4,7 @@ description: "Get up and running with Fabro" --- -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). ## Supported platforms @@ -54,7 +51,7 @@ Release binaries and the multi-arch Docker image ship with [SLSA Build Provenanc -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. ## Initialize your project diff --git a/docs/public/reference/architecture.mdx b/docs/public/reference/architecture.mdx index 32ee05428..bc7e2de04 100644 --- a/docs/public/reference/architecture.mdx +++ b/docs/public/reference/architecture.mdx @@ -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. diff --git a/docs/public/administration/deploy-server.mdx b/docs/public/reference/server-operations.mdx similarity index 78% rename from docs/public/administration/deploy-server.mdx rename to docs/public/reference/server-operations.mdx index 684ec2832..94b4c4dab 100644 --- a/docs/public/administration/deploy-server.mdx +++ b/docs/public/reference/server-operations.mdx @@ -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" --- The server interface is in private early access. Contact [bryan@qlty.sh](mailto:bryan@qlty.sh) if you're interested in trying it. -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 + + Choose where the server runs: laptop or self-hosted Docker container. + Full settings.toml reference — authentication, reverse-proxy TLS, run defaults, and more. - - Step-by-step guide for deploying Fabro on Railway. - REST API for submitting runs, streaming events, and managing resources. - The workflow engine that powers both interfaces. + The workflow engine and architecture. diff --git a/fly.toml b/fly.toml deleted file mode 100644 index 58a9f8fdd..000000000 --- a/fly.toml +++ /dev/null @@ -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" diff --git a/lib/crates/fabro-sandbox/src/lib.rs b/lib/crates/fabro-sandbox/src/lib.rs index 1fc600583..adad75bf2 100644 --- a/lib/crates/fabro-sandbox/src/lib.rs +++ b/lib/crates/fabro-sandbox/src/lib.rs @@ -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; diff --git a/lib/crates/fabro-sandbox/src/sandbox.rs b/lib/crates/fabro-sandbox/src/sandbox.rs index 0abe33235..f14dc1629 100644 --- a/lib/crates/fabro-sandbox/src/sandbox.rs +++ b/lib/crates/fabro-sandbox/src/sandbox.rs @@ -451,15 +451,7 @@ impl ExecResult { &self, max_bytes_per_stream: usize, ) -> Option { - 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 { @@ -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 { + 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, bool) { if text.is_empty() || max_bytes == 0 { return (None, !text.is_empty()); diff --git a/lib/crates/fabro-server/tests/it/api/docs.rs b/lib/crates/fabro-server/tests/it/api/docs.rs index b8a120f82..54eae87bb 100644 --- a/lib/crates/fabro-server/tests/it/api/docs.rs +++ b/lib/crates/fabro-server/tests/it/api/docs.rs @@ -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" ); } diff --git a/lib/crates/fabro-workflow/src/sandbox_metadata.rs b/lib/crates/fabro-workflow/src/sandbox_metadata.rs index eca510284..c022970ba 100644 --- a/lib/crates/fabro-workflow/src/sandbox_metadata.rs +++ b/lib/crates/fabro-workflow/src/sandbox_metadata.rs @@ -326,21 +326,14 @@ fn parse_fast_import_mark(stdout: &str) -> Result "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_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 diff --git a/railway.toml b/railway.toml deleted file mode 100644 index 185d7486a..000000000 --- a/railway.toml +++ /dev/null @@ -1,8 +0,0 @@ -[build] -builder = "dockerfile" -dockerfilePath = "Dockerfile.deploy" - -[deploy] -healthcheckPath = "/health" -restartPolicyType = "ALWAYS" -restartPolicyMaxRetries = 5 diff --git a/render.yaml b/render.yaml deleted file mode 100644 index be4c88043..000000000 --- a/render.yaml +++ /dev/null @@ -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