📚 Docs

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)

  1. Open the operator-provided access URL in a browser (or use the SSH flow above).
  2. 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.
  3. 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/sh and 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 default terminal app includes developer tools such as git, ssh, curl, Python 3, jq, less, and vim; 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

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