# Single-box deploy (many projects)

This layout hosts **30 to 50 small projects (for example one per mobile app or
game) on one VPS** in a 6 to 8 GB RAM budget. It is the layout Scribase itself
is operated on. For a plain one-tenant stack, see
[Self-hosting](https://docs.scribase.com/docs/self-hosting.md). The full operator reference, with memory
budgets and every overlay, is `DEPLOY.md` in the repository.

| File | Role |
|---|---|
| `deploy/compose/docker-compose.yml` | Base stack: control-plane Postgres, migrator, control API |
| `deploy/compose/docker-compose.single-box.yml` | Overlay: operator, activator, project router, restore-verify scratch database, memory caps |
| `deploy/compose/docker-compose.shared-services.yml` | One shared storage, realtime and edge-functions runtime for every project |
| `deploy/compose/docker-compose.postgres-cow.yml` | Default engine: one small Postgres 17 per environment on this box, branches as copy-on-write clones |
| `deploy/compose/docker-compose.neon-local.yml` | Optional: self-hosted Neon on the same box (instead of `postgres-cow`) |
| `deploy/compose/single-box.env.example` | Env template (variable names only, no values) |

## How it works

- **Every environment gets its own Postgres 17 on the box**
  (`SCRIBASE_DATA_ENGINE=postgres-cow`). A branch (preview, staging, a sandbox
  per agent session) is a copy-on-write clone of its parent's data directory:
  it takes milliseconds and only costs the blocks that later change. An idle
  environment's database is stopped after `SCRIBASE_POSTGRES_COW_SUSPEND_SECONDS`
  (default 900, like the API container) and started again by the next
  connection. The bundled `db` holds only the control plane. No cloud account
  is needed.
- **Managed Neon is optional.** Set `SCRIBASE_NEON_API_KEY` and new projects
  can use `neon-cloud` (one Neon project per Scribase project, 0.25 CU,
  suspended after 5 idle minutes, point-in-time restore). `scribase project
  move <org> <project> --engine postgres-cow|neon-cloud|neon-local` moves a
  project between engines and keeps its URL and keys.
- **One container per environment** runs GoTrue (`/auth/v1`) and PostgREST
  (`/rest/v1`). It **scales to zero**: the activator starts it on the first
  request and stops it after `SCRIBASE_PROJECT_IDLE_SECONDS` (default 900)
  without traffic. An idle project uses disk, not memory.
- **Storage, realtime and edge functions are shared**, multi-tenant services.
  Each function call runs in its own isolated Deno worker that only sees its
  environment's variables and secrets.
- **Every environment is served at `https://<host label>.<your domain>`**
  through the project router.
- **Operator sign-in is built in** (`SCRIBASE_AUTH_MODE=builtin`). No identity
  provider is needed. See [Built-in admin login](https://docs.scribase.com/docs/admin-login.md).

## 1. Build the images

```sh
sudo git clone <your scribase remote> /srv/src/scribase
cd /srv/src/scribase/deploy/compose

# Control-plane image (control API, operator, activator, CLI).
docker compose -f docker-compose.yml build migrate
# Per-environment runtime image (GoTrue + PostgREST).
docker compose -f docker-compose.yml -f docker-compose.single-box.yml \
  --profile build build project-runtime-image
```

## 2. Configure

```sh
sudo install -m 640 -o root -g deploy single-box.env.example /etc/products/scribase.env
sudoedit /etc/products/scribase.env
```

Fill every blank in the template:

- Control-plane passwords and keys (`openssl rand -hex 32` for each).
- `SCRIBASE_ORGANIZATION_ID`, the id of your first organization.
- `SCRIBASE_POSTGRES_COW_HOST_DIR` (default `/srv/scribase/pg-cow`): a
  directory on a filesystem that can clone files (XFS, btrfs or ZFS), owned by
  uid 10001, mode 0700. On an ext4 root, create an XFS image file and
  loop-mount it there (see [Copy-on-write storage](#copy-on-write-storage)).
- Optional: `SCRIBASE_NEON_API_KEY`, a Neon API key, to offer `neon-cloud` too.
- The `SHARED_*` keys for the shared storage and realtime services, and SMTP.
- `SCRIBASE_FUNCTIONS_ADMIN_KEY`, its own random value. The functions runtime
  refuses an admin key that equals a key functions can read.
- `SCRIBASE_RESTORE_VERIFY_DB_PASSWORD`, the password of the bundled
  `restore-verify-db` scratch server that Backup Verify restores into. Leave
  `SCRIBASE_RESTORE_VERIFY_DATABASE_URL` blank to use that server. Without a
  verification database, Backup Verify answers `503 verification_not_configured`.
- Leave `SCRIBASE_ADMIN_EMAIL` and `SCRIBASE_ADMIN_PASSWORD_HASH` blank to get a
  one-time setup token in the log, or pre-seed the administrator (see
  [Built-in admin login](https://docs.scribase.com/docs/admin-login.md)).

Keep every published port on `127.0.0.1`. Docker bypasses host firewalls such
as UFW.

## 3. DNS and TLS

- `api.<domain>` and `console.<domain>`: ordinary records pointing at the box.
- `*.<domain>`: one wildcard record to the box. Every environment is served at
  `https://<host label>.<domain>`.
- A wildcard certificate. HTTP-01 cannot issue one, so use DNS-01 or, behind
  Cloudflare, a Cloudflare Origin CA certificate for `*.<domain>, <domain>`
  with SSL mode "Full (strict)".
- Your TLS terminator forwards `api.` to `127.0.0.1:8081` and `*.` to
  `127.0.0.1:8010` (the project router).

## 4. Boot

```sh
cd /srv/src/scribase/deploy/compose
dc="docker compose -p scribase --env-file /etc/products/scribase.env \
  -f docker-compose.yml -f docker-compose.single-box.yml -f docker-compose.shared-services.yml \
  -f docker-compose.postgres-cow.yml"

$dc config -q      # validate first
$dc up -d
```

## 5. Create the administrator and the organization

Skip `admin setup` if you pre-seeded `SCRIBASE_ADMIN_*`, and sign in with
`scribase login --email ...` instead.

```sh
$dc logs control-api | grep -A1 'setup token'          # scb_setup_...
$dc exec -e SCRIBASE_API_URL=http://127.0.0.1:8080 control-api \
  scribase admin setup --email you@yourdomain --setup-token scb_setup_...
$dc exec -e SCRIBASE_API_URL=http://127.0.0.1:8080 control-api \
  scribase org put <SCRIBASE_ORGANIZATION_ID value> "Your Org"
```

You can also open the console: on a control plane with no administrator, its
sign-in page asks for the setup token instead of a password.

## 6. First project

Create a project and a `production` environment in the console, or with the
CLI (see [Organizations, projects and environments](https://docs.scribase.com/docs/cli/resources.md)):

```sh
scribase project put <org> my-app aws-eu-central-1
scribase env create <org> my-app production production snapshot aws-eu-central-1 first-env-1
```

The operator creates the environment's database, bootstraps the Supabase
roles, publishes the runtime and creates the project container. Then check it with the
host label and `anon` key the console shows on the environment's API keys page:

```sh
host=https://<label>.<domain>
curl -fsS "$host/auth/v1/health"                          # starts the container
curl -fsS "$host/rest/v1/" -H "apikey: $ANON" -H "Authorization: Bearer $ANON"
```

Point an app at it with [`scribase apps switch`](https://docs.scribase.com/docs/cli/apps.md) and generate
its types with [`scribase gen types`](https://docs.scribase.com/docs/cli/gen-types.md).

## Backups on the single box

Backups, verification restores and scheduled restore drills run inside the
control API. Verification restores into the bundled `restore-verify-db` scratch
server and never touches `db` or a project's own database. Check it after boot:

```sh
$dc exec restore-verify-db pg_isready -U postgres -h localhost
```

A restore into an environment that already has data lands in a new
`restore_<id>` database. Switching the environment to it (console: restore
history, **Switch**) recreates the project container on the restored database.
See [Backups and restore](https://docs.scribase.com/docs/cli/backup.md).

## Limits to know

- The control API caps environments at `SCRIBASE_MAX_ENVIRONMENTS` (60 by
  default on this layout). On `neon-cloud`, your Neon plan's project limit also
  applies.
- A cold request pays the container start (measured 0.7 s to healthy) plus the
  database start (well under a second for `postgres-cow`; typically 0.5 to 1 s
  for a Neon compute).
- `postgres-cow` restores to a backup or to the head of the parent branch; it
  has no point-in-time restore to an arbitrary timestamp (use `neon-cloud` for
  that). Cutting a branch from a running environment pauses that environment's
  database for the length of the clone (milliseconds).
- An engine move (`scribase project move`) pauses writes for the duration of
  the copy.

## Copy-on-write storage

`postgres-cow` refuses to start on a filesystem that cannot clone files,
because every branch would silently become a full copy. Most VPS images boot
from ext4, so give the engine an XFS filesystem inside an image file:

```sh
sudo apt-get install -y xfsprogs
sudo fallocate -l 30G /srv/scribase-pg-cow.xfs
sudo mkfs.xfs -m reflink=1 /srv/scribase-pg-cow.xfs
sudo install -d -m 700 /srv/scribase/pg-cow
echo '/srv/scribase-pg-cow.xfs /srv/scribase/pg-cow xfs loop,noatime,nofail 0 0' | sudo tee -a /etc/fstab
sudo mount /srv/scribase/pg-cow
sudo chown 10001:10001 /srv/scribase/pg-cow && sudo chmod 700 /srv/scribase/pg-cow
```

Check it from the worker: `$dc exec worker scribase-postgres-cow status`.
