main-desktop

Custom Bazzite DX image for my desktop, built with BlueBuild, published to the container registry on my Gitea instance (git.lazypugs.com) by Gitea Actions, and signed with cosign.

  • Base: ghcr.io/ublue-os/bazzite-dx-nvidia — Bazzite DX, KDE Plasma, NVIDIA open kernel modules. There is no separate -open DX image anymore: DX NVIDIA is open-driver-only, which is exactly what the RTX 5090 (Blackwell) requires. Current stable ships driver 610.x (open).
  • Pinned to Fedora 44 via image-version: stable-44 in recipes/recipe.yml. No surprise major upgrades; bump to stable-45 deliberately.
  • This image layers almost nothing. The base already includes Steam and the gaming stack, Flatpak, Docker CE + CLI, Podman, VS Code, Homebrew, and nvidia-container-toolkit with automatic CDI setup (ublue-nvctk-cdi.service regenerates /etc/cdi/nvidia.yaml each boot). What this recipe adds: the cosign signature-verification policy and a declarative first-boot Flatpak set. That's it — by design.

Why BlueBuild instead of the raw Containerfile template: with the container toolkit already in-image, the whole custom layer is "install these Flatpaks on first boot + trust my signature", which the default-flatpaks and signing modules express declaratively in ~40 lines of YAML — no hand-rolled first-boot systemd unit needed.

CI setup (Gitea Actions)

The workflow lives at .gitea/workflows/build.yml and runs the BlueBuild CLI directly (the blue-build/github-action is GitHub-only). It builds on push, manual dispatch, and daily at 06:00 UTC so base-image updates flow through automatically — that schedule is what makes the image self-updating.

Repo secrets (already configured):

  • SIGNING_SECRET — contents of cosign.key (generated with an empty password; the public half is committed as cosign.pub). The CLI signs automatically on push when both are present.
  • REGISTRY_TOKEN — a Gitea personal access token with write:package scope, used to push to the registry. Gitea's automatic per-job token cannot publish packages (documented Gitea limitation), hence the PAT. Currently seeded with a temporary token — before revoking it, create a durable PAT (Settings → Applications → Generate token, write:package) and update the secret (repo Settings → Actions → Secrets, or PUT /api/v1/repos/ckoch/main-desktop/actions/secrets/REGISTRY_TOKEN).

Runner requirements (act_runner on the instance):

  • Privileged job containers must be allowed — the build runs buildah inside the job container (ghcr.io/blue-build/cli). In the runner's config.yaml: container: { privileged: true }. Without it the build fails at the buildah stage.
  • Internet access (pulls the multi-GB Bazzite base from ghcr.io) and ~25+ GB free scratch disk.

Known first-run watch-items (verified docs, but no public precedent for this exact stack): cosign signature push to a Gitea registry and the CLI's tag generation under act_runner's GitHub-compat env are both expected to work but unproven in the wild — if the first run fails at signing or tagging, that's where to look, not the recipe.

Rebasing the desktop onto this image

From the existing Bazzite install, rebase in two steps — first unsigned (this installs the image, which contains the signing policy and public key), then signed:

# Step 1: unsigned rebase, then reboot
rpm-ostree rebase ostree-unverified-registry:git.lazypugs.com/ckoch/main-desktop:latest
systemctl reboot

# Step 2: switch to the signed ref, then reboot
rpm-ostree rebase ostree-image-signed:docker://git.lazypugs.com/ckoch/main-desktop:latest
systemctl reboot

After step 2, every update is signature-verified. :latest is fine here — the Fedora pin lives in the recipe, so latest never crosses a major until the recipe says so.

Roll back anytime with rpm-ostree rollback (or pick the previous deployment in the boot menu).

Notes for a self-hosted registry:

  • The repo/package is public, so anonymous pulls work and the desktop needs no auth config. If you ever make it private, the host needs credentials in /etc/ostree/auth.json (containers-auth.json format) — podman login alone does not cover OS updates.
  • Updates only flow while git.lazypugs.com is reachable; if the box is down, updates pause and retry later. The running system is unaffected.

Install-time checklist

  1. Driver sanity: nvidia-smi reports driver ≥ 580 (currently 610.x) and the RTX 5090 is listed. Confirm open kernel modules: modinfo -F license nvidia should say Dual MIT/GPL (the proprietary module says NVIDIA). Blackwell only works on the open modules.

  2. Signature: verify the published image against the committed public key:

    cosign verify --key cosign.pub git.lazypugs.com/ckoch/main-desktop:latest
    
  3. Deployment: rpm-ostree status shows the booted deployment is ostree-image-signed:docker://git.lazypugs.com/ckoch/main-desktop and the base version matches a recent build.

  4. Flatpaks: the first-boot service installs the whole list below; give it a few minutes on first login (it notifies when done). Check with flatpak list --system.

  5. Docker group: run ujust dx-group (adds you to docker and friends), then log out/in.

  6. GPU in containers: see the section below — run the one-liner test.

GPU in containers (CUDA on the 5090)

nvidia-container-toolkit is already in the image and a boot service generates the CDI spec automatically. CDI is the shipped mechanism:

# Podman — works out of the box:
podman run --rm --device nvidia.com/gpu=all docker.io/nvidia/cuda:12.8.0-base-ubuntu24.04 nvidia-smi

# Docker — CDI device syntax also works on current Docker CE:
docker run --rm --device nvidia.com/gpu=all nvidia/cuda:12.8.0-base-ubuntu24.04 nvidia-smi

If you want the classic docker run --gpus all syntax, that needs a one-time runtime hookup (this writes to /etc/docker/daemon.json, which is mutable and survives updates):

sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

First-boot Flatpaks

Installed system-wide from Flathub by the default-flatpaks module. All IDs verified against Flathub (2026-07).

Official / verified listings: Bambu Studio, Bitwarden, Discord, Firefox, Thunderbird, FreeCAD, GIMP, HandBrake, LibreOffice, Moonlight, PCSX2 (net.pcsx2.PCSX2 — the official build), Podman Desktop, Warzone 2100, XIVLauncher (dev.goats.xivlauncher).

Community / unverified listings — know what you're running:

App ID Note
GitHub Desktop io.github.shiftey.Desktop Community Linux fork; the "shiftey" spelling is the real ID
MakeMKV com.makemkv.MakeMKV Community packaging of the proprietary app
GeForce NOW io.github.hmlendea.geforcenow-electron Unofficial Electron wrapper, not an NVIDIA product
VLC org.videolan.VLC No verified badge on Flathub (still the VideoLAN-maintained build)
Godot org.godotengine.Godot No verified badge on Flathub

GeForce NOW on the 32:9 monitor: the service only streams 16:9 (and some 21:9) aspect ratios, so expect pillarboxing at 5120x1440 — that's a service limitation, not a config problem.

Post-install steps (manual by design — don't automate these)

PIA VPN — native WireGuard, not the app

The PIA desktop app installs into /usr and breaks on the read-only filesystem. Skip it:

  1. Log in at PIA's OpenVPN config generator (or generate a WireGuard config via their API/support flow) and download configs for your preferred regions.
  2. KDE System Settings → Network → Connections → Add (+) → Import VPN connection…, pick the downloaded file, and enter your PIA credentials.
  3. This is plain NetworkManager — it lives in /etc, survives every update, and gets a connect/disconnect toggle in the system tray.

Microsoft Teams — PWA via Edge

No native Linux client. Install Microsoft Edge specifically (corporate conditional access / Intune compatibility): flatpak install flathub com.microsoft.Edge, sign in to https://teams.microsoft.com, then menu → Apps → Install this site as an app.

Claude desktop — PWA

No official Linux app. Open https://claude.ai in Edge (or any Chromium browser) and use Install this site as an app the same way.

Proton-GE

Already handled by Bazzite: use the preinstalled ProtonUp-Qt GUI (or browse ujust — run ujust with no arguments to list recipes). Don't install anything extra.

Node / toolchains — not in the image

Node, npm, etc. are deliberately not layered. Use per-project versions instead:

brew install mise         # Homebrew ships in the base image
mise use node@22          # per-project .mise.toml; `mise use -g node@22` for a global default

(Plain brew install node also works if you just want one global Node.)

Display note

The 49" 32:9 (5120x1440) ultrawide needs nothing at the image layer. Post-install: set scale and refresh rate in KDE System Settings → Display; KDE's window tiling (Meta+drag, or a tiler like Polonium) is worth setting up at this width. VMs are remote via Remmina — there is intentionally no local hypervisor tooling in this image.

Maintenance

  • Change the Flatpak list / packages: edit recipes/recipe.yml, push, and the next update picks it up. (Removing an app from the list does not uninstall it from the machine; flatpak uninstall it once by hand.)
  • Jump Fedora majors: change image-version: stable-44stable-45 in the recipe when ready, push, then update normally.
  • Check build status: the repo's Actions tab; builds also run nightly, so a broken base shows up there before it reaches the machine.
  • Key hygiene: cosign.key is git-ignored and lives only on the workstation + in the SIGNING_SECRET secret — keep a backup (password manager). Losing it means generating a new pair, updating the secret, and re-doing the two-step rebase to re-establish trust.
S
Description
Custom Bazzite DX NVIDIA (open) bootc image — BlueBuild, signed
Readme
78 KiB
Languages
Shell 100%