Web shell (browser terminal)
The shell app serves the browser terminal at
https://shell.prototype.imeme.name. By default, it opens a shell in the
separate preinstalled terminal Dokku app, a shared, ephemeral development
workspace based on Open Terminal. Explicit ?app=<name> instead opens a shell
inside that app's own running container; ?app=shell explicitly targets the
frontend-only shell app. A registered Dokku SSH key can approve browser
access without an administrator.
The browser sessions use unprivileged docker exec inside the selected app
container, not a host shell. The default terminal workspace is shared:
anyone authorized for terminal can see or modify files and saved Copilot
login state there. A separate private Open Terminal HTTP API is reachable by
all Dokku app containers using credentials automatically provisioned into
every app; browser app-scoped grants do not isolate API clients. Do not put
secrets or untrusted tenants in this shared workspace. See
App container confinement for the restrictions
and exceptions.
Getting access
Access is granted per person, per app, for a limited time. Open
https://shell.prototype.imeme.name/?app=myapp (omit ?app= or use an empty
?app= to access the default terminal workspace). To access the frontend
container itself, use explicit ?app=shell. Without a valid session,
the page displays a short-lived SSH
login command. Run it on your own computer using your registered Dokku SSH
key (specify -i <private-key> if needed), for example:
ssh -t -i ~/.ssh/id_ed25519 dokku@<platform-host> shell:login ABCD-EFGH
Read the app and browser address in the SSH prompt and type yes only
if they match your request. The waiting browser connects automatically. Codes
expire after 5 minutes and are single-use; the resulting app-scoped session
lasts 24 hours. A stolen code alone cannot log a different browser in.
The SSH key must already be registered with Dokku; local dokku shell:login
does not authenticate you. Treat registered keys as authorized for the apps
they request: this platform has no per-app ownership registry, and self-service
login does not add one. If you need app-specific grants, disable self-service
(SHELL_SSH_LOGIN=false in the service unit) and use the operator flow below.
Operator fallback (no registered SSH key): On the Dokku host run:
sudo paas-shell-token mint --user alice --app myapp --ttl 28800
This prints a signed token and a ready-to-share access URL:
https://shell.prototype.imeme.name/login?token=v1.eyJ...
The operator sends that URL to the developer (chat, email, ticket). The
--ttl is the token lifetime in seconds (default 3600 = 1 hour; 28800 = 8
hours). When it expires, the developer's next session needs a fresh URL.
Opening a shell (developer side)
- Open the operator-provided access URL in a browser (or use the SSH flow above).
- The platform verifies the token, sets a session cookie
(
HttpOnly, tied to the token's remaining lifetime), and lands you on the terminal page, already scoped to your app. - Type. It's a full terminal: Tab completes commands and paths; Ctrl-L clears
the display and Ctrl-R searches command history in Bash. Other app images
without Bash fall back to
/bin/shand may not support these shortcuts. Arrows, Ctrl keys, and resizing work. On macOS, Option follows your active keyboard layout (including Swedish-layout@and$); Option is not used as a terminal Meta shortcut. The defaultterminalapp includes developer tools such asgit,ssh,curl, Python 3,jq,less, andvim; shells in other apps use those apps' own images and toolsets. Close the tab or let it sit — sessions idle out after 30 minutes; the pty is always cleaned up server-side.
Re-opening the same URL later works until the token expires. The token rides
the URL exactly once (at /login); the live session uses the cookie, so the
token never sits in browser history or proxy logs beyond that first click.
GitHub Copilot CLI in the terminal workspace
The default terminal app ships the GitHub Copilot CLI: type copilot for an
AI pair-programmer with built-in PaaS instructions and a paas-deploy skill.
The shell frontend does not ship Copilot; other app-scoped shells use their
own app images and may not include it.
The default copilot-openrouter launcher calls
https://openrouter.ai/api/v1 directly with the exact model ID
~deepseek/deepseek-v4-flash-latest. ~/copilot-setup recognizes OpenRouter
without GitHub login; a missing key fails closed. GitHub login is an explicit
alternative: COPILOT_USE_GITHUB=1 ~/copilot-setup, then
COPILOT_USE_GITHUB=1 copilot. See GitHub Copilot CLI
for details and shared deploy-key limitations.
How a session is scoped
- The token binds one user to exactly one app. Switching
?app=to a different app prompts for a new SSH approval and replaces the browser's session cookie; switching back requires approval again. An operator access URL remains scoped to the app it was minted for. Asking for another app with the old token fails with403, and an unknown/stopped app fails with404— enforced server-side before the WebSocket opens. - The shell lands inside the selected app's own container (found via
Dokku's container labels), as an unprivileged runtime user, with a scrubbed
environment. The API key is not passed directly in the browser shell's
environment, but this does not keep it secret from a shell user: the
terminal shell runs as the same UID as the API process and may read that
process's environment (for example through
/proc), and its mounted shared Dokku deploy key can inspect app configuration. Treat every authorizedterminalbrowser user as trusted with the workspace, API key and platform-wide deploy privileges. The installed service unit usesSHELL_ENV_ALLOWLIST=OPENROUTER_API_KEY,OPENROUTER_MODELto read those values from the selected terminal app's container environment. These AI credentials are directly available to terminal browser sessions; never log or forward them to chat. - Caps: 4 concurrent sessions per app, 64 platform-wide; 30-minute idle timeout; liveness checks drop dead connections.
Architecture at a glance
| Piece | Where it runs | Role |
|---|---|---|
shell Dokku app |
its own container | serves the public browser frontend (xterm.js), not the default developer toolchain |
terminal Dokku app |
its own container | default shared Open Terminal workspace, Copilot/PaaS tools and authenticated private HTTP API |
paas-shell-backend.service |
the Dokku host, 127.0.0.1:8790 |
verifies browser tokens, spawns the pty inside selected app containers, enforces scoping/caps/timeouts |
| nginx include | host (/home/dokku/shell/nginx.conf.d/) |
routes /ws/terminal, /login, and /login/ssh/ from the app domain to the backend |
paas-shell-login Dokku plugin |
Dokku host | checks the SSH key identity and approves codes over a group-restricted unix socket |
paas-shell-token |
host (/usr/local/bin/) |
operator CLI: mint/verify tokens |
The browser-token HMAC secret lives only on the host
(/etc/paas-shell/token-secret), readable by root and the service user —
never inside any app container. The Open Terminal API key is different:
it is automatically provisioned into all Dokku apps, including existing
apps and future deployments, as OPEN_TERMINAL_API_KEY alongside
OPEN_TERMINAL_API_URL. The host attaches Dokku apps to the internal
paas-terminal-api Docker network and supplies the stable URL
http://terminal.web:5000. The terminal app's Dokku proxy and public
domain are disabled before first deploy; there is no published host port.
Do not forward the API through shell or public nginx. Network membership
alone is not authorization: non-PaaS containers on Docker's default
bridge may also reach the endpoint, but protected endpoints require the
shared key. Treat both variables as sensitive, avoid logging them, and use
authenticated API calls from application code only when needed.
For example, from an app container with curl installed, check that
unauthenticated requests are rejected, then authenticate without printing
the key:
curl -s -o /dev/null -w '%{http_code}\n' "$OPEN_TERMINAL_API_URL/files/cwd"
# 401
curl -fsS -H "Authorization: Bear $OPEN_TERMINAL_API_KEY" \
"$OPEN_TERMINAL_API_URL/files/cwd"
The /health endpoint is intentionally unauthenticated for internal health
checks; it must not be reachable through a public app proxy. Every app's
runtime is in the same API trust boundary: a compromised app or operator
able to inspect app configuration can obtain the shared key and access the
same workspace. Rotating this key requires redistributing it to
all apps and restarting/redeploying running containers to pick it up; it is
not a per-app permission.
Workspace and public HTTPS
The terminal workspace and saved Copilot login are ephemeral. A container replacement/redeploy loses projects; do not rely on restart behavior for persistence. Save source to a separate durable Git remote, not only a Dokku deployment remote, before redeploying or destroying the terminal app.
Every public app, including developer-created apps, gets automatic Let's
Encrypt from the post-deploy hook; certificate renewal runs through cron.
Issuance errors are explicit. Check apex/wildcard A records and public port
80, then have an operator run sudo paas-enable-https <app> on the host.
Verify the actual HTTPS response. This never makes the terminal API public.
Audit trail
Every grant and every session is a JSON line in the service journal:
sudo journalctl -u paas-shell-backend
Look for auth.ssh_login.start, auth.ssh_login.approve,
auth.ssh_login.complete, and auth.ssh_login.reject for self-service
access; or auth.login for operator grants (user, app, token id, expiry,
source address). session.start and session.end record duration,
byte counts, and reason. Service liveness:
sudo systemctl status paas-shell-backend.
Operator reference
- Revoke/expire: tokens are stateless and expire on their own (
--ttl). To invalidate everything immediately, rotate the secret — replace/etc/paas-shell/token-secretwith a freshopenssl rand -base64 32andsudo systemctl restart paas-shell-backend. All outstanding tokens die. - Verify a token without using it:
sudo paas-shell-token verify --token <token>. - Update the service: re-run
bash scripts/terraform.sh apply(source changes re-trigger the install) orsudo paas-install-shell-backendon the host. - Backend health from the host:
curl -s http://127.0.0.1:8790/health, or through the app domain:https://shell.prototype.imeme.name/backend-health.