Web shell (browser terminal)
The shell app gives developers a terminal inside their app's own running
container, straight from the browser — no SSH key, no VPN. It lives at
https://shell.54.170.127.122.sslip.io and is installed automatically with the
platform.
Sessions are real shells inside the app container (docker exec, same entry
point as sudo paas-app-shell <app> on the host): unprivileged runtime user,
minimal environment, and every container restriction from
App container confinement applies. A browser
shell can never reach the host or another tenant.
Getting access
Access is granted per person, per app, for a limited time by the platform operator. There is no self-service signup and no password.
The operator runs, on the Dokku host:
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.54.170.127.122.sslip.io/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 access URL in a browser.
- 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: arrows, Ctrl keys, resizing all work. 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 shell
The shell environment ships the GitHub Copilot CLI: type copilot for an
AI pair-programmer in the terminal. Sign-in uses GitHub's device code flow
— see GitHub Copilot CLI for the steps and requirements.
How a session is scoped
- The token binds one user to exactly one app. The browser asks for the
app taken from the token; asking for any other app fails with
403, and an unknown/stopped app fails with404— all enforced server-side by the shell backend before the WebSocket even opens. - The shell lands inside that app's own container (found via Dokku's
container labels), as the unprivileged runtime user (
uid 32767), with a scrubbed environment (TERM,HOME=/app,PATH, … — never the platform's or other apps' config). - 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 terminal page (xterm.js) |
paas-shell-backend.service |
the Dokku host, 127.0.0.1:8790 |
verifies tokens, spawns the pty inside app containers, enforces scoping/caps/timeouts |
| nginx include | host (/home/dokku/shell/nginx.conf.d/) |
routes /ws/terminal and /login from the app domain to the backend |
paas-shell-token |
host (/usr/local/bin/) |
operator CLI: mint/verify tokens |
The 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.
Audit trail
Every grant and every session is a JSON line in the service journal:
sudo journalctl -u paas-shell-backend
Look for auth.login (who was granted access to which app, token id,
expiry, source address), session.start, and session.end (duration,
byte counts, 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
terraform 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.54.170.127.122.sslip.io/backend-health.