Files
punktfunk/packaging/bazzite
enricobuehler 2190dad2ad feat(packaging/bazzite): systemd-sysext replaces rpm-ostree layering as the primary install path
Layering is a last resort per the Bazzite docs (slows every OS update, can
block upgrades until removed); a sysext never enters an rpm-ostree
transaction, survives OS updates, and installs/updates with no reboot —
the mechanism Fedora Atomic ships via fedora-sysexts.

- build-sysext.sh wraps the built host+web RPMs into punktfunk-<V-R>-x86-64.raw:
  /etc payload relocated to /usr/share/punktfunk/etc (a sysext carries only
  /usr), the punktfunk-sysext helper embedded, ID=fedora + VERSION_ID pinned
  (merges on Bazzite via ID_LIKE; REFUSED after a major rebase instead of
  running soname-broken binaries — both behaviors validated live on Bazzite 43).
  SELinux labels are baked in as squashfs pseudo-xattrs from matchpathcon:
  unlabeled files run fine for user units but system daemons are DENIED
  (udev couldn't read the gamepad rule under enforcing) — validated on-glass.
  Refuses duplicate input package names (a stale noarch punktfunk-web next to
  the x86_64 one built a chimera image with the dead node launcher once).
- punktfunk-sysext.sh: install/update/status/remove against per-Fedora-major
  feeds (…/generic/punktfunk-sysext/f43[-canary]), SHA-256-verified, applies
  the udev/sysctl scriptlet work + /etc copies, prints the layering-migration
  hint. Live-validated on the .41 Bazzite box incl. service restart + web console.
- publish-sysext-feed.sh + rpm.yml: build + publish the image per matrix leg
  (fedver 43/44), canary feeds pruned to 6, stable release assets attached.
- update-punktfunk.sh warns when the sysext shadows a layered install.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 16:39:01 +00:00
..

Setting up punktfunk on Bazzite

A step-by-step setup guide for running the punktfunk low-latency streaming host on Bazzite (the immutable, Fedora-Atomic gaming distro). Everything below is grounded in this repo's packaging and ops files; where something is not yet published or not in the repo, it's flagged explicitly. For the higher-level packaging rationale ("why not Flatpak", the build), see ../README.md.

What you get on Bazzite: it already ships the three things punktfunk normally has to fight for — gamescope, PipeWire/WirePlumber, and (on the -nvidia images) the NVIDIA driver with NVENC/EGL. The only genuinely new runtime bits punktfunk adds are ffmpeg-libs (with NVENC, from RPM Fusion nonfree), opus, and libei. Source: packaging/README.md, packaging/rpm/punktfunk.spec.

⚠️ COPR note (Path C only). The legacy layering path's commands reference a COPR project named enricobuehler/punktfunk that is operator-run and may not be published (see packaging/copr/README.md); layer from the Gitea RPM registry instead (../rpm/README.md, the repo file https://git.unom.io/api/packages/unom/rpm/bazzite.repo) — it's what CI actually publishes to. Paths A (sysext) and B (bootc) don't involve the COPR at all.


1. Choose an install path

There are three paths on Bazzite, driven by different files in packaging/:

Path Driven by What it does Best for
A — systemd-sysext recommended packaging/bazzite/punktfunk-sysext.sh + build-sysext.sh (published by .gitea/workflows/rpm.yml) Overlays the host onto /usr as a system extension — no layering, no reboot, one-command updates Everyone; the default
B — bootc / OCI image packaging/bootc/Containerfile Bakes punktfunk into a FROM bazzite-nvidia image once; you bootc switch any number of hosts onto it Fleets, reproducible appliances, no per-host drift
C — rpm-ostree layering (legacy) packaging/rpm/ + the Gitea RPM registry Layers the punktfunk RPM onto your deployment with rpm-ostree install Only if you specifically want the RPM database to own the files

Why A over C: the Bazzite docs treat layering as a last resort — every layered package makes every OS update slower and can block upgrades entirely until removed. A sysext never enters an rpm-ostree transaction: it merges/unmerges at runtime, survives OS updates, and updating punktfunk is one command with no reboot (layering needs one per update). It's the mechanism the Fedora Atomic maintainers ship via fedora-sysexts. All paths require the same first-run setup (sections 36).

Run on the Bazzite host:

# One-time bootstrap; afterwards the tool is on PATH as `punktfunk-sysext` (it ships inside
# the image). `--channel canary` for rolling main-branch builds instead of releases.
curl -fsSLO https://git.unom.io/unom/punktfunk/raw/branch/main/packaging/bazzite/punktfunk-sysext.sh
sudo bash punktfunk-sysext.sh install

This downloads the newest image for your Fedora base (host + tray + web console, SHA-256-verified from the feed …/packages/unom/generic/punktfunk-sysext/f<ver>[-canary]/), installs it as /var/lib/extensions/punktfunk.raw, merges it, and immediately applies what the RPM scriptlets would have (udev reload, sysctl) plus the two /etc files a sysext can't carry (the gamescope-session drop-in and the tray autostart entry, staged under /usr/share/punktfunk/etc/). No reboot at any point. Day-2:

sudo punktfunk-sysext update    # fetch + merge the newest build (then restart the user service)
sudo punktfunk-sysext status    # merged?, installed vs latest, channel/feed
sudo punktfunk-sysext remove    # unmerge + delete; ~/.config/punktfunk is left alone

Details worth knowing:

  • The image embeds ID=fedora + VERSION_ID (matched through Bazzite's ID_LIKE), so after a major Bazzite rebase (F43 → F44) the old image is refused instead of merging soname-broken binaries — punktfunk-sysext update then fetches the image built for the new base (feeds exist per Fedora major, from the same CI matrix as the RPM groups).
  • SELinux labels are baked into the image at build time (squashfs pseudo-xattrs computed from the targeted policy) — without them udev couldn't read the gamepad rule under enforcing. Validated live on Bazzite 43.
  • Migrating from layering (path C): install the sysext (it shadows the layered copy at once), then sudo rpm-ostree uninstall punktfunk punktfunk-web && systemctl reboot.

Path B — bootc image (FROM bazzite-nvidia)

The image is built off-host (on any machine with podman) from packaging/bootc/Containerfile, which bases on ghcr.io/ublue-os/bazzite-nvidia:stable (override with --build-arg BASE_IMAGE=…), enables RPM Fusion free + nonfree, adds the Gitea RPM repo (--build-arg PUNKTFUNK_RPM_GROUP=…, default bazzite), and installs the host and the web console (punktfunk punktfunk-web). It uses the Gitea registry rather than the COPR specifically because the registry carries punktfunk-web (COPR's mock chroot can't build it — no bun).

# Build + push (run from the repo root, on your builder machine):
podman build -t ghcr.io/<you>/bazzite-punktfunk -f packaging/bootc/Containerfile .
podman push  ghcr.io/<you>/bazzite-punktfunk

# On each target Bazzite host:
sudo bootc switch ghcr.io/<you>/bazzite-punktfunk && systemctl reboot

⚠️ The image installs from the Gitea RPM registry (group bazzite), so Path B depends on that registry being populated — CI (.gitea/workflows/rpm.yml) publishes punktfunk + punktfunk-web on every push to main. Packages are unsigned with GPG-signed metadata (repo_gpgcheck=1), matching packaging/rpm/README.md.

Path C — rpm-ostree layering (legacy)

Run on the Bazzite host. (Commands verbatim from packaging/README.md.)

# 1. RPM Fusion (free + nonfree) — provides the NVENC-capable ffmpeg-libs.
#    Usually already enabled on Bazzite; harmless to re-run.
rpm-ostree install \
  https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm \
  https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm

# 2. Enable the punktfunk COPR repo  ⚠️ requires the COPR to be published (see callout above)
sudo wget -O /etc/yum.repos.d/_copr_punktfunk.repo \
  https://copr.fedorainfracloud.org/coprs/enricobuehler/punktfunk/repo/fedora-$(rpm -E %fedora)/

# 3. Layer punktfunk and reboot to activate the new deployment.
rpm-ostree install punktfunk
systemctl reboot

The reboot is mandatoryrpm-ostree install stages a new deployment that only takes effect on the next boot. This is normal atomic-distro behavior, not a punktfunk quirk.

Updating a Path-C host — rpm-ostree upgrade is NOT enough

⚠️ rpm-ostree upgrade will not update punktfunk on its own. upgrade bumps the base image and only re-resolves layered packages when the base changes. A Bazzite base can sit frozen for months (a pinned :stable tag, a paused rebase), so rpm-ostree upgrade keeps reporting "No updates available" and your layered punktfunk stays put even after new RPMs land in the repo. (Diagnose: rpm-ostree status shows the base Version: unchanged, while dnf -q repoquery --upgrades punktfunk lists newer builds.)

To actually pull a newer host on a static base, force rpm-ostree to re-resolve just the punktfunk layer — remove + re-add the same names in one transaction:

sudo rpm-ostree refresh-md --force
sudo rpm-ostree update \
  --uninstall punktfunk --uninstall punktfunk-web \
  --install   punktfunk --install   punktfunk-web
systemctl reboot

Or just run the helper, which detects what's layered and does the above:

sudo bash packaging/bazzite/update-punktfunk.sh          # stage; reboot when ready
sudo bash packaging/bazzite/update-punktfunk.sh --reboot # stage + reboot now

Channel gotcha: the re-resolve picks the highest version across every enabled /etc/yum.repos.d/punktfunk*.repo. If punktfunk-canary.repo is enabled alongside the stable punktfunk.repo, canary's <next-minor>.0-0.ciN outranks the stable X.Y.Z-1 and the box silently tracks canary. Enable exactly one channel — set enabled=0 in the other repo file.


2. Prerequisites — what Bazzite gives you vs. what you must still do

Already satisfied on Bazzite (-nvidia images):

  • NVIDIA driver: libnvidia-encode (NVENC) + libEGL_nvidia for the zero-copy path.
  • gamescope — the default compositor backend punktfunk uses on Bazzite.
  • PipeWire + WirePlumber — the capture/audio graph.

You must still do (covered below):

  1. Reboot after layering / rebasing (section 1).
  2. Join the input group and ensure the udev rule is installed (section 3) — required for virtual gamepads / DualSense.
  3. Place host.env and enable the systemd user service (sections 45).
  4. Open firewall ports (section 6).

RPM Fusion's ffmpeg-libs is a weak dependency (Recommends: in the spec) — the package installs without it, but NVENC encoding will fail at runtime if it's missing. The RPM Fusion step in section 1 covers this.


3. udev rule + the input group

punktfunk creates virtual X-Box-360 gamepads via /dev/uinput and virtual DualSense pads via /dev/uhid (kernel hid-playstation driver — LEDs, adaptive triggers, touchpad, gyro). The udev rule grants the input group access to both nodes.

The RPM already installs the rule to /usr/lib/udev/rules.d/60-punktfunk.rules and its %post reloads udev. So on a packaged install (Path A or B) you only need to join the input group:

ujust add-user-to-input-group     # then LOG OUT and back in (or reboot)

⚠️ On Bazzite use ujust add-user-to-input-group, NOT sudo usermod -aG input $USER. Bazzite is an atomic (rpm-ostree) OS where /etc/group is managed declaratively — a plain usermod either doesn't stick or gets reverted on the next update. The ujust recipe edits the group the immutable-OS-correct way (and reloads udev). (ujust ships with Bazzite; ujust --list shows all recipes.)

🔁 The group change does not apply to your current login session — you must re-login (or reboot). Until then, gamepad creation fails with a permission error on /dev/uinput. This is the single most common "why don't my gamepads work" gotcha.

If you installed from a tarball/source instead of the RPM (so the rule isn't in place), install it manually — the exact commands from the rule file's header (scripts/60-punktfunk.rules):

sudo cp scripts/60-punktfunk.rules /etc/udev/rules.d/
ujust add-user-to-input-group      # NOT `usermod` on Bazzite (see the note above); then re-login
sudo udevadm control --reload-rules && sudo udevadm trigger

The rule contents, for reference:

KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", GROUP="input", MODE="0660", TAG+="uaccess"
KERNEL=="uhid",   SUBSYSTEM=="misc", OPTIONS+="static_node=uhid",   GROUP="input", MODE="0660", TAG+="uaccess"

4. Configure host.env

The systemd user unit reads its environment from ~/.config/punktfunk/host.env (EnvironmentFile=%h/.config/punktfunk/host.env in scripts/punktfunk-host.service). The RPM ships a Bazzite-tuned template at /usr/share/punktfunk/host.env.bazzite. Copy it into place:

mkdir -p ~/.config/punktfunk
cp /usr/share/punktfunk/host.env.bazzite ~/.config/punktfunk/host.env
# then edit ~/.config/punktfunk/host.env

The Bazzite template (packaging/bazzite/host.env) contains:

XDG_RUNTIME_DIR=/run/user/1000
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus

# gamescope backend: spawned per session, no compositor login required.
PUNKTFUNK_COMPOSITOR=gamescope
PUNKTFUNK_VIDEO_SOURCE=virtual
PUNKTFUNK_GAMESCOPE_APP=steam -gamepadui

# gamescope hosts its own EIS input socket — input lands in the nested session.
PUNKTFUNK_INPUT_BACKEND=gamescope

# GPU zero-copy capture (dmabuf -> CUDA -> NVENC). Auto-falls back to CPU if unavailable.
PUNKTFUNK_ZEROCOPY=1

#RUST_LOG=info

What each knob means and why these are the Bazzite defaults:

Knob Value Meaning
XDG_RUNTIME_DIR / DBUS_SESSION_BUS_ADDRESS …/user/1000 Session bus / runtime dir. 1000 assumes your user is UID 1000 — change both if id -u says otherwise.
PUNKTFUNK_COMPOSITOR gamescope The Bazzite default. The host spawns a headless gamescope per session at the client's exact resolution/refresh and captures its PipeWire node — so you need no graphical desktop login to stream. Bazzite ships gamescope, so this "just works."
PUNKTFUNK_VIDEO_SOURCE virtual Create a per-client virtual output at the client's exact WxH@Hz (the flagship "native resolution, no scaling" mode), vs. portal which captures an existing monitor.
PUNKTFUNK_GAMESCOPE_APP steam -gamepadui The command launched inside the nested gamescope — here, a SteamOS-style couch UI. Set it to whatever you want the session to run.
PUNKTFUNK_INPUT_BACKEND gamescope Inject mouse/keyboard/gamepad into the nested gamescope via its own EIS socket.
PUNKTFUNK_ZEROCOPY 1 GPU zero-copy capture (dmabuf → CUDA → NVENC). Falls back to CPU automatically if unavailable.
RUST_LOG (commented) Uncomment RUST_LOG=info for verbose logs while debugging.

Optional — a real DualSense for clients holding one: add PUNKTFUNK_GAMEPAD=dualsense to present games a virtual Sony DualSense (lightbar, adaptive triggers, touchpad, motion) instead of the default X-Box-360 pad. The feedback flows back to a real DualSense on the client.

Alternative — drive the full Plasma/GNOME desktop instead of a nested gamescope (per the template's footer comment): switch to PUNKTFUNK_COMPOSITOR=kwin and PUNKTFUNK_INPUT_BACKEND=libei, and run the host inside a KDE session with WAYLAND_DISPLAY / XDG_CURRENT_DESKTOP set. The full knob list (FEC %, per-stage timing, etc.) is in scripts/host.env.example / /usr/share/punktfunk/host.env.example.

The gamescope default is what makes Bazzite the easy path: it's a headless, per-session compositor — no desktop login, no display manager, no --drm scanout. You don't need any of the headless-KDE bring-up scripts (scripts/headless/run-headless-kde.sh) on Bazzite unless you deliberately switch to the KWin backend.


5. Enable and start the service

punktfunk runs as a systemd --user service (not root) — it needs your graphical/user session's PipeWire and D-Bus. The unit (scripts/punktfunk-host.service) is installed by the RPM into the user unit directory.

systemctl --user daemon-reload
systemctl --user enable --now punktfunk-host
# Management web console (pairing + status), if you installed punktfunk-web (it ships in the Gitea
# RPM registry / bootc image — COPR can't build it; see ../rpm/README.md). Read the login password:
systemctl --user enable --now punktfunk-web
journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p'   # then open https://<host-ip>:47992

Check health and logs:

systemctl --user status punktfunk-host
journalctl --user -u punktfunk-host -f

What serve actually starts. The bundled unit's ExecStart runs punktfunk-host serve --gamestream, so out of the box you get the unified host: the native punktfunk/1 (QUIC) plane — always on in serveplus the GameStream/Moonlight-compat planes (mDNS discovery, pairing, RTSP, the fixed GameStream ports) and the management REST API on 47990. The --gamestream flag is what adds the Moonlight surface; GameStream pairs over plain HTTP and its legacy encryption is weaker than the native plane's (security-review #5/#9), so it's opt-in and trusted-LAN only. For a secure native-only host, drop --gamestream from the unit's ExecStart (bare serve) — native clients still work; only stock Moonlight stops. (Source: crates/punktfunk-host/src/main.rsserve runs the native plane + mgmt; --gamestream adds gamestream::serve.)

Unit caveat: scripts/punktfunk-host.service declares only After=pipewire.service and (in the upstream/dev layout) assumes the binary at %h/punktfunk/target/release/punktfunk-host. The RPM-installed binary lives at /usr/bin/punktfunk-host. If systemctl --user cat punktfunk-host shows ExecStart pointing at a missing path in your home dir, drop an override (systemctl --user edit punktfunk-host) setting ExecStart=/usr/bin/punktfunk-host serve --gamestream (or bare serve for a secure native-only host).


6. Firewall

⚠️ There is no firewall script or firewall doc in the repo. The ports below are derived directly from the code constants (crates/punktfunk-host/src/gamestream/mod.rs, mgmt.rs) and the GameStream-host port-map (design/gamestream-host-plan.md). Treat the firewall-cmd lines as recommended-but-verified, not a checked-in script.

GameStream / Moonlight ports (fixed; Moonlight derives them from the HTTP base). These only apply when the host runs serve --gamestream (the bundled unit's default); on a bare-serve native-only host you don't open them:

Port Proto Purpose
47984 TCP HTTPS nvhttp (paired, mutual-TLS)
47989 TCP HTTP nvhttp (/serverinfo, /pair PIN flow)
48010 TCP RTSP handshake
47998 UDP Video RTP (+ FEC)
47999 UDP ENet control stream + remote input
48000 UDP Audio (Opus)
5353 UDP mDNS — so Moonlight auto-discovers the host (_nvstream._tcp.local.)

Management REST API: TCP 47990 — but serve binds it to 127.0.0.1 (loopback) by default, so you do not open it in the firewall unless you deliberately move it off loopback with --mgmt-bind IP:PORT (which also requires --mgmt-token). Leave it closed for a normal setup.

Open the GameStream ports with firewalld (Bazzite uses firewalld):

sudo firewall-cmd --permanent --add-port=47984/tcp \
                  --add-port=47989/tcp \
                  --add-port=48010/tcp
sudo firewall-cmd --permanent --add-port=47998/udp \
                  --add-port=47999/udp \
                  --add-port=48000/udp \
                  --add-port=5353/udp
sudo firewall-cmd --reload

If you also run the native punktfunk/1 host (punktfunk-host punktfunk1-host, not started by the default unit):

  • QUIC control plane: UDP 9777 (default --port; change with --port N).
  • Data plane: an ephemeral UDP portpunktfunk1-host binds 0.0.0.0:0 and tells the client which port it got, so there is no fixed data port to open. For a restrictive firewall you'd need to allow the ephemeral UDP range; the repo does not pin one.
# Only if you run `punktfunk1-host`:
sudo firewall-cmd --permanent --add-port=9777/udp && sudo firewall-cmd --reload

6.5 Desktop (KDE) mode — stream the desktop at the client's resolution (optional)

The host auto-detects the live session per connect: in Steam Gaming Mode it attaches to the running gamescope (no setup); switch the box to the KDE Desktop and it drives a KWin virtual output at the connecting client's exact resolution (no TV-stretch, churn-free). The Desktop path needs one one-shot setup the first time, because a normal KDE login withholds two things the headless host needs — the privileged zkde_screencast virtual-output protocol, and an auto-approved RemoteDesktop input grant:

bash /usr/share/punktfunk/bazzite/kde-desktop-setup.sh
# then log out + back into the KDE Desktop session once (or reboot) so KWin restarts with the flag

That writes ~/.config/environment.d/10-punktfunk-kwin.conf (KWIN_WAYLAND_NO_PERMISSION_CHECKS=1) and seeds the kde-authorized RemoteDesktop grant into ~/.local/share/flatpak/db/. Gaming Mode is unaffected. To connect from Desktop Mode, switch to it (Steam → Power → Switch to Desktop), then connect the client; switching mid-stream requires a reconnect (the host resolves the backend per connect).


7. Verify it's working

1. Watch the startup log:

journalctl --user -u punktfunk-host -f

A healthy serve startup logs the punktfunk-host (punktfunk_core ABI v…) banner, then mDNS advertising, and an RTSP listening line on port 48010. No NVENC/EGL errors on the first connection.

2. Pair a stock Moonlight client (recommended first test):

  • Open Moonlight on your phone/PC on the same LAN — the host should appear automatically (mDNS).
  • Select it; Moonlight shows a 4-digit PIN. The host completes the GameStream pairing handshake (it persists across restarts).
  • Launch the app — you should get video at your client's native resolution/refresh, with the nested steam -gamepadui (or whatever PUNKTFUNK_GAMESCOPE_APP you set) running inside gamescope.

3. (Optional) native punktfunk/1 client — only if you're running the separate punktfunk1-host. The repo's reference client is punktfunk-probe, e.g. punktfunk-probe --mode 1280x720x120 --out /tmp/a.h265 (add --pin HEX for PIN pairing). This is a headless/decode-to-file reference, not a desktop viewer.


8. Troubleshooting (grounded in the repo's real gotchas)

  • Gamepads don't appear / permission denied on /dev/uinput or /dev/uhid. Either you haven't joined the input group, or you haven't re-logged-in since. On Bazzite join it with ujust add-user-to-input-group (a plain sudo usermod -aG input doesn't stick on an atomic OS — see section 3), then log out and back in (or reboot): group membership only takes effect on a new session. The host log makes this unambiguous — it prints virtual gamepad created / virtual DualSense created on success, or … creation failed — controller input disabled when the device node isn't writable. (scripts/60-punktfunk.rules, packaging/README.md.)

  • No video / NVENC fails to encode. RPM Fusion's ffmpeg-libs (with NVENC) is missing — it's a weak dependency, so the package installed without it. Re-run the RPM Fusion step in section 1. (packaging/rpm/punktfunk.spec: Recommends: ffmpeg-libs.)

  • gamescope session won't come up / capture deadlocks. punktfunk needs gamescope ≥ 3.16.22 — older versions (e.g. the broken 3.16.20 some bases shipped) deadlock on PipeWire ≥ 1.6, and a wedged capture link can head-block the whole PipeWire daemon system-wide. Check with gamescope --version. Bazzite tracks recent gamescope, but verify if you hit hangs. (Project notes.)

  • NVENC/EGL silently stops working after a system update. punktfunk's reference box uses the NVIDIA open kernel module, and a kernel update can silently drop it. On Bazzite the NVIDIA stack is image-managed (bazzite-nvidia), so this is far less likely — but if NVENC dies right after an rpm-ostree/bootc update, confirm the NVIDIA driver still loads (nvidia-smi) before blaming punktfunk.

  • PUNKTFUNK_ZEROCOPY=1 but it falls back to CPU. The zero-copy path needs working EGL/CUDA from the NVIDIA driver. The code falls back to CPU automatically; check the log for the fallback line and verify the -nvidia image / driver is healthy.

  • Wrong UID in host.env. XDG_RUNTIME_DIR=/run/user/1000 and the bus path assume UID 1000. Run id -u; if it's different, fix both lines or the host can't reach your session's PipeWire/D-Bus.

  • Service ExecStart points at a missing path in $HOME. The dev unit references %h/punktfunk/target/release/.... The RPM binary is /usr/bin/punktfunk-host. Override ExecStart=/usr/bin/punktfunk-host serve --gamestream (or bare serve for native-only) if needed (section 5).

  • Moonlight can't see the host. Ensure UDP 5353 (mDNS) and the GameStream ports are open (section 6) and client + host are on the same L2 LAN segment.


Appendix — if the COPR isn't published yet

The COPR (enricobuehler/punktfunk) is operator-run and may not be live. If rpm-ostree install punktfunk can't find the package, build the RPM yourself on a Fedora machine/toolbox (not Debian/Ubuntu — the host links system FFmpeg/PipeWire and won't build there), per packaging/README.md:

git archive --format=tar.gz --prefix=punktfunk-0.3.0/ \
  -o ~/rpmbuild/SOURCES/punktfunk-0.3.0.tar.gz HEAD    # 0.3.0 = the spec's default version
rpmbuild -ba packaging/rpm/punktfunk.spec    # needs the spec's BuildRequires + RPM Fusion

To publish the COPR for others (so rpm-ostree install punktfunk / the bootc image work), follow packaging/copr/README.md — create the project, point build-from-SCM at the repo with spec path packaging/rpm/punktfunk.spec, add RPM Fusion nonfree as an external repo, and select chroots matching your Bazzite Fedora base (rpm -E %fedora).


Accuracy flags

  1. The COPR is operator-run / not assumed published — both install paths depend on it.
  2. There is no firewall script/doc in the repo — the ports above are derived from the code.
  3. The bundled systemd unit runs serve --gamestream — the native punktfunk/1 QUIC plane (always on) plus the GameStream/Moonlight planes. Drop --gamestream for a secure native-only host; punktfunk1-host is a separate standalone native host, unmanaged by the unit.
  4. The mgmt port (47990) is loopback-only by default — don't open it.