lscr.io/linuxserver/firefox, streamed over KasmVNC/Selkies. The interactive counterpart to Browserless: Browserless is headless and built for agents, Firefox is for the times you want eyes and hands on a real session.
Runs on i3 (debian-1) as a compose-only service, and is off by default.
Why not Neko?
Neko is the more popular dedicated “virtual browser,” but it streams over WebRTC and wants a UDP port range. The homelab’s only ingress is the Cloudflare tunnel → Traefik, which is HTTP/TCP. linuxserver/firefox streams over plain HTTP+WS, so it routes through Traefik and the tunnel with no special plumbing — the right fit for this topology.Why compose-only
Needsshm_size (Firefox crashes on Docker’s default 64 MB /dev/shm) and security_opt: seccomp:unconfined, neither of which swarm’s deploy: block can express — same exception bucket as Jellyfin and the downloads stack. It still joins the swarm overlay and routes via Traefik with service-level labels.
Endpoint
| What | URL |
|---|---|
| Visual browser | https://firefox.augustin.ai |
CUSTOM_USER / PASSWORD, set in firefox/.env). Enforced on the container’s 3001/https endpoint, which Traefik reaches via the insecure@file transport.
Lifecycle — start and stop on demand
This is a desktop-class workload with norestart:. Bring it up when you want it, shut it down when you’re done; the idle cost is the whole reason it isn’t always-on.
firefox/config/, so a stopped-and-restarted session picks up where you left off.
Stream tuning
Wayland/pixelflux mode otherwise makes the remote resolution follow your browser window — on a 5K display that’s a 4080×2608 @ 60fps software-encoded stream, which pegs the CPU and tends to wedge on reconnect (“waiting for stream”). The compose pins it instead:| Env | Value | Effect |
|---|---|---|
SELKIES_FRAMERATE | 30 | fixed 30fps |
SELKIES_IS_MANUAL_RESOLUTION_MODE | true | ignore the client window size |
SELKIES_MANUAL_WIDTH / _HEIGHT | 1920 / 1080 | force 1080p |
selkies server, not the ls.io shell scripts (which only honor the X11-path SELKIES_MANUAL_*). Confirm they took with docker logs firefox | grep "Stream settings" → Res: 1920x1080 | FPS: 30.0.
A note on idle cost
The framerate/resolution cap only governs the encoder, which is idle when no one’s connected. Theselkies server process still polls the display and burns ~20% of a core even with zero clients. There’s no env knob for that — the only real way to spend nothing when idle is to stop the container (above). Don’t leave it running as a background convenience; a software-streamed desktop is never free.
Reconnect: “waiting for stream”
When you disconnect, Selkies cleanly stops the pipeline (good for idle cost). If you return to a stale tab and it hangs on “waiting for stream,” the cause is Selkies’ service worker, not the server — the worker caches the app and holds a dead connection, and it survives a hard-refresh. Every server hop has been verified to upgrade WebSockets correctly (container nginx, Traefik, and the full Cloudflare tunnel all return101), so don’t chase the backend.
Fixes, fastest first:
- Incognito/private window — skips the service worker entirely; use it to confirm the worker is the culprit.
- Normal browser: DevTools → Application → Service Workers → Unregister, then Clear site data for
firefox.augustin.ai, and reopen the tab.