# main-desktop Custom [Bazzite DX](https://github.com/ublue-os/bazzite-dx) image for my desktop, built with [BlueBuild](https://blue-build.org), 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](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](.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: ```bash # 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: ```bash 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: ```bash # 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): ```bash 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](https://www.privateinternetaccess.com/pages/ovpn-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 , then menu → **Apps → Install this site as an app**. ### Claude desktop — PWA No official Linux app. Open 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: ```bash 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](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-44` → `stable-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.