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. 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 afterSCRIBASE_POSTGRES_COW_SUSPEND_SECONDS(default 900, like the API container) and started again by the next connection. The bundleddbholds only the control plane. No cloud account is needed. - Managed Neon is optional. Set
SCRIBASE_NEON_API_KEYand new projects can useneon-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-localmoves 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 afterSCRIBASE_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.
1. Build the images
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-image2. Configure
sudo install -m 640 -o root -g deploy single-box.env.example /etc/products/scribase.env
sudoedit /etc/products/scribase.envFill every blank in the template:
- Control-plane passwords and keys (
openssl rand -hex 32for 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).- Optional:
SCRIBASE_NEON_API_KEY, a Neon API key, to offerneon-cloudtoo. - 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 bundledrestore-verify-dbscratch server that Backup Verify restores into. LeaveSCRIBASE_RESTORE_VERIFY_DATABASE_URLblank to use that server. Without a verification database, Backup Verify answers503 verification_not_configured.- Leave
SCRIBASE_ADMIN_EMAILandSCRIBASE_ADMIN_PASSWORD_HASHblank to get a one-time setup token in the log, or pre-seed the administrator (see Built-in admin login).
Keep every published port on 127.0.0.1. Docker bypasses host firewalls such
as UFW.
3. DNS and TLS
api.<domain>andconsole.<domain>: ordinary records pointing at the box.*.<domain>: one wildcard record to the box. Every environment is served athttps://<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.to127.0.0.1:8081and*.to127.0.0.1:8010(the project router).
4. Boot
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 -d5. Create the administrator and the organization
Skip admin setup if you pre-seeded SCRIBASE_ADMIN_*, and sign in with
scribase login --email ... instead.
$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):
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-1The 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:
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 and generate
its types with scribase gen types.
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:
$dc exec restore-verify-db pg_isready -U postgres -h localhostA 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.
Limits to know
- The control API caps environments at
SCRIBASE_MAX_ENVIRONMENTS(60 by default on this layout). Onneon-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-cowrestores to a backup or to the head of the parent branch; it has no point-in-time restore to an arbitrary timestamp (useneon-cloudfor 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:
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-cowCheck it from the worker: $dc exec worker scribase-postgres-cow status.