Platform matrix
Rendered from spec at76d46f3 (2026-10-25) · its source · Edit this page
Status: Accepted
Where the three platforms genuinely differ, and what lemonfiber must do about it.
Most cross-platform work is trivial — paths and line endings. The differences below are not: each one changes behaviour, and getting any of them wrong produces a silent failure rather than an error.
The four environments
Three operating systems, but four environments, because Linux has two Docker deployments that behave differently:
| Environment | Docker | Detection |
|---|---|---|
| macOS | Docker Desktop (VM + VirtioFS) | cfg!(target_os = "macos") |
| Linux native | Docker Engine directly | Linux, no Desktop context |
| Linux Desktop | Docker Desktop (VM) | Linux, Desktop context reported by daemon |
| Windows | Docker Desktop (WSL2) | cfg!(target_os = "windows") |
Linux-native versus Linux-Desktop matters because file ownership is real in one
and mapped in the other — so PUID/PGID is load-bearing in one and cosmetic in
the other.
The core has the four environments and does not detect the fourth: it never asks
the daemon for a Desktop context, so every Linux machine is read as Linux native
(ARCH-R27).
The matrix
| Concern | macOS | Linux native | Linux Desktop | Windows (WSL2) |
|---|---|---|---|---|
| Hardlinks | ✅ APFS/HFS+ | ✅ ext4/btrfs/xfs | ✅ | ⚠️ only inside WSL2 |
| Jellyfin HW transcode in Docker | ❌ | ✅ /dev/dri or NVIDIA |
✅ | ❌ |
| Native Jellyfin worth offering | ✅ VideoToolbox | ❌ pointless | ❌ pointless | ✅ QSV/NVENC |
host.docker.internal |
✅ built in | ❌ needs host-gateway |
✅ | ✅ |
| PUID/PGID | cosmetic | real | cosmetic | cosmetic |
| Docker autostart at login | ❌ manual | ✅ systemd | ❌ manual | ❌ manual |
| Docker API transport | Unix socket | Unix socket | Unix socket | named pipe |
| Bind-mount performance | VirtioFS, good | native | VirtioFS | poor across the boundary |
/dev/net/tun for VPN |
✅ | ✅ | ✅ | ✅ |
The five that actually bite
1. The Windows data-root boundary
Docker Desktop on Windows runs via WSL2. A bind mount from a Windows path
(C:\Media) crosses the drvfs/9p translation layer, where hardlinks do not work
correctly and I/O is slow.
So on Windows the data root must live inside the WSL2 filesystem. This is not a preference — the entire import model (ADR-0006) depends on it.
lemonfiber detects the boundary crossing during setup and explains it in
consequences rather than in filesystem terminology (C5-R14).
2. Hardware transcoding is a Linux-only Docker capability
The common claim “Docker means no hardware transcoding” is false on Linux,
where /dev/dri passthrough or the NVIDIA runtime works fully.
It’s true on macOS and Windows, where the VM cannot reach the encoder.
Consequence: native-mode Jellyfin
(ADR-0007) is offered only
where it buys something (A2-R7). Offering it on Linux would add a deployment
model for no gain; withholding it on macOS would leave 4K users stuck.
3. host.docker.internal doesn’t exist on native Linux
It’s a Docker Desktop convenience. On native Linux Docker it must be added explicitly:
extra_hosts:
- "host.docker.internal:host-gateway"
Without it, native-mode Jellyfin would be unreachable from Seerr — the container cannot resolve the name, and the symptom is “Seerr can’t see my library” with nothing in any log explaining why. Native mode is never accepted on native Linux, because Docker there can hardware-transcode (section 2): the wizard refuses the choice, so no stack reaches this failure and none carries the entry.
4. Autostart differs three ways
The reboot hole:
| Platform | What must happen |
|---|---|
| macOS | Docker Desktop set to open at login — off by default |
| Linux native | docker.service enabled — usually already true |
| Linux Desktop | Docker Desktop at login — off by default |
| Windows | Docker Desktop at login, WSL2 up first — off by default |
On three of four environments, the default is that nothing comes back after a reboot, with no error anywhere. Container restart policies don’t help, because the daemon itself isn’t running.
lemonfiber verifies the setting rather than assuming it, and reports
enabled-unverified where it cannot confirm (B8-R3, B8-R4) — the state where
an operator believes they have autostart and doesn’t.
5. PUID/PGID flips from cosmetic to load-bearing
On Docker Desktop, file ownership is mapped by the VM’s sharing layer and these values change nothing observable. On native Linux Docker, they determine who owns every file the stack writes — and getting them wrong produces permission failures far from their cause.
So the wizard asks only on native Linux (A2-R6). Asking elsewhere would be
a question the operator cannot meaningfully answer, violating the rule that every
question must earn its place.
Requirements
| ID | Requirement |
|---|---|
| ARCH-R27 | lemonfiber MUST distinguish four environments, including Linux-native from Linux-Desktop. |
| ARCH-R28 | On Windows, a data root outside the WSL2 filesystem MUST be detected and its consequence explained. |
| ARCH-R29 | Native Jellyfin mode MUST be offered only where Docker cannot hardware-transcode. |
| ARCH-R30 | On Linux, /dev/dri presence MUST be detected and passthrough enabled where available. |
| ARCH-R31 | Native Jellyfin mode MUST NOT be accepted on native Linux Docker, the one environment where host.docker.internal does not resolve without a host-gateway entry. |
| ARCH-R32 | Autostart MUST be configured per environment, and the prerequisite verified rather than assumed. |
| ARCH-R33 | PUID/PGID MUST be requested only on native Linux Docker. |
| ARCH-R34 | The Docker API transport MUST adapt to named pipes on Windows and Unix sockets elsewhere. |
| ARCH-R35 | Platform detection MUST be a single component; per-platform behaviour MUST NOT be scattered through call sites. |
ARCH-R35 is the maintainability one: cfg! checks sprinkled across modules
are how a codebase becomes untestable on any single machine. One component
decides, everything else asks it — and it can be faked in tests, so all four
environments are exercisable from one laptop.
Related
- ADR-0006 · ADR-0007
- C5 Storage · B8 Autostart
- component-model.md — where
platformlives