# 5. Start the web interface with scribe-ctl and Docker

Previous: [operate the cluster](/docs/tutorials/04-use-ctl.md) · [All tutorials](/docs/tutorials.md)

You will set up a second cluster that runs in Docker with the web interface
beside it, sign in with a user, and browse a repository in the browser. One
`scribe-ctl setup --runtime docker --web` builds the images, generates the
Compose project, and provisions everything, including the web interface's own
secrets and service account.

The web interface runs only as part of a Docker cluster, and `scribe-ctl`
never moves a cluster between runtimes, so this stage starts a new cluster;
the repositories of stages 1–4 stay in `scribe-demo`. (To run the services and
the web interface by hand instead, see [Docker Compose](/docs/manual/docker-compose.md)
and [the web interface by hand](/docs/manual/web-interface.md).)

You need Docker Engine or Docker Desktop with Compose v2 (`docker compose up
--wait`), and the Scribe source checkout your `scribe-ctl` was built from: the
images are built from it and tagged with that `scribe-ctl`'s build. No Node.js
or Rust toolchain is needed beyond what built `scribe-ctl`; the first image
build downloads base images and dependencies and takes a while.

## 1. Free the default ports and create a directory

The new cluster uses the same default ports as the stage 1–4 cluster. Stop
that one first (or give this setup `--port-offset 1000` and add 1000 to every
port below). From `scribe-demo`:

```sh
scribe-ctl --config scribe-cluster.toml down
cd ..
umask 077
mkdir scribe-web-demo
cd scribe-web-demo
```

## 2. Set up the Docker cluster

Replace `/path/to/scribe` with your checkout. The plan also builds the images
(building touches no cluster file), then runs every check:

```sh
scribe-ctl --config scribe-cluster.toml setup --variant gateway --runtime docker --web --source /path/to/scribe --plan
```

```text
  Building scribe/auth:tutorial-next from /path/to/scribe (the first build takes a while)…
  Building scribe/storage:tutorial-next from /path/to/scribe (the first build takes a while)…
  Building scribe/gateway:tutorial-next from /path/to/scribe (the first build takes a while)…
  Building the web interface's images (api, assets-derive, assets-index, cache, content, database, nginx, objects, search, web, worker) from /path/to/scribe/apps/web…
Scribe · local · setup plan · gateway variant · docker runtime
  ...
  ENDPOINTS
    auth       tls://127.0.0.1:7443
    storage-a  tls://127.0.0.1:7447
    gateway    tls://127.0.0.1:7557
    web        http://localhost:8080
  ...
  STEPS
     1. Write the cluster definition scribe-cluster.toml and scribe-gateway.toml
     2. Record the images scribe/auth:tutorial-next, scribe/storage:tutorial-next, scribe/gateway:tutorial-next and the web interface's of 0.1.0 (build tutorial-next) as a set in cluster-state/local/bin/tutorial-next
     ...
    13. Create the web interface's missing secrets secrets/web/jwt.key and secrets/web/surreal.password
     ...
    20. Ensure service account scribe-web reads in studio
    21. Ensure scribe-web's credential secrets/web-reader/scribe-web.credential
    22. Prepare the web database data/web (migrate.js on the empty store)
    23. Write the cluster summary CLUSTER-INFO.txt
```

Then run it and start the cluster:

```sh
scribe-ctl --config scribe-cluster.toml setup --variant gateway --runtime docker --web --source /path/to/scribe
scribe-ctl --config scribe-cluster.toml up
```

```text
Scribe · local · Setup complete
  wrote scribe-gateway.toml
  wrote scribe-cluster.toml
  recorded the images of 0.1.0 (build tutorial-next) in cluster-state/local/bin/tutorial-next
  recorded tutorial-next as the active set
  created the storage and gateway CA in pki/ca
  created the auth CA in pki/auth-ca
  issued pki/auth/server.pem
  issued pki/storage-a/server.pem
  issued pki/gateway/server.pem
  initialized data/gateway
  initialized data/auth; administrator credential secrets/admin.credential
  initialized data/storage-a
  storage-a: bound to namespace studio
  wrote secrets/web/jwt.key
  wrote secrets/web/surreal.password
  server storage-a: registered in studio
  storage-a: credential issued to secrets/storage-a.credential
  scribe-gateway: created with its grants in studio
  scribe-gateway: credential issued to secrets/gateway.credential
  scribe-monitor: created with its grants in studio
  scribe-monitor: credential issued to secrets/monitor.credential
  scribe-web: created with its grants in studio
  scribe-web: credential issued to secrets/web-reader/scribe-web.credential
  prepared the web database data/web: applied 0001-stamp-shape-versions, 0002-sign-out-bearer-sessions
  wrote CLUSTER-INFO.txt
  ...
Scribe · local · running
  SERVICE                      STATE        PID  ENDPOINT
  auth                         running        —  tls://127.0.0.1:7443
  storage-a                    running        —  tls://127.0.0.1:7447
  gateway                      running        —  tls://127.0.0.1:7557
  api                          running        —  —
  assets-derive                running        —  —
  assets-derive-sandbox-image  running        —  —
  assets-derive-sandbox-scene  running        —  —
  assets-index                 running        —  —
  cache                        running        —  —
  content                      running        —  —
  database                     running        —  —
  nginx                        running        —  http://localhost:8080
  objects                      running        —  —
  search                       running        —  —
  web                          running        —  —
  worker                       running        —  —
```

`up` is `docker compose up --wait`: it returns once every container is healthy
and the gateway has fresh inventory.

### What scribe-ctl did for you

- **Everything from stages 1–3**, in one run: both CAs, the server
  certificates, the auth database with its pepper, revision witness and
  administrator credential, the store bound to `studio` with its credential,
  and the gateway with its `scribe-gateway` account. The files have the same
  names and meaning; the definition has `runtime = "docker"`, and auth's pepper
  and witness sit in `secrets/auth/`, a directory of their own that only auth's
  container mounts.
- **Images and the Compose project.** It built `scribe/auth`, `scribe/storage`,
  `scribe/gateway` and the web interface's `scribe/web-*` images from the
  checkout, tagged with its own build, and recorded them as the cluster's first
  set (so a later `upgrade --source` can switch and switch back). Before every
  start it generates the Compose project in `cluster-state/local/docker/`.
  Containers reach each other by service name and publish their ports on
  loopback only. The Scribe services' containers bind-mount only their own
  data and files and run as the owner of this directory, with a read-only root
  and no capabilities.
- **The web interface** (`[web]` in the definition): its session-signing key
  and database password in `secrets/web/`; its database in `data/web`,
  prepared with the web worker's `migrate.js`; and the `scribe-web` service
  account, a namespace reader in `studio` whose credential
  (`secrets/web-reader/scribe-web.credential`) the background services use to
  index repositories. The web services verify auth and the gateway against the
  two public CAs, like any client.
- **The summary.** `where` shows every file, and the `docker compose` command
  for the project if you want to look yourself:

  ```text
  Docker: Compose project scribe-local-2926d0f4
    cluster-state/local/docker
        images: scribe/auth:tutorial-next, scribe/storage:tutorial-next, scribe/gateway:tutorial-next
        run docker compose for it as: docker compose --project-directory /home/you/scribe-web-demo/cluster-state/local/docker -p scribe-local-2926d0f4 -f /home/you/scribe-web-demo/cluster-state/local/docker/web.yaml -f /home/you/scribe-web-demo/cluster-state/local/docker/compose.yaml
  ```

The generated files are rewritten before every start: change the definition,
not them. Details: [run a cluster in Docker](/docs/cluster-usage.md#run-a-cluster-in-docker)
and [run the web interface with scribe-ctl](/docs/web-interface.md#run-it-with-scribe-ctl).

The same deployment by hand, with the same files, is the [Docker
Compose](/docs/manual/docker-compose.md) guide followed by the [web
interface](/docs/manual/web-interface.md) guide. A Compose project made that way
comes under `scribe-ctl` later with `adopt`, which leaves its data and secrets
where they are and keeps clients' endpoints, sessions and trust: see [take over
an existing Docker deployment](/docs/cluster-usage.md#take-over-an-existing-docker-deployment).

## 3. Add a user and a repository

The helpers from stage 2 work here unchanged: they read the files relative to
the directory you load them in, and `client-env.sh` names the gateway from
stage 3. Copy them over, create the password file as in stage 2, and add Alice:

```sh
cp ../scribe-demo/admin-env.sh ../scribe-demo/client-env.sh .
mkdir -p passwords
(umask 077 && touch ./passwords/alice)   # then put a password in it with an editor
. ./admin-env.sh
. ./client-env.sh
auth users add alice
auth users password alice --password-file ./passwords/alice
auth namespace-admins set alice studio
alice login alice --auth tls://127.0.0.1:7443 --auth-ca ./pki/auth-ca/ca.pem
alice repo create game --storage-compression lz4
alice workspace create game ./work/alice
(
  cd work/alice
  mkdir Assets
  printf 'hero mesh v1\n' > Assets/Hero.txt
  alice add Assets/Hero.txt
  alice submit -m "Initial hero"
)
```

```text
Logged in as alice; session valid for 90 days.
created: studio/game id=72b2caea0caddd27a7e52823d1fa4b95 head=0 main=1b24da60b905 storage-compression=lz4
...
submitted change 1 (1 file, 1 of 1 chunks sent, 208 object bytes sent for 13 logical bytes, 3 server flushes)
```

The CLI on your machine reaches the containers through their published ports,
exactly as in stages 2 and 3.

## 4. Sign in and browse

Check the web API first:

```sh
curl --fail --show-error http://localhost:8080/api/v1/health
```

```text
{"status":"ok","version":"v1"}
```

Open [http://localhost:8080](http://localhost:8080). Anonymous visitors see
only public repositories, so the list is empty before you sign in.

1. Open **Sign in** and enter `alice` with her password. Browser login uses
   Scribe auth; the web interface has no users of its own.
2. Open `studio/game`, then `Assets/Hero.txt`, and its history: the change
   Alice submitted from the CLI.
3. Use the Clone menu to see the native CLI commands for a working copy.
   Browser credentials are never included in them.
4. Open **Settings → Asset index**. Within a minute it shows the files indexed,
   read with the `scribe-web` credential setup issued.

Web login does not grant repository access: a user sees what their Scribe
grants allow. See the [web interface guide](/docs/web-interface.md) for file-size
limits, session behavior, and integration details.

## 5. Diagnose

```sh
scribe-ctl --config scribe-cluster.toml status
scribe-ctl --config scribe-cluster.toml logs assets-index --lines 20
scribe-ctl --config scribe-cluster.toml logs api
```

`logs` takes any service of the project: the Scribe services by name, and the
web interface's (`api`, `content`, `worker`, `assets-index`, `assets-derive`,
`web`, `nginx`, `database`, `cache`, `search`, `objects`).

| Symptom | Check |
| --- | --- |
| The image build fails | Docker's network access and the build error; the same `setup` command resumes |
| `up` times out | `status`, then `logs SERVICE` of the one that is not healthy |
| Sign-in is refused | The user's password (`auth users password`), and that you opened exactly `http://localhost:8080` (`localhost` and `127.0.0.1` are different origins) |
| Signed in, but a repository is missing | That user's grants (`scribe-ctl access --user NAME`) |
| **Settings → Asset index** warns about the background account | `scribe-ctl access --user scribe-web` shows its reader grant; `renew` replaces its credential before it expires |
| `assets-derive-sandbox-*` exits with `bubblewrap cannot create a child's sandbox` | See [sandbox containers](/docs/web-interface.md#sandbox-containers) (Ubuntu 23.10 and later hosts) |

On Linux the web services run as their images' user (uid 1000): run
`scribe-ctl` as uid 1000, or give that uid read access to the web secrets and
the reader credential with an ACL, never by making them group-readable
([the ACL commands](/docs/manual/web-interface.md#3-issue-the-reader-credential)).
Docker Desktop maps ownership for you.

## 6. Stop, start, and serve HTTPS

```sh
scribe-ctl --config scribe-cluster.toml down
scribe-ctl --config scribe-cluster.toml up
```

`down` stops the containers (`docker compose stop`) and keeps them, their logs,
and every volume; the data roots, the web database and the secrets are
directories here, on the host. Web sessions survive a restart. Back up what
[what to back up](/docs/migration-backup-restore.md#what-to-back-up) lists:
besides `data/` and `secrets/`, the web interface's `objects` volume holds
flow-run bundles that exist nowhere else, and the web database needs the
`secrets/web/` files to be of any use.

This walkthrough serves plain HTTP on loopback. For team access, the web
interface's own TLS certificate is yours to provide: put `fullchain.pem` and
`key.pem` in `secrets/web-tls/`, set `[web] origin` in the definition to the
`https://` address people use, and run `up` again. See [production
deployment](/docs/production-deployment.md) for the rest of a team setup, and
[run a cluster in Docker](/docs/cluster-usage.md#run-a-cluster-in-docker) for
`upgrade --source`, which also migrates and backs up the web database.
