Files
punktfunk/docs-site/content/docs/running-as-a-service.md
enricobuehlerandClaude Fable 5 bfd0de8973
ci / rust-arm64 (pull_request) Successful in 1m30s
apple / swift (pull_request) Successful in 1m59s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 2m5s
ci / bun-nix (pull_request) Successful in 46s
ci / docs-site (pull_request) Successful in 1m25s
ci / docs-drift (pull_request) Successful in 46s
android / android (pull_request) Successful in 8m17s
ci / rust (pull_request) Successful in 22m6s
docs: two audiences, one home per fact — get-started rewrite, a Switching-from-Sunshine page, and install commands quoted from platforms.json
WP2 of the docs-and-onboarding overhaul (punktfunk-planning design/docs-and-onboarding-overhaul.md),
stacked on #337 (WP0+WP1).

Nav (meta.json) is now Get started / Guides / Switching from Sunshine / Reference / Troubleshooting.

Get-started track, rewritten for someone with no Linux expertise — one task per page, happy path,
under two minutes each: quickstart (5 steps), install (a pick-your-system hub), ubuntu, debian,
fedora, arch, bazzite (+ a firewall step it was missing), a new nixos page (moved out of install),
pairing. Every distro page quotes its install snippet through a new <Install platform="…"/> MDX
component that reads docs-site/src/data/platforms.json — a byte-identical snapshot of
data/platforms.json (the Docker build context is docs-site/ alone, same arrangement as
openapi.json) which scripts/ci/check-docs-drift.sh now gates. <Ports/> renders the port table the
same way on a new reference page, ports.mdx, so the four distro pages stop restating port lists.

platforms.json itself was fixed while wiring it up: the apt snippet lacked the keyring lines, the
winget one lacked `winget source add`, the Arch repo-add is now idempotent (grep guard — the
documented double-append gotcha disappears at the source), names match the real floors (Ubuntu
26.04+, Windows 11 22H2+), a web-console port entry and the firewall profile names were added, and
client platforms (Windows MSIX, macOS, TestFlight, Play, Steam Deck) so the website can render its
cards from the same file.

Dense material moved down rather than deleted: build-from-source.md (the three distro appendices),
the Mint/LMDE/Cinnamon analysis → requirements.md, TOFU/`--open` → security.md, Secure Boot MOK
paths and the Arch libavcodec soname refusal → troubleshooting, sysext channels/rollback/rebase →
updating.md, the 0.28.1 cert note → windows-host.

New page switching-from-sunshine.md: coexistence via the 47990 move (PUNKTFUNK_MGMT_BIND), the
Windows display-topology overlap, detect-conflicts exit semantics (1 only for an active host — the
troubleshooting page said "non-zero if any"), what maps to what, migration steps. The
troubleshooting Sunshine section is tightened and points there.

The three facts whose only home was a README now live in troubleshooting — the ffmpeg-libs weak
dependency (no NVENC on Fedora), the pacman double-append error, the ds_inhibit SELinux storm with
DualSense pads on Bazzite — and packaging/bazzite + packaging/arch READMEs are pointers.

Stale claims corrected against the tree: the packaged unit runs the native-only `serve` and
GameStream is opt-in on every route (kde, hyprland, security, steamos-host said otherwise);
host.env is optional (EnvironmentFile=-); Fedora 43 uses the `bazzite` group; the iOS clipboard
bridge exists; PUNKTFUNK_GAMESCOPE_SDR_NITS defaults to 203.

Guides trimmed for verbosity (~5% by words — they are fact-dense; every heading, command, number,
env var and link target is preserved, verified by token-inventory diffs). Tooling: docs-site README
and CONTRIBUTING describe the platforms snapshot; input/uninstall/support-matrix links retargeted.

Verified: check-docs-drift.sh and check-docs-links.sh green, docs-site `bun run build` + `bun run
lint` green, and the built site served locally to confirm every cross-page anchor the rewrite
links to (37) renders, plus the <Install/> blocks and the <Ports/> table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 19:08:38 +02:00

15 KiB

title, description
title description
Running as a Service Start the host at boot — for a desktop you log into, or a fully headless always-on machine.

Running serve in a terminal is fine for trying Punktfunk out; an always-available host runs as a service. First what the service starts, then the two cases: a desktop you log into, and a headless box.

What the unit starts

The bundled unit runs serve: the secure native-only host — the punktfunk/1 plane plus the management API. Stock Moonlight (GameStream) support is opt-in on every install route: its pairing runs over plain HTTP and its legacy encryption is weaker (see Security & Safe Use), so it belongs on trusted LANs you chose to enable it on. See what yours starts:

systemctl --user cat punktfunk-host

To serve stock Moonlight clients too, add one line to ~/.config/punktfunk/host.env (the unit's EnvironmentFile — no drop-in or unit editing needed) and restart:

PUNKTFUNK_GAMESTREAM=1
systemctl --user restart punktfunk-host

Then open the punktfunk-gamestream firewall service alongside punktfunk-native — your distro guide's firewall step has the commands.

Upgrading? Earlier releases baked --gamestream into the unit's ExecStart, so a packaged host served Moonlight by default. The upgrade replaces that unit with the native-only one — if you relied on Moonlight, add PUNKTFUNK_GAMESTREAM=1 to host.env as above. (A hand-made systemctl --user edit drop-in that sets its own ExecStart keeps winning either way — systemctl --user cat punktfunk-host shows what is in effect.)

Windows is the same by default — GameStream off unless you tick it in the installer, through its own mechanism — see Windows.

A. A desktop you log into

If you sit at the machine (or it auto-logs-in to a desktop), run the host as a systemd user service that starts with your session.

host.env is optional. The unit reads ~/.config/punktfunk/host.env if it exists (no package creates it — they ship templates under /usr/share) and runs with sane defaults without it: the host auto-detects the live session, so an ordinary desktop needs no file. Copy a template when you want to set a knob (the Bazzite one is host.env.bazzite; every knob is in Configuration):

mkdir -p ~/.config/punktfunk
# /usr/share/punktfunk/ on Fedora/Arch/Bazzite, /usr/share/punktfunk-host/ on Ubuntu
cp /usr/share/punktfunk/host.env.example ~/.config/punktfunk/host.env

Installed from a package (apt, dnf, pacman, or the Bazzite sysext) — the unit is already at /usr/lib/systemd/user/punktfunk-host.service, its ExecStart pointing at the installed binary. Nothing to copy:

systemctl --user daemon-reload             # the sysext route needs this; harmless elsewhere
systemctl --user enable --now punktfunk-host

Built from source — install the unit from your checkout, and take host.env from there too (cp scripts/host.env.example ~/.config/punktfunk/host.env). The unit's ExecStart points at %h/punktfunk/target/release/punktfunk-host (%h is your home directory); edit the copy if your checkout lives elsewhere:

mkdir -p ~/.config/systemd/user
cp scripts/punktfunk-host.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now punktfunk-host

Don't do the copy on a packaged install: a unit in ~/.config/systemd/user/ shadows the packaged one, and the source unit points at a build tree you don't have — the service fails with status=203/EXEC.

The host now starts whenever you log in. Check with systemctl --user status punktfunk-host.

You don't need to export anything for it. The host finds the live compositor session itself on every connect and works out where to reach it (WAYLAND_DISPLAY, XDG_RUNTIME_DIR, the session bus, sway's SWAYSOCK, Hyprland's instance signature) from the running compositor — host.env is for policy, not session plumbing, and systemctl --user import-environment is not a prerequisite.

Restart the host with your desktop

One drop-in makes the host follow your session's lifetime:

mkdir -p ~/.config/systemd/user/punktfunk-host.service.d
# /usr/share/punktfunk/ on Fedora/Arch/Bazzite, /usr/share/punktfunk-host/ on Ubuntu,
# scripts/ in a source checkout
cp /usr/share/punktfunk/punktfunk-host-desktop-session.conf \
   ~/.config/systemd/user/punktfunk-host.service.d/desktop-session.conf
systemctl --user daemon-reload
systemctl --user reenable punktfunk-host
systemctl --user restart punktfunk-host

Without it, restarting Plasma or GNOME — a crash, a log out and back in, "restart the shell" — leaves the host running against a compositor that no longer exists: it keeps listening and answering, and every session after that fails at capture. The drop-in makes a compositor restart a host restart.

On NixOS don't copy anything — the module has the option:

services.punktfunk.host.desktopSession = true;

Skip it on the headless/appliance route below (which has its own session unit), and on Sway or Hyprland, which don't hand their session to systemd: they never reach graphical-session.target, so the drop-in is harmless but does nothing. There, start the host from the compositor's own config instead of enabling the unit — exec systemctl --user start punktfunk-host in your sway config, or exec-once = systemctl --user start punktfunk-host in Hyprland's — and leave the unit disabled (systemctl --user disable punktfunk-host), so it isn't also started at login.

B. A headless, always-on host

No monitor and no login — a machine in a closet that's always ready — needs two things: a desktop session that comes up at boot, and the host service started without a login.

First let the host service start at boot with nobody logged in:

sudo loginctl enable-linger "$USER"

Then bring up a session automatically. Auto-login, lock disable and the session unit differ per compositor, so each has its own page:

  • GNOME: GNOME → Headless session.
  • KDE Plasma: KDE → Headless session.
  • Steam / gamescope: gamescope — the host launches its own session per client, so there's no separate session unit. A headless box that autologins into Gaming Mode needs one more thing: your user in the punktfunk group (sudo usermod -aG punktfunk "$USER", then log out and back in). Without it the host cannot stop the display manager to take that session over, so every connect quietly mirrors the box's own screen — which, headless, is a black one. See gamescope → autologin display managers.

Once a session comes up at boot, enable the host user service (section A) and reboot. The host comes up on that session.

Headless Bazzite

On Bazzite the host launches its own gamescope/Steam session per client, so no separate session unit is needed — see Bazzite and gamescope.

Windows

The Windows host (newer than the Linux one; not the Windows client, which streams to a PC) ships as a signed installer with an SCM service and Punktfunk's own indirect display driver the host pushes frames straight into.

The host runs as a LocalSystem service that launches into the interactive session, so it captures the secure desktop (UAC / lock screen) and survives reboots with nobody logged in — the same model Sunshine/Apollo use. At that privilege level, keep it on a trusted network and be deliberate about which machine you host on — see Security & Safe Use.

The easy path is the signed installer: download punktfunk-host-setup-<ver>.exe from the package registry (punktfunk-host-windows) and run it. It drops the host into C:\Program Files\punktfunk, installs the bundled pf-vdisplay virtual-display driver, and registers + starts the service (/VERYSILENT for unattended). Upgrades and uninstall go through Add/Remove Programs.

Prefer the CLI? punktfunk-host service install from an elevated prompt — see Windows Host. Hardware encode needs a GPU — NVIDIA (NVENC), AMD (AMF), or Intel (QSV); the host falls back to software H.264 without one.

GameStream on Windows. The installer leaves Moonlight compatibility off — a checkbox in the wizard (/MERGETASKS="gamestream" to select it unattended). There's no ExecStart to edit: the service launches whatever PUNKTFUNK_HOST_CMD in %ProgramData%\punktfunk\host.env says, which is also where the rest of the Windows host's configuration lives. To change it later, from an elevated prompt:

punktfunk-host service install --gamestream=on   # or --gamestream=off
punktfunk-host service restart

Registering the service by hand is the exception. A bare punktfunk-host service install writes a fresh host.env with PUNKTFUNK_HOST_CMD commented out, and with no value set the service falls back to serve --gamestream — add --gamestream=off to that command for the native-only host.

Firewall scope. The installer opens the streaming + console ports on Private and Domain networks only — not Public. If your LAN is (mis)classified Public, clients won't connect until you set it to Private (Windows Settings → Network), and the host logs a warning when it's on a Public network. For a trusted network Windows insists is Public, tick "Allow connections on Public networks" at install (or pass --allow-public-network to service install). See Security & Safe Use for the reasoning.

Verifying

After a reboot, from another machine on the network:

punktfunk reachable 192.168.1.50   # exit 0 = the host answered, 2 = it didn't
punktfunk hosts list --probe       # every saved host, online or offline

punktfunk is the headless client CLI — it ships in the Linux client packages (punktfunk-client) and with the Windows client. From a source checkout, punktfunk-probe --discover browses the LAN instead; it's a dev tool and isn't packaged. Or open a native client / Moonlight and look for the host.

If the host answers, it's up. If not, check journalctl --user -u punktfunk-host on the host — on Windows, punktfunk-host service status from an elevated prompt on the machine itself.

GPU scheduling priority

The PyroWave codec encodes on the same GPU shader cores your game uses, so a demanding game can crowd it out and the stream's frame rate drops. The fix is to ask the driver to schedule that encode ahead of the game, and every driver we tested gates the request on a single Linux capability, CAP_SYS_NICE. The other codecs use a separate video engine on the GPU and are unaffected either way.

That capability cannot live on the host, so it lives next to it. Every way of installing a Linux host — apt, dnf, pacman, the Bazzite sysext, the NixOS module, the Steam Deck script — ships a second, deliberately small program, punktfunk-encode-worker, and grants cap_sys_nice=ep to that. The host starts one per PyroWave session, hands it the captured frames, and takes the compressed video back; the worker talks to nothing else, not your desktop and not the network. punktfunk-host itself carries no capability, on any channel — its posture since 0.25.0, and the one KDE needs.

Never setcap punktfunk-host. Not by hand, not through a systemd AmbientCapabilities= line, not through a NixOS security.wrappers entry. All three put the capability in the same place, and all three take KDE desktop streaming away completely. There is no capability the host wants: the worker needs one, and your packages already gave it one.

Why: to hand the host its virtual display, KWin first works out which program is asking, by reading the connecting process's /proc/<pid>/exe and matching it against the .desktop file the packages install. Linux refuses that read unless the reader holds every capability the target holds — and KWin holds none. So a host carrying a capability is one KWin cannot identify, its restricted protocols are never offered, and every session fails at capture with:

KWin virtual output failed: KWin does not expose zkde_screencast_unstable_v1 to this client

which reads exactly like a missing or mis-installed .desktop file and survives reinstalling both ends. The first 0.26.0 packages granted the host the capability and shipped precisely this, on every Linux channel; the grant was revoked everywhere later the same day, and 0.27.0 is the first release whose version number carries that revocation to a machine that already installed one of them. If you see that error, check the binaries first — the host's own message names the capability when it finds one:

getcap /usr/bin/punktfunk-host              # correct output is nothing at all
getcap /usr/bin/punktfunk-encode-worker     # /usr/bin/punktfunk-encode-worker cap_sys_nice=ep

sudo setcap -r /usr/bin/punktfunk-host      # clear it, then restart the host

On the Bazzite image /usr is read-only, so there is nothing to repair in place — take the next image (sudo punktfunk-sysext update). On NixOS the worker is wrapped, because a file capability cannot live on a read-only store path: the module creates the wrapper and points the host at it, and the host's own ExecStart stays on the plain store path.

The grant is best-effort, and no session depends on it. A worker without the capability still encodes — it asks for the elevated priority, is refused, says so once, and runs at the normal one. So does a host that cannot find or start a worker at all: it encodes in-process, logs one line, and streams. The only thing at stake is frame pacing under a GPU-bound game. PYROWAVE_QUEUE_PRIORITY=off stops the host asking for priority, and PUNKTFUNK_ENCODE_WORKER=off keeps the encode in the host process — both on Configuration.

Stopping and removing

After a Linux package update the user service keeps running the old binary until restarted, and a package can't restart another user's --user units for you — Updating has the update command for every install method and the restart that finishes the job. The Windows installer restarts its own service.

To stop the host for now:

systemctl --user stop punktfunk-host          # Linux
punktfunk-host service stop                   # Windows, elevated prompt

To stop it for good, so it doesn't come back at login or boot:

# add punktfunk-web (the console) and punktfunk-kde-session (the headless KDE route) if you enabled them
systemctl --user disable --now punktfunk-host
rm -rf ~/.config/systemd/user/punktfunk-host.service.d   # any drop-ins you added
sudo loginctl disable-linger "$USER"                     # only if you enabled lingering

On Windows, punktfunk-host service uninstall from an elevated prompt stops the service, removes it, and removes the firewall rules it added. To remove the whole install, use Add/Remove Programs.

Neither removes ~/.config/punktfunk (Linux) or %ProgramData%\punktfunk (Windows) — your certificate, pairings and console password stay, so a reinstall picks up where you left off. See Uninstall to clear them out.