Custom Bazzite DX NVIDIA (open) image: BlueBuild recipe, GHCR workflow, cosign setup
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
# 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 GHCR by GitHub 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.
|
||||
|
||||
## One-time setup (before first push)
|
||||
|
||||
1. Create a GitHub repo and push this directory to it (`main` branch).
|
||||
2. Generate the signing keypair in the repo root (**press Enter for an empty password** — the
|
||||
build action cannot use an encrypted key):
|
||||
|
||||
```bash
|
||||
cosign generate-key-pair
|
||||
```
|
||||
|
||||
3. Store the private key as an Actions secret and commit only the public half:
|
||||
|
||||
```bash
|
||||
gh secret set SIGNING_SECRET < cosign.key
|
||||
git add cosign.pub && git commit -m "Add cosign public key" && git push
|
||||
```
|
||||
|
||||
`cosign.key` is git-ignored — never commit it.
|
||||
4. The workflow (`.github/workflows/build.yml`) now builds on every push, on manual dispatch, and
|
||||
daily at 06:00 UTC (so base-image updates flow through automatically — this is what makes the
|
||||
image self-updating). Images publish to `ghcr.io/<your-github-user>/main-desktop`.
|
||||
5. If the GHCR package ends up private, make it public in the package settings (Packages →
|
||||
main-desktop → Package settings → Change visibility), or rebasing will fail to pull.
|
||||
|
||||
## 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:ghcr.io/<your-github-user>/main-desktop:latest
|
||||
systemctl reboot
|
||||
|
||||
# Step 2: switch to the signed ref, then reboot
|
||||
rpm-ostree rebase ostree-image-signed:docker://ghcr.io/<your-github-user>/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).
|
||||
|
||||
## 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 ghcr.io/<your-github-user>/main-desktop:latest
|
||||
```
|
||||
|
||||
3. **Deployment:** `rpm-ostree status` shows the booted deployment is
|
||||
`ostree-image-signed:docker://ghcr.io/<your-github-user>/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
|
||||
<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:
|
||||
|
||||
```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 Actions tab; builds also run nightly, so a broken base shows up
|
||||
there before it reaches the machine.
|
||||
Reference in New Issue
Block a user