appstream-gateway turns compatible Linux desktop applications into isolated browser sessions.
It supports AppImages, preinstalled X11 commands, packaged archives, and other compatible Linux desktop apps.
The stack is:
- one session manager exposed to users
- one container per browser session
xprawith its built-in HTML5 client inside each session container- a catalog-driven runtime for AppImages or preinstalled X11 commands
- a pluggable session backend: local Docker or Kubernetes Pods
This keeps each user in a separate desktop session instead of sharing one global VNC desktop.
- Isolated per-user sessions — each browser tab gets its own container with a virtual desktop
- Generic application support — AppImages, archives (tar/zip), in-image binaries, X11 commands
- Dynamic session management — containers created and destroyed on demand via HTTP API
- Signed session URLs — VNC traffic is never exposed directly; the manager proxies everything
- Bidirectional clipboard — copy/paste between host browser and session container
- File upload via drag & drop — drop files onto the browser to upload into the session
- xdg-open bridge — URLs and files opened inside the session are forwarded to the host browser
- Dynamic resize — session display follows the browser window size
- Local cursor mode — use your native browser cursor instead of the VNC-rendered one
- Fullscreen mode — immersive full-screen desktop experience (F11)
- Collapsible HUD — toolbar auto-hides, toggle with Ctrl+Shift+H
- Idle session reaping — unused sessions are automatically cleaned up
- CPU/RAM limits — per-session resource controls
- Shared download cache — AppImages are downloaded once and cached across sessions
- Resumable sessions — reconnect to your existing session from the same browser
- Persistent home volumes — per-client or per-app storage modes
- Public app permalinks — signed launch links that open the same app for the same user, without the manager UI
- Catalog-based configuration — curated production apps via
config/apps.json - Structured logging — JSON logs on stdout, Prometheus metrics on
/metrics - Kubernetes support — deploy sessions as Pods instead of Docker containers
- The
managerservice exposes the public HTTP entrypoint. GET /api/appsreturns the public catalog for the launcher UI.GET /api/apps/<id>/launch-linkcreates a signed permalink for a specific user key.GET /launch/<token>resolves the permalink, sets the user key cookie, and starts or resumes the app session.POST /api/sessionsstill exists for the admin dashboard and API-driven session creation.- The session container starts
xpraand the target app. - The browser connects to
/sessions/<id>/, which the manager proxies to the Xpra HTML5 session. - Session traffic is authorized by a signed cookie scoped to that session path.
- The manager exposes Prometheus metrics on
/metrics. - Session diagnostics and container logs are available through the admin API.
- Copy
.env.exampleto.env. - Set
SESSION_SECRET. - Set
ADMIN_API_TOKENif you want the admin APIs protected. - Leave
PUBLIC_BASE_URLempty to auto-detect the current host, or set it explicitly behind a proxy/public domain. - Start the stack:
docker compose up --build- Open the manager on the host or domain you exposed.
The default catalog includes demo apps (xterm, xclock, xeyes) and production apps like VSCodium, Firefox, Brave, Obsidian, Joplin, Logseq, Krita, and Lens.
To publish an app behind a bastion, generate a signed public launch link from the manager. The link points to the app directly, not the admin dashboard, and the session storage stays attached to the same client key as long as the app uses per-client or shared-app storage. That means the same app data comes back even if the session pod is recreated, as long as the underlying volume still exists.
The public permalink flow depends on a stable user key. The manager stores that key in a cookie named appweb_client_id, and the same key is reused when the link is opened again.
| App | Type | Category |
|---|---|---|
| Terminal (xterm) | command | debug / utility |
| XClock | command | demo |
| XEyes | command | demo |
| VSCodium | AppImage | development / editor |
| Obsidian | AppImage | productivity / notes |
| Joplin | AppImage | productivity / notes |
| Logseq | AppImage | productivity / knowledge |
| Krita | AppImage | graphics / creative |
| Firefox | archive (tar) | internet / browser |
| Brave Browser | archive (zip) | internet / browser |
| Lens Desktop | AppImage (local) | devops / kubernetes |
Edit config/apps.json and add an entry. Three source types are supported:
If you change the catalog and want the Kubernetes ConfigMap to stay in sync, run:
node scripts/render-catalog-configmap.mjsThis regenerates k8s/configmap-catalog.yaml from the JSON source of truth.
The catalog is what the manager reads at runtime. In Kubernetes, the app-web-catalog ConfigMap is mounted into the manager container from this generated manifest.
{
"id": "my-app",
"name": "My App",
"source": {
"type": "appimage-url",
"url": "https://example.com/MyApp.AppImage",
"sha256": ""
},
"launch": {
"args": "--no-sandbox",
"extractAndRun": true
},
"resources": { "cpuCores": 2, "memoryMb": 4096 },
"display": { "width": 1440, "height": 900, "depth": 24 }
}{
"id": "firefox",
"name": "Firefox",
"source": {
"type": "archive-url",
"url": "https://download.mozilla.org/?product=firefox-latest-ssl&os=linux64&lang=en-US",
"archiveEntrypoint": "firefox/firefox",
"archiveFormat": "auto"
},
"launch": { "args": "--no-remote" },
"resources": { "cpuCores": 2, "memoryMb": 4096 },
"display": { "width": 1440, "height": 900, "depth": 24 }
}{
"id": "xterm",
"name": "Terminal",
"source": {
"type": "command",
"command": "xterm -fa 'DejaVu Sans Mono' -fs 11"
}
}Each application entry supports:
| Field | Description |
|---|---|
id |
Stable slug used in URLs and storage |
name |
Display name |
description |
Optional description shown in the dashboard |
featured |
Show prominently in the dashboard |
categories |
Array of category tags for filtering |
tags |
Array of search tags |
source.type |
command, appimage-url, appimage-file, binary-path, or archive-url |
source.command |
Shell command (for command type) |
source.url |
Download URL (for appimage-url and archive-url) |
source.sha256 |
Optional checksum for supply-chain verification |
source.path |
Filesystem path (for appimage-file and binary-path) |
source.archiveEntrypoint |
Relative path to the executable inside the archive |
source.archiveFormat |
auto, zip, or tar |
source.archiveStripComponents |
Number of leading path components to strip |
launch.args |
CLI args appended to the app command |
launch.extractAndRun |
Pre-extract AppImage for better GTK compatibility |
launch.preLaunchCommand |
Command executed before the app starts |
launch.workingDirectory |
Working directory for the app |
launch.healthcheckPath |
HTTP path for the manager readiness probe |
resources.cpuCores |
CPU quota |
resources.memoryMb |
Memory limit in MB |
display.width |
Virtual screen width |
display.height |
Virtual screen height |
display.depth |
Color depth (16, 24, or 32) |
storage.mode |
ephemeral, per-client, or shared-app |
session.resume |
Allow reconnecting to existing sessions |
env |
Extra environment variables for the session |
| Shortcut | Action |
|---|---|
Ctrl+Shift+H |
Toggle HUD toolbar visibility |
F11 |
Toggle fullscreen mode |
The session container runs a lightweight HTTP bridge server for host↔session integration:
- Clipboard sync — bidirectional clipboard between browser and session
- File upload — drag & drop files onto the browser to upload them into
/data/home - xdg-open bridge — URLs opened inside the session (e.g. clicking links in VSCodium) are forwarded to your host browser
- File download — files passed to
xdg-openinside the session are downloaded to your host
The repository ships Kubernetes manifests in k8s/.
Those manifests deploy the manager in SESSION_BACKEND=kubernetes mode so it creates session Pods directly through the Kubernetes API instead of using a local Docker socket.
Included resources: namespace, service accounts, RBAC, app catalog ConfigMap, PVCs, manager Deployment, Service, Ingress, NetworkPolicy.
- Build and push the manager and session images somewhere your cluster can pull them from.
- Edit
k8s/deployment.yamland replace the placeholder images and public URL. - Create a real secret from
k8s/secret.example.yaml. - Check that your storage class supports
ReadWriteManyfor the PVCs. - Apply:
kubectl apply -f k8s/secret.yaml
kubectl apply -k k8s- Structured JSON logs on stdout (manager and session containers)
- Prometheus metrics on
/metrics(session count, launch duration, etc.) - Dashboard overview via
GET /api/overview - Session diagnostics via
GET /api/sessions/<id>/diagnostics(runtime state, logs, CPU/memory)
- Put the manager behind HTTPS and set
SECURE_COOKIES=true - Set a strong
SESSION_SECRETandADMIN_API_TOKEN - Use
SESSION_READY_TIMEOUT=5mor higher if heavier apps need more startup time - Use
SESSION_DPI=96by default; raise it only if you want larger UI scaling - Use
APP_UI_MODE=kioskon apps that should open with the HUD hidden by default - Electron-style apps are auto-detected and launched in kiosk mode; others fall back to the forced window layout
- Use
PUBLIC_LAUNCH_TOKEN_TTLto control how long shared app links stay valid - Set
PUBLIC_BASE_URLto the bastion-facing URL when you want generated permalinks to point at the public entrypoint - Use
DEFAULT_STORAGE_MODEand per-appstorage.modetogether to decide whether new sessions are ephemeral, per-client, or shared-app - Leave
RESUME_SESSIONS=trueif you want the same client key to reconnect to an existing session when it is still alive - Keep
ALLOW_CUSTOM_APPS=falseunless you trust the admins - Curate
config/apps.json— don't let users supply arbitrary binaries - Restrict the Docker socket to the manager only
- Monitor and prune stale session containers and cache volumes
- Scrape
/metricsfrom Prometheus
After a deploy, run scripts/smoke-release.sh with ADMIN_API_TOKEN to verify the launch path for a few representative apps before declaring the release done.
This approach works well for many X11/Electron/AppImage applications, but it is still application streaming over Xpra:
- Latency-sensitive GPU apps will be a poor fit
- Audio, USB, webcam, DRM, and advanced window manager integrations may need extra work
- Every session consumes RAM and CPU, so sizing matters
| Path | Description |
|---|---|
Dockerfile |
Generic session image |
app/entrypoint.sh |
Session bootstrap (Xpra, app launch, bridges) |
app/public/index.html |
Legacy noVNC client kept for reference |
app/file-bridge.py |
Session bridge server (clipboard, upload, xdg-open) |
app/xdg-open-bridge.sh |
xdg-open override that forwards to the bridge |
manager/ |
Session manager, proxy, and API server |
config/apps.json |
Curated application catalog |
k8s/ |
Kubernetes manifests |
docker-compose.yml |
Local development stack |