Run Studio as containers
Run the Studio server — gateway, launcher, console server and CouchDB — as container images on volumes: first start, upgrade, roll back, and rotate the CouchDB administrator.
This page is for the person who runs the Studio server and wants it as container images on volumes
rather than as native services. A container image install is the install route of every edition,
including Solo; the native install (saphan service install) is the other route, available in Team
and Enterprise (editions). Docker with the compose plugin, or Podman,
is required on the host.
What you get
One compose project, saphan-studio:
| service | what it runs | restarts on boot |
|---|---|---|
gateway | the gateway. Published: its mutual-TLS listener on :8443, which refuses any caller without a certificate your client CA signed. Not published: the console socket (127.0.0.1:8788, inside the container) | yes |
launcher | the launcher, which starts the models, terminals and runs the gateway asks for | yes |
server | the console server: fleet supervisor and projection service | yes |
couchdb | the channel store, on 127.0.0.1:5984 only | yes |
studio-init, studio-config, studio-declare, studio-migrate | one-shot jobs: set volume ownership and install the credentials file, record the configuration, declare the workspace on a first start, run the store migration | no |
All three Saphan services run one image, selected by digest. docker compose ps shows server
as unhealthy whenever it is not serving — no workspace declared, a workspace it cannot open, or an
installation whose owner is not enrolled yet (step 5 below).
Where your data lives. The root filesystem of every Saphan service is read-only; everything the product writes is on a volume:
| volume | holds |
|---|---|
workspace | the store and the record |
home | the configuration and the trust material |
launcher | the launcher's socket directory |
couchdb-data, couchdb-config | CouchDB's data and its local configuration |
docker compose down keeps them. ⛔ docker compose down -v deletes them — never run it on a
deployment you mean to keep.
1. First start
⚠ Saphan Studio Solo is in final testing. The Studio server image this page names is published from the release day; until then the image is not available.
-
Put the deployment files on the host.
-
Place the secrets. Create
/opt/saphan/docker/secrets/(mode 0700) with the files below, each mode 0600 and readable by uid 10001 if your host enforces that:file what it is credentials.jsonthe credentials file, with the CouchDB administrator entry gateway-tls.crt,gateway-tls.keythe gateway's TLS certificate and key client-ca.pemthe CA whose client certificates the gateway accepts healthcheck-client.crt,healthcheck-client.keya client certificate that CA signed (extended key usage clientAuth), used only by the gateway's own healthcheckTo mint the healthcheck certificate from the CA's key:
The secrets are mounted read-only at
/run/secrets/and never copied into an image. To keep them elsewhere, setSAPHAN_STUDIO_SECRETS_DIRto that directory. -
Have two image digests ready: the Studio server image and CouchDB. ⚠ A tag is refused: only
<repository>@sha256:<64 hex digits>is accepted, because a tag can be moved under a running deployment. For CouchDB, read the digest from the registry you pull it from, for exampledocker buildx imagetools inspect couchdb:3.4.2. -
Start it.
This records both digests in
/opt/saphan/docker/studio-image.envand starts the project. CouchDB's administrator and cookie secret come from your credentials file and are never container environment variables, sodocker inspectdoes not print them. After a reboot the four long-lived services come back by themselves; to start the project again on the recorded digests, runsaphan infra studio up --dir /opt/saphan/dockerwith no image flags.⚠ On a first start
upends withservernot healthy. That is expected: the server also needs the owner enrolled, which is the next step.gateway,launcherandcouchdbcome up healthy. -
Enrol the owner. The server will not start until this installation's actor is enrolled in the signed registry, and that entry is signed with your root key, which is never in the stack. Run this once, on the machine that holds
~/.saphan/trust/root.keyandroot.key.pub, with Docker reachable from it (DOCKER_HOST=ssh://<host>reaches another machine). It runs a one-off container of the server image against the project'sworkspaceandhomevolumes; the root key is read from standard input, so it appears in no argument, no environment variable and no image layer, and inside the container it exists only on atmpfsuntil the command ends. The first line sets the actor's name.It prints
identity enroll: actor "<name>" enrolled in the signed identity store. Then start the project again;upreturns onceserveris healthy: -
Check it.
docker compose psshowsgatewayas healthy. From any machine, a request with no client certificate is refused, and one with a certificate your client CA signed is answered:The published port is the gateway's machine-to-machine door. The console socket asks for no client certificate, which is why it is not published: it is reachable only inside the gateway container's network namespace, for example from a TLS proxy container of your own started with
network_mode: service:gateway.
2. Upgrade
-
Back up first. Copy the
workspaceandcouchdb-datavolumes. If going back turns out to be unsafe, that copy is the only way back. -
Run the upgrade.
-
Read what it did. In this order, it:
- records the new digest as current and the old one as previous in
studio-image.env; - runs the store migration with the new image on the same volumes, while the old containers keep serving;
- runs the rollback probe: a one-off container of the previous image tries to open the
migrated store. The answer is recorded as
SAPHAN_STUDIO_ROLLBACK=safeorSAPHAN_STUDIO_ROLLBACK=UNSAFE; a probe that could not run counts asUNSAFE; - moves the containers to the new digest and waits for them to be healthy.
If the migration fails, no running service has moved. If the new containers do not become healthy, the refusal names the next act,
saphan infra studio rollback; nothing is undone for you. - records the new digest as current and the old one as previous in
-
If it printed
ROLLBACK IS UNSAFE, the services are on the new digest and a rollback will be refused. Going back needs a restore of theworkspaceandcouchdb-datavolumes from the backup of step 1.
⚠ The probe reads the store on the workspace volume only. It does not check CouchDB's databases;
if an upgrade changes those, the backup of step 1 is the only guard.
3. Roll back
-
grep SAPHAN_STUDIO_ROLLBACK /opt/saphan/docker/studio-image.envmust not printUNSAFE. If it does, stop: the way back is the restore above. -
Roll back:
This puts the record back on the previous digest and starts the project on it, on the same volumes. A second
rollbackgoes forward again, without a safety verdict.
⚠ A safe verdict means the previous image could open the store at the time of the upgrade, not
that data written since then is understood by it.
4. Rotate the CouchDB administrator
-
Edit the
SAPHAN_COUCHDB_CREDENTIALSentry (<login>:<password>) in/opt/saphan/docker/secrets/credentials.json. Keep it mode 0600; do not touch other entries. -
Start the project again, saying the administrator changed:
CouchDB is recreated with the new administrator, and the gateway, the server and the migration pick it up without a restart.
-
Check:
docker compose psshowscouchdbandgatewayhealthy.
Settings the compose file owns
On every up and upgrade the project writes these configuration keys; a value you set for one of
them with saphan config set is put back at the next start. Every other key is left alone.
| key | value | to change it |
|---|---|---|
channel.base_url, evidence.base_url | http://couchdb:5984 | run saphan infra studio up once with SAPHAN_STUDIO_CHANNEL_URL=<http(s) URL> in its environment; the address is recorded in studio-image.env and kept by later starts. An address with a login or password in it is refused |
gateway.mtls_listen | 0.0.0.0:8443 | not changeable: it is the port the compose file publishes |
credentials.file | the credentials copy on the home volume | not changeable |
channel.credential, evidence.credential | SAPHAN_COUCHDB_CREDENTIALS | not changeable |
When something refuses
| you see | it means | do |
|---|---|---|
the compose project mounts secrets from files that are not there | a secret file is missing | place the files named in step 2 |
the image reference is not a digest | you passed a tag | pass the @sha256: digest |
the rollback is recorded as UNSAFE | the previous image cannot read the migrated store | restore the volumes from the pre-upgrade backup |
already records … as the running image | up with image flags on a started deployment | use upgrade, or up with no flags |
server is unhealthy after a first start | docker compose logs server names why: no workspace means the declare step did not run; registry: runtime registry not provisioned means the owner is not enrolled | step 5 |
permission denied on a CouchDB path | the volume was not initialised for CouchDB | recreate the project; do not bind-mount over couchdb-data |
⚠ Where to get the Studio server image is not described on this page yet: it needs the registry and digest your Saphan distribution provides.
Updating a fleet that is already installed
The same delivery, repeated — and the traps that make it look like it worked when it did not.
The built-in git server, and choosing another forge
Every Studio install brings its own git server, Gitea. Start it, serve it over TLS, or point Studio at your own Gitea or at GitHub instead.