Multi-User Deployment#
The multi-user Web Terminal turns one OSPREY project into a small shared
product: a landing page where each member of your team picks their name, and a
private, containerized Web Terminal behind each card — all served from a single
host, brought up with a single osprey deploy up.
What You’ll Learn
Why the single-user
osprey webworkflow is unchanged, and when each mode is the right toolThe three ideas behind the multi-user stack: one container per user, personas as capability tiers, and one nginx front door
The
modules.web_terminalsconfig block that switches it onDay-to-day operations: adding, reseeding, and removing users
Prerequisites: The concepts need none. To stand the stack up you’ll
want Docker (or Podman) and a generated project — the
multi-user-demo preset ships everything pre-wired, and
Run the Multi-User Demo walks through it end to end.
Single-user is still the front door#
Nothing about multi-user changes the everyday workflow. From any project directory,
osprey web
still launches the single-user Web Terminal as one local process — no
containers, no proxy, ready in seconds at http://127.0.0.1:8087. It remains
the fastest way to try OSPREY and the right tool whenever one person sits in
front of one machine; Run the Web Terminal covers it.
The multi-user stack is strictly opt-in. It lives in a
modules.web_terminals block in the project’s config.yml, read only by
osprey deploy (and validated by osprey scaffold web-terminals lint).
osprey web never looks at it — so a project that carries the block (the multi-user-demo preset
ships one) still runs single-user exactly as before. Reach for the multi-user
stack when several people need their own terminal on a shared machine, and
stay with osprey web for everything else.
How it works#
flowchart LR
B[Browser] -->|:9080| N[nginx landing page]
N -->|/u/alice/| A[alice's terminal container]
N -->|/u/bob/| Bo[bob's terminal container]
A --- S[(shared services:<br/>databases · telemetry)]
Bo --- S
Three ideas carry the whole design:
One container per user. Every name on the roster gets its own Web Terminal
container, plus two named volumes that belong to the user, not the container:
a workspace volume (the files the agent reads and writes) and an
agent-configuration volume. Upgrading or rebuilding an image replaces the
container but never touches those volumes, so a user’s files and settings
survive every redeploy. On first start, osprey deploy up seeds each user’s
configuration volume automatically — no per-user setup steps.
A persona is a capability tier — and a whole project. Users map to
personas, and each persona is its own rendered OSPREY project with its own
config.yml, permissions, skills, and tool servers. Because permissions are
a property of a project, the tiers are genuinely different agents — not one
agent with a UI toggle. The demo ships two: a read-only tier and a
read-write tier — the same agent and tool surface, differing on exactly one
config key (control_system.writes_enabled). osprey deploy up
auto-renders any persona project that doesn’t exist yet and builds its
container image locally, so no registry or CI is involved.
One front door. An nginx reverse proxy serves the landing page and proxies
/u/<name>/ to that user’s container. The per-user containers are pinned to
the loopback interface, so nginx is the only network path in. The landing
page’s user cards are a convenience for choosing an identity on a trusted
machine — not authentication (see the Authentication and TLS tab below).
Running the stack#
The whole feature is one config block. This is what the
multi-user-demo preset renders into config.yml:
modules:
web_terminals:
enabled: true
image_source: local # deploy up builds persona images itself
nginx_port: 9080 # the landing page
web_base_port: 9091 # per-user ports: base + user index
artifact_base_port: 9291
ariel_base_port: 9391
lattice_base_port: 9491
channel_finder_base_port: 9591
default_persona: readonly
users:
- alice # bare name → default_persona
- name: bob
index: 1
persona: readwrite
personas:
readonly:
project: multi-user-demo-readonly
project_path: ../multi-user-demo-readonly
build_profile: multi-user-demo-readonly
readwrite:
project: multi-user-demo-readwrite
project_path: ../multi-user-demo-readwrite
build_profile: multi-user-demo-readwrite
The users list is the roster — the single source of truth for who
exists. A bare name resolves to default_persona; an entry with an
explicit persona picks its tier. Each user’s host ports are
base + index in every port family — one family per companion panel
(artifact gallery, ARIEL, channel finder, lattice dashboard, …) plus the
terminal itself — so alice (index 0) serves her terminal on 9091 and
bob (index 1) on 9092. A panel whose *_base_port you don’t set
falls back to its built-in default, so the block above lists them only
to make the layout visible.
Tip
Give every roster entry an explicit index before you ever
remove one. Once indices are pinned, deleting an earlier user can
no longer shift a later user’s ports out from under a running
deployment.
The roster drives everything: edit it, then let osprey deploy
reconcile reality against it.
Task |
Command |
|---|---|
Add a user |
Add a roster entry with the next free |
Reseed workspaces |
|
Remove one user |
|
Clean up leftovers |
|
Tear it all down |
|
osprey deploy status and osprey deploy down work exactly as they
do for any other OSPREY service stack.
The multi-user stack ships with no authentication: anyone who can reach the nginx port can open any user’s terminal. The landing page’s user cards are a convenience for choosing an identity on a shared machine, not an access-control boundary — and the persona split is a capability boundary enforced per project, not an identity one. Traffic is plain HTTP; there is no TLS. That makes this stack right for a single trusted host — a workstation or control-room machine you already trust — and wrong for an untrusted network. Run the Multi-User Demo spells out the full posture.
Both are recognized seams rather than oversights.
modules.web_terminals.auth.method and modules.web_terminals.tls
are part of the config schema, and the nginx config renders against
them. What’s missing is a backend behind the auth seam; wiring one
(oauth2-proxy, for instance) is the remaining work.
Warning
Don’t set auth.method expecting a login prompt. Until a real
backend sits behind the seam, its auth_request target is a
placeholder that returns 403 — fail-closed by design, so
enabling it locks every user out rather than silently authorizing
them.
If authentication is what stands between you and deploying this, please open an issue or get in touch. The seam is already in place, and knowing someone needs it is what moves it up the queue.
Try it#
Stand up the two-persona demo — a read-only and a write-capable tier, one container each — from a fresh checkout with one build and one deploy.