Previous: Docker Compose · Manual setup · Next: certificates and credentials
The web interface (apps/web) runs only in Docker, as a Compose project of its
own beside a Scribe cluster. This guide starts it by hand and connects it to a
cluster with auth: the Docker Compose guide's project, or
services running on the host. scribe-ctl setup --runtime docker --web does
the same for a cluster it runs (tutorial 5,
what it wires up).
What it needs from the cluster:
| From the cluster | Why |
|---|---|
The storage or gateway endpoint, and the storage and gateway CA (pki/ca/ca.pem) | Every request reaches repositories there, verified against that CA |
The auth endpoint, and the auth CA (pki/auth-ca/ca.pem) | Browser sign-in goes to Scribe auth; the web interface has no accounts of its own |
A reader credential for the scribe-web service account | The worker and the asset services index repositories as this account |
It needs Docker Compose v2 and Node.js 26 or newer for the initialization
script. Run every command from the checkout's apps/web directory. Compose
builds the images from it, so the checkout must be the cluster's release.
1. Create the local files
node deploy/init.mjsLocal secrets are ready. Configure /path/to/scribe/apps/web/.env, then run docker compose up --build -d.It creates, once and never replacing an existing file, the ignored
deploy/local/ directory: secrets/jwt.key and secrets/surreal.password
(random, private), empty trust/, tls/ and worker/ directories, and
.env from deploy/.env.example. It never copies a CA key or a credential.
2. Trust both CAs
Copy the two public CA certificates:
cp /srv/scribe/pki/ca/ca.pem deploy/local/trust/ca.pem
cp /srv/scribe/pki/auth-ca/ca.pem deploy/local/trust/auth-ca.pemThe services see them read-only under /etc/scribe/trust. The web services
use Node's TLS, which follows no CA handover: when a CA is rotated, give them
both the old and the new CA for the overlap
(certificates).
3. Issue the reader credential
Create the scribe-web service account, let it read every repository of each
namespace the interface serves (including later ones), and issue its read-only
credential. The credential records the auth address and CA the web services
use, so name auth as they reach it. With the Docker Compose
guide's project, that is the service name, which its
administrator credential already carries; from /srv/scribe:
docker compose run --rm auth-admin users add scribe-web --service
docker compose run --rm auth-admin namespace-readers set scribe-web studio
docker compose run --rm auth-admin credentials create scribe-web --service --name web-reader --days 365 --out /new/reader.credential
mv secrets/new/reader.credential /path/to/scribe/apps/web/deploy/local/worker/reader.credentialFor a cluster on the host, run the same three commands with scribe-auth
(the auth helper), naming the address
containers reach the host at:
auth users add scribe-web --service
auth namespace-readers set scribe-web studio
auth credentials create scribe-web --service --name web-reader --days 365 \
--auth tls://host.docker.internal:7443 --auth-ca deploy/local/trust/auth-ca.pem \
--out deploy/local/worker/reader.credentialThe account has no password, never signs in and never administers; a
namespace reader grant never writes (service accounts).
To limit it to chosen repositories instead, skip namespace-readers set and
grant it reader on each with grants set scribe-web NAMESPACE/REPOSITORY_ID --role reader.
On Linux the web services run as uid 1000 and see the host files' owner and
mode. If your account is not uid 1000 (id -u), let that uid read the
credential, the secrets and the CAs without making anything group- or
world-readable, for example with ACLs (package acl):
sudo setfacl -m u:1000:x deploy/local/worker deploy/local/secrets deploy/local/trust
sudo setfacl -m u:1000:r deploy/local/worker/reader.credential deploy/local/secrets/* deploy/local/trust/*or hand the files to uid 1000 (sudo chown 1000 ..., keeping their modes).
ls -l then shows -rw-r-----+: the group bits display the ACL mask. Docker
Desktop on macOS and Windows maps ownership, and neither step is needed. Never
chmod 644 a credential file: it holds a private key.
4. Point .env at the cluster
Edit .env. For the Docker Compose guide's project (named scribe), the web
services join its network and reach the services by name, which their
certificates carry:
WEB_SCRIBE_NETWORK=scribe_default
WEB_SCRIBE_SERVER=tls://gateway:7557
WEB_SCRIBE_AUTH_SERVER=tls://auth:7443
WEB_SCRIBE_TLS_SERVER_NAME=
WEB_SCRIBE_AUTH_TLS_SERVER_NAME=
WEB_SCRIBE_CLONE_SERVER=tls://127.0.0.1:7557
WEB_SCRIBE_CLONE_AUTH_SERVER=tls://127.0.0.1:7443
WEB_SCRIBE_READER_CREDENTIAL_FILE=/etc/scribe/worker/reader.credentialFor services on the host, keep the example's host.docker.internal
endpoints: the containers connect there while verifying the certificate
identity 127.0.0.1 (WEB_SCRIBE_TLS_SERVER_NAME,
WEB_SCRIBE_AUTH_TLS_SERVER_NAME), and set only
WEB_SCRIBE_READER_CREDENTIAL_FILE. That is validated on Docker Desktop for
macOS. Native Linux Docker reaches the host's bridge address, not services
bound only to host loopback: there, give Scribe a listener the container
network can reach, or run it in Compose as above. Never disable TLS
verification to fix routing.
WEB_SCRIBE_CLONE_* are the addresses the Clone menu shows people.
Connection configuration
describes every variable.
5. Prepare the database
A new database is prepared, and every release that requires it migrated, by
the worker image's migrate.js; no process migrates on startup, and one that
needs a migration refuses to start. Include compose.scribe.yaml whenever the
services join the cluster's network:
docker compose -f compose.yaml -f compose.scribe.yaml build
docker compose -f compose.yaml -f compose.scribe.yaml run --rm worker node apps/worker/dist/migrate.js --dry-run
docker compose -f compose.yaml -f compose.scribe.yaml run --rm worker node apps/worker/dist/migrate.js...
{"event":"applied","id":"0001-stamp-shape-versions","counts":{}}
{"event":"applied","id":"0002-sign-out-bearer-sessions","counts":{}}
{"event":"done","dryRun":false,"migrations":["0001-stamp-shape-versions","0002-sign-out-bearer-sessions"]}run starts the database, cache and object store it needs. The flags and the
migration ledger are described in stored data
versions.
6. Start it
docker compose -f compose.yaml -f compose.scribe.yaml up -d --wait
curl --fail --show-error http://localhost:8080/api/v1/health{"status":"ok","version":"v1"}Open http://localhost:8080 and sign in
with a Scribe user: tutorial 5, step 4
shows what to check, and its diagnosis table
applies here with docker compose logs SERVICE. Only nginx publishes a port,
on 127.0.0.1 by default (networks).
7. Serve HTTPS with your certificate
The web interface's certificate is yours: one browsers trust for the name
people use, not one from Scribe's private CAs. Put the certificate chain and its
key in deploy/local/tls/:
cp /path/to/fullchain.pem deploy/local/tls/fullchain.pem
cp /path/to/privkey.pem deploy/local/tls/key.pemand set, in .env, the exact origin and ports:
WEB_TLS=1
APP_ORIGIN=https://scribe.example.com
WEB_BIND=0.0.0.0
WEB_PORT=443
WEB_HTTP_PORT=80Run the same up again. nginx serves HTTPS on WEB_PORT and redirects HTTP on
WEB_HTTP_PORT to APP_ORIGIN; missing certificate files stop it rather than
falling back to HTTP. Replace the files and run up again to renew.
For an intranet name that exists in public DNS but is not reachable from the
internet, deploy/issue-tls.sh obtains a Let's Encrypt certificate through
Cloudflare DNS-01 and writes both files; rerun it to renew
(deploy/issue-tls.sh --help lists its options):
# Token with Zone/DNS/Edit and Zone/Zone/Read, kept in an ignored file:
# printf '%s\n' "$token" > deploy/local/secrets/cloudflare.dns.token
# chmod 600 deploy/local/secrets/cloudflare.dns.token
deploy/issue-tls.sh --email ops@example.com scribe.example.comAfterwards
- Stop and start:
docker compose -f compose.yaml -f compose.scribe.yaml stopandup -d --wait. The database, search index and object store are Compose volumes (database,search,objects); neverdown -v. - Backups: the
databaseandobjectsvolumes, taken together while the services are stopped (the commands), anddeploy/local/secrets/with them;objectsholds flow-run bundles that exist nowhere else, and the search index is rebuilt (what to back up). - Reader credential renewal: certificates and credentials; the services reread it on every use.
- Upgrades migrate the database: upgrades.
- Asset previews: the two sandbox containers need user namespaces; see sandbox containers on Ubuntu 23.10 and later.