LWP runs any container that:
lwp-vnc-base (or provides its own KasmVNC setup)ENV LWP_START_APP="<command>" — the command xstartup will exec after openbox starts8080 (KasmVNC HTML5 client)| Detail | Value |
|---|---|
| Base | Ubuntu 22.04 |
| VNC server | KasmVNC (vncserver :1, websocket port 8080) |
| Audio | PulseAudio null sink, socket at /run/user/1000/pulse/native |
| Init | supervisord (root) → lwp-start-kasm.sh (lwp user) |
| App user | lwp (uid 1000) |
| App start | xstartup → openbox (kiosk mode) → $LWP_START_APP |
| Storage | Optional rclone WebDAV mount at $LWP_NC_MOUNT (supervisord lwp-rclone service) |
| Service | Role |
|---|---|
lwp-kasm |
Runs lwp-start-kasm.sh: starts PulseAudio, waits for PA socket, then starts KasmVNC |
lwp-rclone |
Mounts Nextcloud WebDAV at $LWP_NC_MOUNT (skipped if $LWP_NC_URL is unset) |
lwp-mounts |
Mounts the user’s extra SFTP/S3 remotes under ~/Mount/<name> from $LWP_MOUNTS (skipped if unset) |
lwp-vpn-relay |
Per-session SOCKS5 switchboard on 127.0.0.1:1081 — direct or via the user’s VPN gateway, driven by the window’s shield toggle (exits if $LWP_VPN_UPSTREAM is unset) |
When the app exits inside the VNC session:
xstartup detects $LWP_START_APP process exitedPOST $LWP_BACKEND_URL/api/sessions/self-stop with X-Session-Token: $LWP_SESSION_TOKEN/tmp/.lwp-app-exited flag, kills vncserverlwp-start-kasm.sh sees the flag, exits with code 0autorestart=unexpected, exitcodes=0) does not restart → container exitsFROM lwp-vnc-base:latest
# Install the application
RUN apt-get update && apt-get install -y --no-install-recommends \
gedit \
&& rm -rf /var/lib/apt/lists/*
# Tell xstartup what to launch
ENV LWP_START_APP="gedit"
Build and test:
docker build -t lwp-gedit:latest .
# Quick test (no LWP stack needed):
docker run --rm \
-e PUID=1000 -e PGID=1000 \
-e LWP_START_APP="gedit" \
--shm-size=512m \
-p 8080:8080 \
lwp-gedit:latest
# Open http://localhost:8080 — gedit should appear, audio works
Adds VS Code, Python 3, Node.js LTS, and Git on top of lwp-vnc-base.
FROM lwp-vnc-base:latest
# System packages
RUN apt-get update && apt-get install -y --no-install-recommends \
git curl wget jq \
python3 python3-pip python3-venv \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Node.js LTS
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
# VS Code
RUN curl -fsSL "https://code.visualstudio.com/sha/download?build=stable&os=linux-deb-x64" \
-o /tmp/vscode.deb \
&& apt-get install -y /tmp/vscode.deb \
&& rm /tmp/vscode.deb \
&& rm -rf /var/lib/apt/lists/*
ENV LWP_START_APP="code --no-sandbox --user-data-dir=/home/lwp/.vscode-data"
LWP injects these into every session container at launch:
| Variable | Value | Notes |
|---|---|---|
PUID |
1000 |
Run as uid 1000 (lwp) |
PGID |
1000 |
|
TZ |
UTC |
Override per app in admin panel |
LWP_SESSION_TOKEN |
random 32-byte token | Used for self-stop callback only |
LWP_BACKEND_URL |
http://backend:8000 |
Internal network URL |
LWP_NC_URL |
Nextcloud WebDAV URL | Only if user has NC configured |
LWP_NC_USER |
NC username | Only if user has NC configured |
LWP_NC_PASS |
NC password (plaintext in env) | Fernet-decrypted at launch |
LWP_NC_MOUNT |
/home/lwp/Files |
Mount point inside container |
You can also define extra env vars per app definition in the admin panel (env_json field). These merge with the above (app env takes precedence).
/etc/lwp/xstartup runs inside the Xvnc session. It sets up GTK theming, D-Bus, openbox, then executes $LWP_START_APP. To override, add a custom file in your image:
COPY my-xstartup /etc/lwp/xstartup
RUN chmod +x /etc/lwp/xstartup
Minimal working xstartup:
#!/bin/bash
export DISPLAY=:1
export PULSE_RUNTIME_PATH=/run/user/1000/pulse
openbox --config-file /etc/lwp/openbox.xml &
sleep 0.5
${LWP_START_APP}
# Self-stop on exit (copy these lines from the base image xstartup)
curl -sf -X POST "${LWP_BACKEND_URL:-http://backend:8000}/api/sessions/self-stop" \
-H "X-Session-Token: ${LWP_SESSION_TOKEN:-}" 2>/dev/null || true
touch /tmp/.lwp-app-exited
vncserver -kill :1 2>/dev/null || true
| Field | Example |
|---|---|
| Name | Developer Desktop |
| Container image | registry.example.com/lwp/dev-desktop:latest |
| Category | Development |
| Icon URL | any PNG URL |
| App type | stream (VNC) |
| Proxy port | 8080 |
| CPU limit | 2000m |
| Memory limit | 4Gi |
| SHM size | 1Gi (needed for Chromium/Electron apps) |
| Mount home | ✓ (creates persistent Docker volume per user) |
Audio — works automatically. PulseAudio creates a null sink; KasmVNC streams it to the browser via Web Audio API. No extra config needed in your Dockerfile.
Persistent home — enabling mount_home creates a Docker named volume lwp-home-{user_id} mounted at /home/lwp. Data survives container restarts.
Nextcloud files — if the user has Nextcloud configured, rclone mounts it at /home/lwp/Files using --vfs-cache-mode full. Files are cached locally on first access; SQLite databases (browser profiles etc.) work normally.
Large SHM — Chromium-based apps need /dev/shm ≥ 512 MB to avoid silent renderer crashes. Set shm_size = "1Gi" in the app definition.
Browser profile location — use /home/lwp/.config/chromium or ~/.mozilla (on the persistent home volume, if enabled). Do not store browser profiles on the Nextcloud mount — use the home volume for live profile data, and Nextcloud for documents/downloads.
XDG directories on Nextcloud — when Nextcloud is configured, ~/Documents, ~/Downloads, ~/Music, ~/Pictures, ~/Templates, and ~/Videos are automatically symlinked into the Nextcloud mount (~/Files/{dir}). No app configuration needed; save dialogs and file managers just work.
Self-stop — make sure your image runs the curl self-stop call in xstartup (or equivalent). Without it, the container will eventually be cleaned up by the 8-hour session watchdog, but the window will stay open in the browser until then.