punktfunk on Bazzite — packaging and install paths
The packager/ops view of the Bazzite (Fedora Atomic) target: which install path to ship, how each
one is built and updated, and the gotchas of layered/bootc hosts. Setting a host up — udev and
the input group, host.env, the service, the firewall, streaming the KDE desktop, verifying —
is the docs' job: follow docs.punktfunk.unom.io/docs/bazzite
(firewall specifics are on Fedora §4, same firewalld
service definitions). Nothing here duplicates those pages. For the higher-level packaging
rationale ("why not Flatpak", the build), see ../README.md.
⚠️ COPR note (Path C only). The legacy layering path's commands reference a COPR project named
enricobuehler/punktfunkthat is operator-run and may not be published (seepackaging/copr/README.md); layer from the Gitea RPM registry instead (../rpm/README.md, the repo filehttps://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.
Path A — systemd-sysext (recommended)
Install, day-2 commands, channel switching, rollback, feed-signature refusals and the major-rebase behavior are all on the docs page — that's the walkthrough to hand a user. Packager-side facts:
- CI (
.gitea/workflows/rpm.yml) builds the image per Fedora major and publishes it to the feed…/packages/unom/generic/punktfunk-sysext/f<ver>[-canary]/—SHA256SUMSplus a detached OpenPGP signature frompackages@unom.io(AF245C506F4E4763, the RPM signing key). The public key is baked intopunktfunk-sysext.sh; the script refuses a feed it can't verify (PUNKTFUNK_SYSEXT_ALLOW_UNSIGNED=1is the documented escape hatch for pre-signing feeds). - The image embeds
ID=fedora+VERSION_ID(matched through Bazzite'sID_LIKE), so after a major rebase the old image is refused instead of merging soname-broken binaries; 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.
- Install also applies what the RPM scriptlets would have (udev reload, sysctl) and seeds the two
/etcfiles a sysext can't carry (the gamescope-session drop-in, the tray autostart entry), staged under/usr/share/punktfunk/etc/.
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) publishespunktfunk+punktfunk-webon every push tomain. Packages are unsigned with GPG-signed metadata (repo_gpgcheck=1), matchingpackaging/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 mandatory —
rpm-ostree installstages 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 upgradewill not update punktfunk on its own.upgradebumps the base image and only re-resolves layered packages when the base changes. A Bazzite base can sit frozen for months (a pinned:stabletag, a paused rebase), sorpm-ostree upgradekeeps reporting "No updates available" and your layeredpunktfunkstays put even after new RPMs land in the repo. (Diagnose:rpm-ostree statusshows the baseVersion:unchanged, whilednf -q repoquery --upgrades punktfunklists 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. The punktfunk RPM
installs it, so on a layered box it's already there — no checkout needed (it's the same command the
web console's update hint prints):
sudo /usr/share/punktfunk/update-punktfunk.sh # stage; reboot when ready
sudo /usr/share/punktfunk/update-punktfunk.sh --reboot # stage + reboot now
From a repo checkout (e.g. before the first install, or to run a newer helper than the layered RPM
carries) the same script runs directly — it only shells out to rpm-ostree/rpm/systemctl:
sudo bash packaging/bazzite/update-punktfunk.sh --reboot
Channel gotcha: the re-resolve picks the highest version across every enabled
/etc/yum.repos.d/punktfunk*.repo. Ifpunktfunk-canary.repois enabled alongside the stablepunktfunk.repo, canary's<next-minor>.0-0.ciNoutranks the stableX.Y.Z-1and the box silently tracks canary. Enable exactly one channel — setenabled=0in the other repo file.
2. First-run setup
All of it — input/punktfunk groups, host.env, enabling the service, the firewall, KDE
desktop mode, verifying the first stream — lives on the
docs page and applies identically to all three
install paths. Don't restate it here; a fact stated twice is a fact that drifts (see
"Where facts live" in CONTRIBUTING.md).
3. Gotchas
All user-facing ones live on the docs site now — the ffmpeg-libs weak dependency (NVENC fails at
runtime without RPM Fusion's build:
Troubleshooting)
and the ds_inhibit SELinux storm with DualSense-type pads
(Troubleshooting;
the dontaudit-vs-allow rationale is the header of punktfunk-ds-inhibit.cil).
One packager-only note: scripts/punktfunk-host.service (the upstream/dev unit) assumes the binary
at %h/punktfunk/target/release/punktfunk-host; the packaged binary is /usr/bin/punktfunk-host.
If systemctl --user cat punktfunk-host shows ExecStart pointing into a home dir, drop an override
(systemctl --user edit punktfunk-host) setting ExecStart=/usr/bin/punktfunk-host serve.
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).