Move to Ubuntu 26.04 LTS: backup, setup and restore scripts

Replaces the Bazzite/BlueBuild image with plain Ubuntu. apt covers the system
and dev tools, Flathub the desktop apps, and three scripts do the rest:
backup.sh archives this machine's home to server-marvin and reads it back to
verify it; setup.sh builds a fresh 26.04 install (NVIDIA 595-open, Docker with
GPU access, Steam/Lutris, 27 Flatpaks, VS Code, Claude desktop, the Data drive
and marvin mounts); restore.sh unpacks the backup without the Fedora/KDE parts
that would fight Ubuntu.

The app list comes from a scan of the running machine. The 15 Flatpaks left
behind sit commented at the bottom of flatpaks.txt, and their data still rides
along in the backup. No Node or .NET on the host by choice — LudosData and
landingPage build in Docker.

Drops the BlueBuild recipe, the cosign key and the old dev-setup script. The
Bazzite setup stays in this history.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 15:48:47 -04:00
co-authored by Claude Opus 5
parent cf3fc8bb79
commit 1ddc56800a
10 changed files with 638 additions and 370 deletions
+226
View File
@@ -0,0 +1,226 @@
#!/usr/bin/env bash
# Fresh Ubuntu 26.04 LTS -> my desktop, in one pass. Run as your user; it sudos as needed.
# Safe to re-run: each step checks before it changes anything. Reboot when it finishes,
# then run scripts/restore.sh.
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# Second NVMe (970 EVO Plus, label "Data"): SteamLibrary + emulator files. Never formatted.
DATA_UUID=29db4bd3-b743-417d-aa39-4da4acc9df3f
DATA_MNT=/mnt/data
# server-marvin SMB share — the backup lives here. \040 is fstab's escaped space.
MARVIN_SHARE='//192.168.1.193/Personal\040Files\040-\040ckoch'
MARVIN_MNT=/mnt/server-marvin-personal
MARVIN_CREDS=/etc/samba/credentials-marvin
step() { printf '\n\033[1;34m== %s ==\033[0m\n' "$*"; }
warn() { printf '\033[1;33mwarning:\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
apt_install() { sudo DEBIAN_FRONTEND=noninteractive apt-get install -y "$@"; }
[[ $EUID -ne 0 ]] || die "run as your user, not with sudo"
. /etc/os-release
[[ $ID == ubuntu ]] || die "expected Ubuntu, found $ID"
[[ $VERSION_ID == 26.04 ]] || warn "written for 26.04; this is $VERSION_ID — carrying on"
CODENAME=${UBUNTU_CODENAME:-$VERSION_CODENAME}
# Ask for the password once and keep sudo alive for the whole run (Flatpaks take a while).
sudo -v
while true; do sudo -n true; sleep 50; kill -0 "$$" 2>/dev/null || exit; done 2>/dev/null &
keepalive=$!
trap 'kill "$keepalive" 2>/dev/null' EXIT
# ---------------------------------------------------------------------------------------
step "Apt repositories"
sudo apt-get update
apt_install curl gpg ca-certificates software-properties-common
for c in universe multiverse restricted; do sudo add-apt-repository -y -n "$c"; done
sudo dpkg --add-architecture i386 # Steam + 32-bit game libraries
sudo install -d -m 0755 /etc/apt/keyrings
# Docker CE — Docker's own repo, not the snap or Ubuntu's docker.io
if [[ ! -f /etc/apt/sources.list.d/docker.sources ]]; then
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $CODENAME
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF
fi
# NVIDIA Container Toolkit — GPU access inside Docker/Podman
if [[ ! -f /etc/apt/sources.list.d/nvidia-container-toolkit.list ]]; then
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
| sudo gpg --dearmor --yes -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -fsSL https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
| sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \
| sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list >/dev/null
fi
# VS Code — Microsoft's repo (was rpm-ostree-layered on Bazzite)
if [[ ! -f /etc/apt/sources.list.d/vscode.sources ]]; then
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \
| sudo gpg --dearmor --yes -o /usr/share/keyrings/microsoft.gpg
sudo tee /etc/apt/sources.list.d/vscode.sources >/dev/null <<EOF
Types: deb
URIs: https://packages.microsoft.com/repos/code
Suites: stable
Components: main
Architectures: amd64
Signed-By: /usr/share/keyrings/microsoft.gpg
EOF
fi
# GitHub CLI
if [[ ! -f /etc/apt/sources.list.d/github-cli.list ]]; then
sudo curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
-o /etc/apt/keyrings/githubcli-archive-keyring.gpg
sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
| sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null
fi
# Claude desktop — Anthropic's repo (ran inside an Ubuntu distrobox on Bazzite)
if [[ ! -f /etc/apt/sources.list.d/claude-desktop.list ]]; then
key=/usr/share/keyrings/claude-desktop-archive-keyring.asc
sudo curl -fsSLo "$key" https://downloads.claude.ai/claude-desktop/key.asc
if ! gpg --show-keys --with-colons "$key" 2>/dev/null \
| grep -q '^fpr:::::::::31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE:'; then
sudo rm -f "$key"; die "Claude desktop signing key has the wrong fingerprint — not adding the repo"
fi
echo "deb [arch=amd64,arm64 signed-by=$key] https://downloads.claude.ai/claude-desktop/apt/stable stable main" \
| sudo tee /etc/apt/sources.list.d/claude-desktop.list >/dev/null
fi
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get full-upgrade -y
# ---------------------------------------------------------------------------------------
step "Packages"
# steam-installer refuses to install non-interactively until its license is accepted.
echo 'steam-installer steam/question select I AGREE' | sudo debconf-set-selections
echo 'steam-installer steam/license note ' | sudo debconf-set-selections
apt_install \
build-essential cmake pkg-config git git-lfs wget jq zstd unzip p7zip-full rsync htop \
python3-venv python3-pip pipx ffmpeg cifs-utils flatpak \
steam-installer steam-devices gamemode mangohud lutris vulkan-tools \
docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \
nvidia-container-toolkit \
code claude-desktop
# ---------------------------------------------------------------------------------------
step "NVIDIA driver (open kernel modules — the only kind the RTX 5090 supports)"
apt_install ubuntu-drivers-common
installed_driver() {
{ dpkg-query -W -f='${Status} ${Package}\n' 'nvidia-driver-*' 2>/dev/null || true; } \
| awk '/^install ok installed / {print $4}' | sort -V | tail -1
}
if [[ $(installed_driver) != *-open ]]; then
ubuntu-drivers devices 2>/dev/null | grep -E 'model|driver' || true
# Picks Ubuntu's recommended branch plus its prebuilt, signed kernel modules (no DKMS).
sudo ubuntu-drivers install
fi
driver=$(installed_driver)
[[ $driver == nvidia-driver-*-open ]] \
|| die "got '${driver:-no driver}', but the 5090 needs an -open driver — check 'ubuntu-drivers devices'"
branch=${driver#nvidia-driver-}; branch=${branch%-open}
apt_install "libnvidia-gl-$branch:i386" # 32-bit GL/Vulkan for Steam and Proton
echo "using $driver"
# ---------------------------------------------------------------------------------------
step "Docker, GPU containers, Claude's Cowork VM"
sudo usermod -aG docker "$USER" # docker without sudo (after reboot)
getent group kvm >/dev/null && sudo usermod -aG kvm "$USER" # Cowork needs /dev/kvm + vhost-vsock
echo vhost_vsock | sudo tee /etc/modules-load.d/vhost_vsock.conf >/dev/null
if ! grep -q nvidia /etc/docker/daemon.json 2>/dev/null; then
sudo nvidia-ctk runtime configure --runtime=docker # enables `docker run --gpus all`
sudo systemctl restart docker
fi
# ---------------------------------------------------------------------------------------
step "Flatpak apps"
sudo flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
mapfile -t apps < <(sed -e 's/#.*//' -e 's/[[:space:]]//g' -e '/^$/d' "$REPO/flatpaks.txt")
sudo flatpak install -y --noninteractive --system flathub "${apps[@]}"
# Firefox's read-only peek at landingPage, as set by hand on Bazzite
flatpak override --user org.mozilla.firefox --filesystem="$HOME/Documents/git/landingPage:ro"
# Firefox/Thunderbird come from Flathub instead, so restoring ~/.var/app brings the old
# profiles back untouched. Drop Ubuntu's snaps and the debs that pull them back in.
for s in firefox thunderbird; do
if snap list "$s" >/dev/null 2>&1; then sudo snap remove --purge "$s"; fi
if dpkg -s "$s" >/dev/null 2>&1; then sudo apt-get remove -y "$s"; fi
done
xdg-settings set default-web-browser org.mozilla.firefox.desktop 2>/dev/null || true
# Keep Flatpaks updated daily, like Bazzite did. Starts next boot.
sudo tee /etc/systemd/system/flatpak-update.service >/dev/null <<'EOF'
[Unit]
Description=Update system Flatpaks
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/flatpak update --system --noninteractive --assumeyes
EOF
sudo tee /etc/systemd/system/flatpak-update.timer >/dev/null <<'EOF'
[Unit]
Description=Update system Flatpaks daily
[Timer]
OnBootSec=10min
OnUnitActiveSec=1d
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable flatpak-update.timer
# ---------------------------------------------------------------------------------------
step "Claude Code CLI"
# No Node or .NET on the host by choice — LudosData and landingPage build in Docker.
# If you want them back: sudo apt install mise dotnet-sdk-10.0 (mise needs ppa:jdxcode/mise).
command -v claude >/dev/null || [[ -x ~/.local/bin/claude ]] || curl -fsSL https://claude.ai/install.sh | bash
# ---------------------------------------------------------------------------------------
step "Data drive + server-marvin share"
sudo mkdir -p "$DATA_MNT" "$MARVIN_MNT"
if ! grep -q "$DATA_UUID" /etc/fstab; then
echo "UUID=$DATA_UUID $DATA_MNT ext4 defaults,nofail,x-gvfs-show,x-gvfs-name=Data 0 2" \
| sudo tee -a /etc/fstab >/dev/null
fi
sudo blkid -U "$DATA_UUID" >/dev/null || warn "Data drive ($DATA_UUID) not found — fstab entry is nofail, so boot is fine"
if [[ ! -f $MARVIN_CREDS ]]; then
read -rp "server-marvin SMB username: " smb_user
read -rsp "server-marvin SMB password: " smb_pass; echo
sudo install -d -m 0755 /etc/samba
printf 'username=%s\npassword=%s\n' "$smb_user" "$smb_pass" \
| sudo install -m 600 /dev/stdin "$MARVIN_CREDS"
unset smb_pass
fi
if ! grep -q "$MARVIN_MNT" /etc/fstab; then
echo "$MARVIN_SHARE $MARVIN_MNT cifs credentials=$MARVIN_CREDS,uid=$(id -u),gid=$(id -g),iocharset=utf8,vers=3.0,x-systemd.automount,_netdev,nofail 0 0" \
| sudo tee -a /etc/fstab >/dev/null
fi
sudo systemctl daemon-reload
sudo mount -a || warn "a mount failed — check 'sudo mount -a' (is server-marvin up? right password?)"
# ---------------------------------------------------------------------------------------
step "Small carry-overs from Bazzite"
# Xbox controllers over Bluetooth (Bazzite set this as a kernel argument)
echo 'options bluetooth disable_ertm=1' | sudo tee /etc/modprobe.d/bluetooth-xbox.conf >/dev/null
step "Done"
cat <<EOF
Reboot now (NVIDIA driver + docker/kvm groups take effect), then:
scripts/restore.sh
EOF