> ## Documentation Index
> Fetch the complete documentation index at: https://docs.augustin.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Firefox

A full, visual Firefox you click around in from your own browser — `lscr.io/linuxserver/firefox`, streamed over KasmVNC/Selkies. The interactive counterpart to [Browserless](/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](https://github.com/m1k1o/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

Needs `shm_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](/media/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` |

Gated by HTTP basic auth (the container's built-in `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 no `restart:`. 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.

```bash theme={null}
cd ~/apps/firefox
set -a; source .env; set +a

docker compose up -d      # start a session
docker compose stop       # stop (keeps container + browsing profile)
docker compose down       # remove container (profile in ./config persists)
docker compose logs -f    # logs
```

The browsing profile, history, and extensions persist in `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                   |

These are read directly by the `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. The `selkies` 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 return `101`), 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.

To verify the WS path itself is healthy (it almost always is):

```bash theme={null}
set -a; source firefox/.env; set +a
curl -s --http1.1 -o /dev/null -w '%{http_code}\n' \
  -u "$CUSTOM_USER:$PASSWORD" \
  -H 'Connection: Upgrade' -H 'Upgrade: websocket' \
  -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
  https://firefox.augustin.ai/websocket    # expect 101 (use --http1.1; HTTP/2 returns a misleading 426)
```
