Bake Sunshine remote admin + add one-command configured ISO builder

- Sunshine flatpak in first-boot list; autostarts with the console
  account's session (bc250-sunshine-autostart, honors CONSOLE_KID_USER)
- bc250-personalize: one-shot display names from /etc/bc250-console.conf
- build-console.sh + console.env(.example): fill in usernames/passwords/
  display names, get a fully personalized ISO — no post-install steps
  beyond per-account logins. Generated files git-ignored (credentials)
- Docs: Moonlight is the remote-admin path; RDP marked blocked by the
  mutter cursor-metadata screencast crash on GFX1013

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 10:51:57 -04:00
co-authored by Claude Fable 5
parent c2d0f7967a
commit 6194756d2a
12 changed files with 222 additions and 7 deletions
+5
View File
@@ -3,3 +3,8 @@ __pycache__/
release-preview/
/.bluebuild-scripts_*
/Containerfile
# build-console.sh generated files (contain credentials/machine-specific)
/console.env
/iso-config.generated.toml
/files/console/etc/bc250-console.conf
+15
View File
@@ -1,5 +1,20 @@
# BC-250 Console — local build → ISO
## Quick path: build-console.sh
```bash
cp console.env.example console.env # fill in usernames/passwords/display names
./build-console.sh # → iso-output/bootiso/bc250-console-gnome.iso
```
The script runs the whole verified pipeline below and bakes your accounts
(names, passwords, display names) and the console config into the ISO —
a fresh install needs no post-install setup beyond per-account logins
(Steam + Family View, Moonlight pairing, `sudo tailscale up`).
`console.env` is git-ignored: it contains real passwords, never commit it.
Everything below documents the manual pipeline the script automates.
Custom recipe: `recipes/bc250-console-gnome.yml` (Bazzite GNOME + cyan-skillfish
governor + SSH/RDP remote admin + BC-250 ttm kargs). Build path is **local**
(no CI, no registry push, no cosign needed).
+10 -1
View File
@@ -13,7 +13,16 @@ kid's password any time: `sudo passwd kid`.
| | Command / client | Works when |
|---|---|---|
| SSH (terminal) | `ssh parent@<console-address>` | Always (console is never asleep) |
| RDP (see the screen) | Any RDP client → `<console-address>:3389` | Only while the kid's session is logged in |
| Moonlight (see + control the screen) | [Moonlight](https://moonlight-stream.org) → console → **Desktop** | Only while the kid's session is logged in |
One-time Moonlight pairing: browse to `https://<console-address>:47990`,
create the Sunshine admin login, then enter the PIN Moonlight shows you in
Sunshine's PIN tab. Sunshine starts automatically with the kid's session.
> Why not RDP? GNOME Remote Desktop crashes the session on this GPU
> (mutter 50.x cursor-metadata screencast segfault on cyan-skillfish).
> `bc250-remote-setup` still configures it if you want to retry after a
> future mutter update — but Moonlight is smoother anyway.
`<console-address>`: the LAN IP, or (recommended) the Tailscale name/IP so it
works from anywhere. Never port-forward 22/3389 on the router.
+6 -4
View File
@@ -15,7 +15,7 @@ over-the-air from this Gitea's container registry.
|---|---|
| GPU | `cyan-skillfish-governor-smu` (BC-250 DVFS governor, from the [filippor COPR](https://copr.fedorainfracloud.org/coprs/filippor/bazzite/)), enabled |
| Memory | `ttm.pages_limit` / `ttm.page_pool_size` kargs so the GPU can use the full 16 GB shared GDDR6 |
| Remote admin | `openssh-server` (enabled) + `gnome-remote-desktop` (RDP); one-time `bc250-remote-setup` script; `tailscaled` enabled |
| Remote admin | `openssh-server` (enabled); **Sunshine** auto-serves the kid's session to Moonlight; `tailscaled` enabled. (RDP via `bc250-remote-setup` exists but is blocked by a mutter/GFX1013 crash) |
| Console behavior | All suspend paths masked, no screen lock/dim, GNOME animations off — always on, always reachable |
| Kid experience | Steam Big Picture autostarts for the `kid` account only |
| Apps | First-boot flatpaks: ProtonUp-Qt, Heroic, RetroDECK, Prism Launcher (Minecraft) — Steam, Firefox, auto-updates already in the Bazzite base |
@@ -25,9 +25,11 @@ over-the-air from this Gitea's container registry.
## Accounts
The ISO installs **unattended** and bakes two users from
[`iso-config.toml`](iso-config.toml): `parent` (admin/wheel) and `kid`
(standard), both with a placeholder password to change on first login.
Build with [`build-console.sh`](build-console.sh) + your `console.env` to
bake real usernames, passwords, and display names directly into the ISO
(see BUILD-CONSOLE.md). Without it, the ISO bakes the placeholder users
from [`iso-config.toml`](iso-config.toml): `parent` (admin/wheel) and `kid`
(standard), passwords to change on first login.
The kid's account can be renamed post-install (see OPERATIONS.md) — the
Big Picture autostart follows `CONSOLE_KID_USER` in `/etc/bc250-console.conf`.
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
# One-command console ISO builder.
#
# cp console.env.example console.env # fill in names/passwords
# ./build-console.sh
#
# Produces iso-output/bootiso/bc250-console-gnome.iso with the accounts,
# display names, and console behavior fully baked — no post-install steps
# beyond per-account logins (Steam, Sunshine pairing, tailscale up).
#
# Pipeline (verified on Ubuntu 24.04 + Docker Desktop + native dockerd):
# BlueBuild generate -> docker build -> throwaway registry -> skopeo into
# a containers-storage volume -> bootc-image-builder ISO (native daemon).
set -euo pipefail
DIR="$(cd "$(dirname "$0")" && pwd)"
OUT="$(dirname "$DIR")/iso-output"
NATIVE="docker -H unix:///var/run/docker.sock"
cd "$DIR"
# --------------------------------------------------------------- config ----
[ -f console.env ] || {
cp console.env.example console.env
echo "Created console.env — fill in your names/passwords, then re-run."
exit 1
}
# shellcheck disable=SC1091
source ./console.env
for v in KID_USER KID_PASSWORD PARENT_USER PARENT_PASSWORD; do
[ -n "${!v:-}" ] || { echo "ERROR: $v is empty in console.env"; exit 1; }
done
case "$KID_PASSWORD$PARENT_PASSWORD" in *'"'*|*'\'*)
echo 'ERROR: passwords must not contain " or \'; exit 1;;
esac
echo ">> Building for kid='$KID_USER' parent='$PARENT_USER'"
cat > iso-config.generated.toml <<EOF
# GENERATED by build-console.sh from console.env — git-ignored, do not commit.
[[customizations.user]]
name = "$PARENT_USER"
password = "$PARENT_PASSWORD"
groups = ["wheel"]
[[customizations.user]]
name = "$KID_USER"
password = "$KID_PASSWORD"
EOF
mkdir -p files/console/etc
cat > files/console/etc/bc250-console.conf <<EOF
# GENERATED by build-console.sh — which accounts get the console treatment.
CONSOLE_KID_USER=$KID_USER
CONSOLE_KID_DISPLAY="${KID_DISPLAY:-}"
CONSOLE_PARENT_USER=$PARENT_USER
CONSOLE_PARENT_DISPLAY="${PARENT_DISPLAY:-}"
EOF
# ---------------------------------------------------------------- image ----
if ! docker info >/dev/null 2>&1; then
systemctl --user start docker-desktop 2>/dev/null || true
for _ in $(seq 1 24); do docker info >/dev/null 2>&1 && break; sleep 5; done
docker info >/dev/null 2>&1 || { echo "ERROR: docker daemon unavailable"; exit 1; }
fi
echo ">> Generating Containerfile"
docker run --rm -v "$PWD":/bluebuild -w /bluebuild ghcr.io/blue-build/cli:latest \
bluebuild generate -o Containerfile ./recipes/bc250-console-gnome.yml
echo ">> Building image"
docker buildx build -f Containerfile -t localhost/bc250-console-gnome:latest .
# ------------------------------------------ transfer to containers-storage -
echo ">> Staging image for bootc-image-builder"
docker network create bibnet 2>/dev/null || true
docker rm -f bc250-registry >/dev/null 2>&1 || true
docker run -d --name bc250-registry --network bibnet -p 127.0.0.1:5000:5000 registry:2 >/dev/null
docker tag localhost/bc250-console-gnome:latest localhost:5000/bc250-console-gnome:latest
docker push -q localhost:5000/bc250-console-gnome:latest
$NATIVE volume create bib-storage2 >/dev/null
$NATIVE run --rm --privileged --network host \
-v bib-storage2:/var/lib/containers/storage \
quay.io/skopeo/stable:latest copy --src-tls-verify=false \
docker://127.0.0.1:5000/bc250-console-gnome:latest \
containers-storage:localhost/bc250-console-gnome:latest
# terra repos reference GPG keys by file:// path; bib's depsolver needs them
if [ ! -d "$OUT/../rpm-gpg-keys" ]; then
echo ">> Extracting RPM GPG keys from image"
cid=$(docker create localhost/bc250-console-gnome:latest true)
docker cp "$cid":/etc/pki/rpm-gpg "$OUT/../rpm-gpg-keys"
docker rm "$cid" >/dev/null
fi
# ------------------------------------------------------------------ ISO ----
echo ">> Building ISO (this is the slow part)"
mkdir -p "$OUT"
$NATIVE run --rm --privileged --security-opt label=type:unconfined_t \
-v "$OUT":/output \
-v "$OUT/../rpm-gpg-keys":/etc/pki/rpm-gpg:ro \
-v "$PWD/iso-config.generated.toml":/config.toml:ro \
-v bib-storage2:/var/lib/containers/storage \
quay.io/centos-bootc/bootc-image-builder:latest \
--type iso --rootfs btrfs \
localhost/bc250-console-gnome:latest
$NATIVE run --rm -v "$OUT":/o alpine:latest chown -R "$(id -u):$(id -g)" /o
mv "$OUT/bootiso/install.iso" "$OUT/bootiso/bc250-console-gnome.iso"
( cd "$OUT/bootiso" && sha256sum bc250-console-gnome.iso > bc250-console-gnome.iso.sha256 )
docker rm -f bc250-registry >/dev/null
echo
echo ">> DONE: $OUT/bootiso/bc250-console-gnome.iso"
echo ">> Flash it, or publish the image for OTA updates:"
echo ">> docker login git.lazypugs.com && docker tag localhost/bc250-console-gnome:latest git.lazypugs.com/ckoch/bc250-console-gnome:latest && docker push git.lazypugs.com/ckoch/bc250-console-gnome:latest"
+12
View File
@@ -0,0 +1,12 @@
# Copy to console.env, fill in, then run ./build-console.sh
# console.env is git-ignored — it holds real passwords. NEVER commit it.
#
# Passwords: avoid double quotes and backslashes in them.
KID_USER=kid
KID_DISPLAY="Kid"
KID_PASSWORD=changeme
PARENT_USER=parent
PARENT_DISPLAY="Parent"
PARENT_PASSWORD=changeme
@@ -0,0 +1,7 @@
[Desktop Entry]
Type=Application
Name=Sunshine streaming host (console autostart)
Comment=Serves the console session to Moonlight for remote admin
Exec=/usr/bin/bc250-sunshine-autostart
X-GNOME-Autostart-enabled=true
NoDisplay=true
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# One-time personalization from /etc/bc250-console.conf (written at image
# build time by build-console.sh): applies friendly display names to the
# accounts created by the installer. Stamps so it never re-runs — later
# on-box renames/chfn are never overwritten.
[ -r /etc/bc250-console.conf ] && . /etc/bc250-console.conf
apply() { # $1 = user, $2 = display name
[ -n "$2" ] || return 0
id "$1" >/dev/null 2>&1 || return 0
chfn -f "$2" "$1" || true
}
apply "${CONSOLE_KID_USER:-kid}" "${CONSOLE_KID_DISPLAY:-}"
apply "${CONSOLE_PARENT_USER:-parent}" "${CONSOLE_PARENT_DISPLAY:-}"
mkdir -p /var/lib/bc250
touch /var/lib/bc250/.personalized
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Start the Sunshine streaming host in the console (kid) account's session,
# so the parent can connect with Moonlight whenever the console is in use.
# Sunshine replaces RDP as the remote-admin path: GNOME RDP crashes mutter
# on the BC-250 (cursor-metadata screencast segfault, mutter 50.3).
[ -r /etc/bc250-console.conf ] && . /etc/bc250-console.conf
[ "$(id -un)" = "${CONSOLE_KID_USER:-kid}" ] || exit 0
flatpak info dev.lizardbyte.app.Sunshine >/dev/null 2>&1 || exit 0
exec systemctl --user enable --now app-dev.lizardbyte.app.Sunshine.service
@@ -0,0 +1,10 @@
[Unit]
Description=One-time console account personalization (display names)
ConditionPathExists=!/var/lib/bc250/.personalized
[Service]
Type=oneshot
ExecStart=/usr/bin/bc250-personalize
[Install]
WantedBy=multi-user.target
@@ -13,7 +13,16 @@ kid's password any time: `sudo passwd kid`.
| | Command / client | Works when |
|---|---|---|
| SSH (terminal) | `ssh parent@<console-address>` | Always (console is never asleep) |
| RDP (see the screen) | Any RDP client → `<console-address>:3389` | Only while the kid's session is logged in |
| Moonlight (see + control the screen) | [Moonlight](https://moonlight-stream.org) → console → **Desktop** | Only while the kid's session is logged in |
One-time Moonlight pairing: browse to `https://<console-address>:47990`,
create the Sunshine admin login, then enter the PIN Moonlight shows you in
Sunshine's PIN tab. Sunshine starts automatically with the kid's session.
> Why not RDP? GNOME Remote Desktop crashes the session on this GPU
> (mutter 50.x cursor-metadata screencast segfault on cyan-skillfish).
> `bc250-remote-setup` still configures it if you want to retry after a
> future mutter update — but Moonlight is smoother anyway.
`<console-address>`: the LAN IP, or (recommended) the Tailscale name/IP so it
works from anywhere. Never port-forward 22/3389 on the router.
+3 -1
View File
@@ -24,7 +24,7 @@ modules:
- type: script
snippets:
- chmod 0755 /usr/bin/bc250-remote-setup /usr/bin/bc250-steam-bpm-autostart /usr/bin/bc250-gfn-install
- chmod 0755 /usr/bin/bc250-remote-setup /usr/bin/bc250-steam-bpm-autostart /usr/bin/bc250-gfn-install /usr/bin/bc250-sunshine-autostart /usr/bin/bc250-personalize
- dconf update # compile the local.d/gdm.d console defaults into the dconf dbs
- type: default-flatpaks@v1
@@ -39,6 +39,7 @@ modules:
- com.heroicgameslauncher.hgl # Heroic: Epic + GOG
- net.retrodeck.retrodeck # RetroDECK: retro emulation
- org.prismlauncher.PrismLauncher # Minecraft (Java; sign in with Microsoft account)
- dev.lizardbyte.app.Sunshine # game-stream host: parent remote admin via Moonlight (RDP blocked by mutter bug on GFX1013)
- type: systemd
system:
@@ -47,6 +48,7 @@ modules:
- sshd.service
- tailscaled.service # idle until 'sudo tailscale up'
- bc250-gfn-install.service # GeForce NOW beta from NVIDIA's flatpak repo (first boot)
- bc250-personalize.service # one-time display names from /etc/bc250-console.conf
masked:
- hhd.service # Handheld Daemon crash-loops on BC-250
# Always-on console: hard-block every suspend path (the box must