Files
punktfunk/docs-site/content/docs/running-as-a-service.md
T
enricobuehler 4d383811c0
ci / bun-nix (pull_request) Successful in 17s
ci / web (pull_request) Successful in 1m7s
apple / swift (pull_request) Successful in 1m38s
ci / rust-arm64 (pull_request) Successful in 1m38s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m46s
android / android (pull_request) Successful in 5m31s
ci / rust (pull_request) Failing after 9m2s
nix / flake (pull_request) Successful in 12m24s
fix(packaging): the same CAP_SYS_NICE broke KDE on FIVE channels, not one — Bazzite included
The Arch fix in the previous commit was incomplete. 0.26.0-1 granted the host CAP_SYS_NICE through
every Linux channel we ship, and each one breaks KWin identification the same way:

  * packaging/rpm/punktfunk.spec .......... %caps(cap_sys_nice=ep) in %files  <- Fedora AND Bazzite
                                            via rpm-ostree layering
  * packaging/bazzite/build-sysext.sh ..... setcap on the staging tree, recorded by mksquashfs
  * packaging/debian/build-deb.sh ......... setcap in the postinst
  * packaging/nix/nixos-module.nix ........ security.wrappers with capabilities = "cap_sys_nice=ep"
  * scripts/steamdeck/install.sh .......... setcap on $BIN, six lines after writing the .desktop
                                            whose Exec= it thereby voids

Bazzite was NOT a separate fault, as first reported here — it is this one. Verified by mounting the
published punktfunk-0.26.0-1-x86-64.raw: `getcap usr/bin/punktfunk-host` reports cap_sys_nice=ep,
stored as security.capability in the squashfs. The claim in packaging/arch/build-sysext.sh that
"file capabilities don't survive this squashfs path" is false and is corrected here; mksquashfs
records them, which is exactly why the image shipped one.

NixOS deserves its own note: a security.wrappers entry does not dodge the problem. The wrapper
raises the capability into its AMBIENT set before exec'ing the store binary, precisely so it
survives — which lands CAP_SYS_NICE in the exec'd process's permitted set and fails the readlink
identically to a file capability. ExecStart now points at the store path directly, which is also the
path packages.nix substitutes into the .desktop's Exec=, so the two finally agree.

Measured blast radius of holding a capability, same-uid reader, CachyOS kernel 7.1.6:

    /proc/PID/exe ....... EPERM   <- KWin's identification. Desktop sessions die.
    /proc/PID/root/* .... EPERM   <- xdg-desktop-portal reads .flatpak-info here to resolve an
                                     app id; the wlroots and Hyprland backends go through it
    /proc/PID/environ ... EPERM
    /proc/PID/cgroup .... OK
    /proc/PID/status .... OK
    /proc/PID/cmdline ... OK

Compositor backends, by exposure: KWin is broken outright (proven, field-confirmed). gamescope has
no identity gate and was never affected, which matches the field — only Desktop mode was reported.
Mutter drives Mutter's own D-Bus API, not the portal, and looks unaffected. wlroots and Hyprland go
through the ScreenCast portal, whose app-id resolution reads a path the capability blocks — a real
exposure, not something I reproduced end to end.

The sysext build now HARD-FAILS if a capability is staged, rather than trusting that the RPM payload
never carries one: a merged sysext's /usr is read-only squashfs, so a bad image cannot be repaired
on the box, and the spec was one %caps() away from baking one in again.

Docs corrected, because they advertised the capability as a feature:
  * docs-site running-as-a-service "GPU scheduling priority" — rewritten: the host carries no
    capability, why it must not, and how to clear a 0.26.0-1 install (Bazzite needs a new image)
  * docs-site configuration.md — the PYROWAVE_QUEUE_PRIORITY row no longer claims the packages grant it
  * packaging/bazzite/README.md — §6.5 still described the kde-desktop-setup.sh behaviour from
    before it stopped writing KWIN_WAYLAND_NO_PERMISSION_CHECKS and started REMOVING it; plus a
    note that 0.26.0-1 Desktop mode cannot be repaired in place
  * packaging/arch/README.md — the false "capabilities don't survive the sysext" line
  * CHANGELOG v0.26.0 PW1 — annotated with the 0.26.0-2 correction rather than rewritten, and the
    owed PyroWave-under-load A/B now says it needs a gamescope-only box

Verified: bash -n on all five changed shell files; nix-instantiate --parse on nixos-module.nix and
packages.nix; the published 0.26.0-1 sysext mounted and its capability read; getcap on an uncapped
file exits 0 with empty output, so the new build assertion cannot false-positive.
2026-08-09 09:56:36 +02:00

14 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. To make a machine an always-available host, run it as a service. First, what that service starts — then the two cases, a desktop you log into and a fully headless box.

What the unit starts

The bundled unit runs serve --gamestream, so it serves both the native punktfunk/1 plane and stock Moonlight clients. Every Linux package installs that unit as it ships and only rewrites the binary path, so a host you installed from apt, dnf, pacman or the Bazzite sysext has GameStream on. See what yours starts:

systemctl --user cat punktfunk-host

For a native-only host (no GameStream — its pairing runs over plain HTTP and its legacy encryption is weaker; see Security & Safe Use), drop the flag. The packaged unit is a package file that an upgrade replaces, so override ExecStart with a drop-in rather than editing it:

systemctl --user edit punktfunk-host
[Service]
ExecStart=
ExecStart=/usr/bin/punktfunk-host serve

The empty ExecStart= is required — without it systemd adds a second command instead of replacing the first — and the path must match the one systemctl --user cat printed (the distro packages use /usr/bin). Save, then systemctl --user restart punktfunk-host.

Windows is the other way round: an install from the setup .exe leaves GameStream off unless you tick it, and it is configured differently — see Windows below.

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.

Put your host.env in place first. The unit reads ~/.config/punktfunk/host.env and won't start until that file exists — no package creates it for you, they only ship a template to copy. The defaults in it are right for an ordinary desktop; your distro and desktop guides say if yours wants a different template (on Bazzite it's host.env.bazzite):

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, with its ExecStart pointing at the installed binary. There's 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), so edit the copy if your checkout lives somewhere else:

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 then fails with status=203/EXEC.

The host now starts whenever you log in. Check it 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 — so host.env is for policy, not session plumbing, and systemctl --user import-environment is not a prerequisite.

Restart the host with your desktop

Add one drop-in so the host follows 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, which is a confusing way to find out. The drop-in makes a compositor restart a host restart.

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 there but does nothing. To make the host come and go with the session on those, start it from the compositor's own config instead of enabling the unit — add exec systemctl --user start punktfunk-host to your sway config, or exec-once = systemctl --user start punktfunk-host to Hyprland's — and leave the unit itself disabled (systemctl --user disable punktfunk-host), so it isn't also started at login.

B. A headless, always-on host

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

Start by making the host service start at boot even when nobody logs in:

sudo loginctl enable-linger "$USER"

Then bring up a session automatically. How you do that is desktop-specific — auto-login, lock disable, and the session unit differ per compositor, so each is documented on its own page:

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 you don't need a separate session unit — see Bazzite and gamescope.

Windows

Punktfunk has first-class Linux and Windows hosts. On Windows it ships as a signed installer with an SCM service and a virtual-display driver — including Punktfunk's own indirect display driver the host pushes frames straight into. The Windows host is newer than the Linux host. (Not to be confused with the Windows client, which streams to a Windows PC.)

On Windows 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. Because it runs 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 for you (/VERYSILENT for unattended). Upgrades and uninstall are handled through Add/Remove Programs.

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

GameStream on Windows. Unlike the Linux unit, the installer leaves Moonlight compatibility off — it's a checkbox in the wizard (/MERGETASKS="gamestream" to select it unattended). There's no ExecStart to edit here: 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 left commented out, and with no value set the service falls back to serve --gamestream — so add --gamestream=off to that command if you want 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 just 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 a Windows host, run punktfunk-host service status from an elevated prompt on the machine itself.

GPU scheduling priority

The host binary carries no Linux capability, and on a KDE desktop it must not.

The PyroWave codec encodes on the same GPU shader cores your game is using, so a demanding game can crowd it out and the stream's frame rate drops with it. The fix is to ask the driver to schedule the encode ahead of the game, and every driver we tested gates that request on CAP_SYS_NICE. Version 0.26.0-1 granted it for that reason — and it broke desktop streaming on every KDE box, so 0.26.0-2 takes it away again. The other codecs use a separate video engine on the GPU and were never affected.

The two cannot coexist. To hand the host its virtual display, KWin first has to work out which program is asking, which it does by reading the connecting process's /proc/<pid>/exe and matching it against the .desktop file the packages install. Linux refuses that read for any process holding a capability the reader does not also hold — and KWin holds none. So a host with CAP_SYS_NICE is a host KWin cannot identify, and every session fails with:

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

which looks exactly like a missing .desktop file and cannot be fixed by reinstalling. Moving the grant into the systemd unit does not help either — same capability, same refused read.

If you are on 0.26.0-1, update. On the Bazzite image the /usr is read-only, so the only repair is the next image (sudo punktfunk-sysext update). Elsewhere you can clear it by hand:

getcap /usr/bin/punktfunk-host          # prints nothing when correct
sudo setcap -r /usr/bin/punktfunk-host  # clear it, then restart the host

Losing the capability costs frame pacing under a GPU-bound game and nothing else — the host asks for the elevated priority, is refused, and encodes at the normal one. PYROWAVE_QUEUE_PRIORITY=off stops it asking at all. If you stream only with gamescope (Steam Gaming Mode) you can grant the capability yourself and keep the pacing, at the cost of desktop streaming; gamescope has no such identity check.

Stopping and removing

After a Linux package update the user service keeps running the old binary until it's 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 instead, 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.