Fedora 44 throwaway-VM bootstrap
Idempotent setup for Fedora 44 minimal/server installs. Built from a real working setup; only proven steps are included. Focused on one thing: a gaming VM with Sunshine streaming + a browser.
Each stage is independent, has a feature flag, and is idempotent — re-running skips work already done (tracked via /var/lib/fedora-bootstrap/*.done).
Stages
- base — Update + remove cockpit/abrt/cups/etc + create user
- rpmfusion — Enable RPM Fusion free + nonfree repos
- nvidia — akmod-nvidia + 32-bit libs + nouveau blacklist (auto-skip if no Nvidia GPU)
- gaming — gamescope, Steam, labwc, PipeWire, mesa i686, fonts. Everything needed to run Steam Big Picture in a Wayland gamescope session
- sunshine — LizardByte's Sunshine streaming host RPM + user-scope systemd unit override + CSRF allowlist
- autostart — tty1 autologin → labwc → gamescope → Steam Big Picture
- firefox — Firefox from default repos
- hostname / firewall —
hostnamectl+ firewalld rules for SSH + Sunshine ports
Usage
sudo ./fedora-bootstrap.sh # all defaults
sudo VM_USER=nsure VM_IP=192.168.20.142 VM_HOSTNAME=gaming ./fedora-bootstrap.sh
CLI args:
--user NAME User to create/configure (default: SUDO_USER)
--hostname NAME Set hostname (default: current)
--ip ADDR IP for Sunshine CSRF list (default: auto-detect)
--no-nvidia --no-sunshine --no-autostart --no-firewall
Requirements
- Fedora 44, fresh minimal install (Server ISO → "Minimal Install" works)
- Root access (run with sudo)
- Network connection
- Nvidia GPU passed through to the VM (or bare metal with an Nvidia card)
What it does NOT do
- Secure Boot / MOK enrollment is interactive. The script warns at the end:
Set a one-time password, reboot, enroll at the blue MOK Manager screen. Or just disable Secure Boot in firmware.sudo mokutil --import /etc/pki/akmods/certs/public_key.der - Doesn't install games (Steam handles that).
- Doesn't pair Moonlight (one-time per client, do it from Sunshine web UI).
Re-running
Each stage writes /var/lib/fedora-bootstrap/<stage>.done. To force re-run:
sudo rm /var/lib/fedora-bootstrap/<stage>.done
sudo ./fedora-bootstrap.sh
Full reset:
sudo rm -rf /var/lib/fedora-bootstrap/
sudo ./fedora-bootstrap.sh
Logs: /var/log/fedora-bootstrap.log.
Troubleshooting
Nvidia module not yet built — Secure Boot is rejecting unsigned modules. Run the mokutil command above and enroll the key.
Sunshine inactive after reboot — Check systemctl --user status app-dev.lizardbyte.app.Sunshine.service from tty2. If enable didn't stick:
systemctl --user enable app-dev.lizardbyte.app.Sunshine.service
Steam game won't launch (Proton) — --xwayland-count 2 must be in the gamescope launch in ~/.config/labwc/autostart (the script includes this). Force Proton Experimental on the game's compatibility settings.
CSRF error in Sunshine web UI — The URL in the browser must match an entry in csrf_allowed_origins. The script adds VM_IP, hostname, hostname.local. Edit ~/.config/sunshine/sunshine.conf to add others.
The script
#!/usr/bin/env bash
set -euo pipefail
# ---------------------------------------------------------------------------
# DEFAULTS — env vars override these; CLI args override env vars
# ---------------------------------------------------------------------------
VM_USER="${VM_USER:-${SUDO_USER:-$(logname 2>/dev/null || echo gamer)}}"
VM_HOSTNAME="${VM_HOSTNAME:-$(hostname)}"
VM_IP="${VM_IP:-$(ip -4 -o addr show scope global 2>/dev/null | awk 'NR==1{print $4}' | cut -d/ -f1)}"
INSTALL_BASE_CLEANUP="${INSTALL_BASE_CLEANUP:-1}"
INSTALL_RPMFUSION="${INSTALL_RPMFUSION:-1}"
INSTALL_NVIDIA="${INSTALL_NVIDIA:-auto}"
INSTALL_GAMING="${INSTALL_GAMING:-1}"
INSTALL_SUNSHINE="${INSTALL_SUNSHINE:-1}"
INSTALL_AUTOSTART="${INSTALL_AUTOSTART:-1}"
INSTALL_FIREFOX="${INSTALL_FIREFOX:-1}"
INSTALL_FIREWALL="${INSTALL_FIREWALL:-1}"
STATE_DIR=/var/lib/fedora-bootstrap
LOG_FILE=/var/log/fedora-bootstrap.log
# ---------------------------------------------------------------------------
# CLI ARG PARSING
# ---------------------------------------------------------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
--user) VM_USER="$2"; shift 2 ;;
--hostname) VM_HOSTNAME="$2"; shift 2 ;;
--ip) VM_IP="$2"; shift 2 ;;
--no-nvidia) INSTALL_NVIDIA=0; shift ;;
--no-sunshine) INSTALL_SUNSHINE=0; shift ;;
--no-autostart) INSTALL_AUTOSTART=0; shift ;;
--no-firefox) INSTALL_FIREFOX=0; shift ;;
--no-firewall) INSTALL_FIREWALL=0; shift ;;
*) echo "Unknown arg: $1"; exit 1 ;;
esac
done
# ---------------------------------------------------------------------------
# HELPERS
# ---------------------------------------------------------------------------
[[ $EUID -eq 0 ]] || { echo "Must run as root (use sudo)."; exit 1; }
mkdir -p "$STATE_DIR"
touch "$LOG_FILE"
exec > >(tee -a "$LOG_FILE") 2>&1
log() { echo -e "\n\033[1;34m==>\033[0m \033[1m$*\033[0m"; }
warn() { echo -e "\033[1;33m[warn]\033[0m $*"; }
done_marker() { touch "$STATE_DIR/$1.done"; }
is_done() { [[ -f "$STATE_DIR/$1.done" ]]; }
ensure_user_exists() {
if ! id "$VM_USER" &>/dev/null; then
log "Creating user $VM_USER"
useradd -m -G wheel "$VM_USER"
echo "$VM_USER ALL=(ALL) NOPASSWD: ALL" > "/etc/sudoers.d/$VM_USER"
warn "Set password for $VM_USER:"
passwd "$VM_USER"
fi
usermod -aG wheel,video,render,input,audio "$VM_USER"
}
as_user() { sudo -u "$VM_USER" "$@"; }
nvidia_present() { lspci 2>/dev/null | grep -qi 'nvidia.*\(vga\|3d\)'; }
if [[ "$INSTALL_NVIDIA" == "auto" ]]; then
if nvidia_present; then INSTALL_NVIDIA=1; else INSTALL_NVIDIA=0; fi
fi
# =============================================================================
# STAGE 1: BASE SYSTEM
# =============================================================================
stage_base() {
is_done base && { log "Skipping base"; return; }
log "Stage 1: base system update + cleanup"
if ! grep -q max_parallel_downloads /etc/dnf/dnf.conf; then
cat >> /etc/dnf/dnf.conf <<EOF
max_parallel_downloads=10
fastestmirror=True
defaultyes=True
EOF
fi
dnf upgrade -y --refresh
if [[ "$INSTALL_BASE_CLEANUP" == "1" ]]; then
dnf remove -y --skip-broken \
cockpit\* abrt\* sssd\* cups\* ModemManager PackageKit\* \
dnf-plugin-system-upgrade 2>/dev/null || true
fi
ensure_user_exists
loginctl enable-linger "$VM_USER" 2>/dev/null || true
done_marker base
}
# =============================================================================
# STAGE 2: RPM FUSION (required for Nvidia drivers, Steam, codecs)
# =============================================================================
stage_rpmfusion() {
[[ "$INSTALL_RPMFUSION" == "1" ]] || return
is_done rpmfusion && { log "Skipping RPM Fusion"; return; }
log "Stage 2: RPM Fusion repos"
local fver=$(rpm -E %fedora)
dnf install -y \
"https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-${fver}.noarch.rpm" \
"https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-${fver}.noarch.rpm"
dnf upgrade -y
done_marker rpmfusion
}
# =============================================================================
# STAGE 3: NVIDIA DRIVERS
# akmod-nvidia auto-rebuilds on kernel updates. 32-bit libs needed by Proton.
# Secure Boot? You'll need to enroll the akmods key via MOK after first boot.
# =============================================================================
stage_nvidia() {
[[ "$INSTALL_NVIDIA" == "1" ]] || return
is_done nvidia && { log "Skipping Nvidia"; return; }
log "Stage 3: Nvidia drivers"
dnf install -y \
akmods kernel-devel-$(uname -r) kernel-headers \
akmod-nvidia xorg-x11-drv-nvidia-cuda \
xorg-x11-drv-nvidia-libs.i686 xorg-x11-drv-nvidia-cuda-libs.i686
cat > /etc/modprobe.d/blacklist-nouveau.conf <<EOF
blacklist nouveau
blacklist nouveaufb
options nouveau modeset=0
EOF
cat > /etc/modules-load.d/nvidia.conf <<EOF
nvidia
nvidia_drm
nvidia_modeset
nvidia_uvm
EOF
cat > /etc/modprobe.d/nvidia.conf <<EOF
options nvidia-drm modeset=1
EOF
grubby --update-kernel=ALL \
--args="rd.driver.blacklist=nouveau modprobe.blacklist=nouveau nvidia-drm.modeset=1" || true
dracut --force --regenerate-all
akmods --force --kernels "$(uname -r)" || true
local i=0
while [[ $i -lt 60 ]] && ! modinfo -F version nvidia &>/dev/null; do
sleep 2; i=$((i+1))
done
if modinfo -F version nvidia &>/dev/null; then
log "Nvidia module built: $(modinfo -F version nvidia)"
else
warn "Nvidia module not yet built — may need a reboot or MOK enrollment"
fi
done_marker nvidia
}
# =============================================================================
# STAGE 4: GAMING STACK
# gamescope + Steam + labwc (tiny wlroots compositor for proper D-Bus session)
# + xdg-desktop-portal-wlr (screen capture for Wayland) + PipeWire + 32-bit
# mesa/vulkan for Proton + bubblewrap/fuse for Steam runtime container.
# udev rule gives input group access to /dev/uinput (Sunshine controller injection).
# =============================================================================
stage_gaming() {
is_done gaming && { log "Skipping gaming stack"; return; }
log "Stage 4: gamescope + Steam + labwc"
dnf install -y \
gamescope steam mangohud gamemode \
xorg-x11-server-Xwayland \
pipewire pipewire-pulseaudio pipewire-alsa wireplumber \
pulseaudio-utils \
polkit dbus-broker dbus-daemon dbus-x11 \
libva-utils vulkan-tools \
bubblewrap fuse fuse-libs \
liberation-fonts \
labwc xdg-desktop-portal-wlr \
alsa-firmware alsa-ucm linux-firmware alsa-sof-firmware \
mesa-libGL.i686 mesa-libEGL.i686 mesa-vulkan-drivers.i686 vulkan-loader.i686
cat > /usr/lib/udev/rules.d/85-uinput.rules <<'EOF'
KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess", GROUP="input", MODE="0660"
EOF
udevadm control --reload-rules || true
udevadm trigger || true
done_marker gaming
}
# =============================================================================
# STAGE 5: SUNSHINE (streaming host for Moonlight)
# - Latest Sunshine RPM from LizardByte's GitHub releases
# - sunshine.conf with CSRF allowlist for VM_IP / hostname / hostname.local
# - user-scope systemd override wires it to graphical-session.target
# =============================================================================
stage_sunshine() {
[[ "$INSTALL_SUNSHINE" == "1" ]] || return
is_done sunshine && { log "Skipping Sunshine"; return; }
log "Stage 5: Sunshine streaming host"
if ! rpm -q Sunshine &>/dev/null; then
local url="https://github.com/LizardByte/Sunshine/releases/latest/download/sunshine-fedora-44-amd64.rpm"
curl -L -o /tmp/sunshine.rpm "$url"
dnf install -y /tmp/sunshine.rpm
rm -f /tmp/sunshine.rpm
fi
local user_home=$(getent passwd "$VM_USER" | cut -d: -f6)
mkdir -p "$user_home/.config/sunshine"
cat > "$user_home/.config/sunshine/sunshine.conf" <<EOF
origin_web_ui_allowed=lan
origin_pin_allowed=lan
csrf_allowed_origins=https://${VM_IP}:47990,https://${VM_HOSTNAME}:47990,https://${VM_HOSTNAME}.local:47990
EOF
mkdir -p "$user_home/.config/systemd/user/app-dev.lizardbyte.app.Sunshine.service.d"
cat > "$user_home/.config/systemd/user/app-dev.lizardbyte.app.Sunshine.service.d/override.conf" <<'EOF'
[Unit]
After=graphical-session.target
PartOf=graphical-session.target
[Install]
WantedBy=graphical-session.target
[Service]
Restart=on-failure
RestartSec=5
EOF
chown -R "$VM_USER:$VM_USER" "$user_home/.config/sunshine" \
"$user_home/.config/systemd"
as_user XDG_RUNTIME_DIR=/run/user/$(id -u $VM_USER) \
systemctl --user daemon-reload 2>/dev/null || true
as_user XDG_RUNTIME_DIR=/run/user/$(id -u $VM_USER) \
systemctl --user enable app-dev.lizardbyte.app.Sunshine.service 2>/dev/null || true
done_marker sunshine
}
# =============================================================================
# STAGE 6: AUTOSTART
# tty1 autologin → bash_profile → /usr/local/bin/steam-session → dbus-run-session
# labwc → autostart launches gamescope + Steam Big Picture + Sunshine
#
# --xwayland-count 2 in gamescope is REQUIRED for Proton, otherwise:
# "nodrv_CreateWindow: Application tried to create a window, but no driver
# could be loaded."
# =============================================================================
stage_autostart() {
[[ "$INSTALL_AUTOSTART" == "1" ]] || return
is_done autostart && { log "Skipping autostart"; return; }
log "Stage 6: tty1 autologin + labwc/gamescope/Steam session"
local user_home=$(getent passwd "$VM_USER" | cut -d: -f6)
mkdir -p /etc/systemd/system/[email protected]
cat > /etc/systemd/system/[email protected]/autologin.conf <<EOF
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin ${VM_USER} --noclear %I \$TERM
EOF
cat > /usr/local/bin/steam-session <<'EOF'
#!/bin/bash
# dbus-run-session gives the session a per-user D-Bus bus, which labwc,
# Sunshine, and Steam all need.
systemctl --user start pipewire pipewire-pulse wireplumber
exec dbus-run-session labwc
EOF
chmod +x /usr/local/bin/steam-session
mkdir -p "$user_home/.config/labwc"
cat > "$user_home/.config/labwc/environment" <<'EOF'
XDG_CURRENT_DESKTOP=labwc:wlroots
XDG_SESSION_TYPE=wayland
STEAM_MULTIPLE_XWAYLANDS=1
ENABLE_GAMESCOPE_WSI=1
MOZ_ENABLE_WAYLAND=1
EOF
cat > "$user_home/.config/labwc/autostart" <<'EOF'
dbus-update-activation-environment --systemd --all 2>/dev/null
systemctl --user import-environment WAYLAND_DISPLAY DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP
systemctl --user start graphical-session.target
# Belt-and-suspenders: start Sunshine explicitly
systemctl --user start app-dev.lizardbyte.app.Sunshine.service &
# Steam Big Picture inside gamescope.
# --xwayland-count 2 is REQUIRED for Proton (gives Wine its own X server).
gamescope -W 1920 -H 1080 -r 60 \
--adaptive-sync --fullscreen \
--steam --expose-wayland \
--xwayland-count 2 \
-- steam -gamepadui &
EOF
cat > "$user_home/.config/labwc/rc.xml" <<'EOF'
<?xml version="1.0"?>
<labwc_config>
<core><decoration>no</decoration><gap>0</gap></core>
<windowRules>
<windowRule identifier="*">
<serverDecoration>no</serverDecoration>
<skipTaskbar>yes</skipTaskbar>
</windowRule>
</windowRules>
</labwc_config>
EOF
if ! grep -q steam-session "$user_home/.bash_profile" 2>/dev/null; then
cat >> "$user_home/.bash_profile" <<'EOF'
# Auto-launch Steam session on tty1
if [[ -z $DISPLAY ]] && [[ $(tty) == /dev/tty1 ]]; then
exec /usr/local/bin/steam-session
fi
EOF
fi
chown -R "$VM_USER:$VM_USER" "$user_home/.config/labwc" "$user_home/.bash_profile"
systemctl set-default multi-user.target
done_marker autostart
}
# =============================================================================
# STAGE 7: FIREFOX
# =============================================================================
stage_firefox() {
[[ "$INSTALL_FIREFOX" == "1" ]] || return
is_done firefox && { log "Skipping Firefox"; return; }
log "Stage 7: Firefox"
dnf install -y firefox
done_marker firefox
}
# =============================================================================
# STAGE 8: HOSTNAME + FIREWALL
# Sunshine/Moonlight ports:
# TCP: 47984 (HTTP), 47989 (HTTP), 47990 (HTTPS web UI), 48010 (RTSP)
# UDP: 47998-48000 (video/control/audio), 8000 (mDNS)
# =============================================================================
stage_hostname() {
is_done hostname && return
log "Stage 8a: hostname"
hostnamectl set-hostname "$VM_HOSTNAME"
done_marker hostname
}
stage_firewall() {
[[ "$INSTALL_FIREWALL" == "1" ]] || return
is_done firewall && { log "Skipping firewall"; return; }
log "Stage 8b: firewall"
if systemctl is-active firewalld &>/dev/null; then
firewall-cmd --permanent --add-service=ssh
if [[ "$INSTALL_SUNSHINE" == "1" ]]; then
firewall-cmd --permanent \
--add-port=47984/tcp --add-port=47989/tcp \
--add-port=47990/tcp --add-port=48010/tcp \
--add-port=47998-48000/udp --add-port=8000/udp
fi
firewall-cmd --reload
else
warn "firewalld not running, skipping firewall config"
fi
done_marker firewall
}
# =============================================================================
# MAIN
# =============================================================================
stage_base
stage_rpmfusion
stage_nvidia
stage_gaming
stage_sunshine
stage_autostart
stage_firefox
stage_hostname
stage_firewall
log "Bootstrap complete."
echo
if [[ "$INSTALL_NVIDIA" == "1" ]] && ! modinfo -F sig_key nvidia &>/dev/null; then
echo "Nvidia module may not load due to Secure Boot. Enroll the key:"
echo " sudo mokutil --import /etc/pki/akmods/certs/public_key.der"
echo " # Set a one-time password, reboot, enroll at the blue MOK screen"
fi
echo
echo "Reboot to apply everything: sudo reboot"
if [[ "$INSTALL_SUNSHINE" == "1" ]]; then
echo "Sunshine web UI after boot: https://${VM_IP}:47990"
fi
echo "Steam Big Picture will launch automatically on boot."
echo
echo "State markers: $STATE_DIR (delete a .done file to re-run a stage)"
echo "Full log: $LOG_FILE"