# nuc — bare-metal reinstall procedure How to rebuild the Incus host from scratch if `/dev/sda` (512 GB SSD, `XINCUU-512GB`) has to be replaced. ## Hardware - NUC-class mini PC, Intel Alder Lake-N (iGPU `i915`, HDMI to projector/TV) - `sda`: 512 GB SSD — OS **and** ZFS pool `data` (all Incus instance disks) - `sdb`: 4 TB WD Red (USB enclosure) — ZFS pool `usb4t`: `usb4t/backup` → `/backup` (incus exports for nuc + ks4, 1 TB quota), `usb4t/media` → `/srv/media` (media library) - USB: Pioneer USB audio (`08e4:0176`), Logitech Unifying receiver (K400), CSCTEK USB Audio and HID - NIC: `enp1s0` (static `192.168.0.3/24`, gw `192.168.0.2`) ## ⚠️ What dies with sda The ZFS pool `data` lives on `sda5` → **all instance root disks are lost** with the OS. The USB pool `usb4t` (backups + media) survives — all instances are replicated onto it with [`scripts/incus-copy.sh`](https://git.lutran.fr/julien/scripts/src/branch/main/incus-copy.sh) (deployed at `/root/scripts/` on nuc; set up 2026-08-09): ```sh # one-time setup (already done; re-run only on a fresh install) incus storage create nucbackup zfs source=usb4t/backup/nuc incus project create backup -c features.images=false -c features.profiles=false # refresh the replicas (all instances, stopped ones included) /root/scripts/incus-copy.sh -p backup -s nucbackup ``` Runs nightly via `/etc/cron.d/incus-copy` at **03:30** (30 min after the profile-scheduled 03:00 instance snapshots, so VM refreshes stay incremental), logging to `/var/log/incus-copy.log` (logrotate: `/etc/logrotate.d/incus-copy`). Note the script's `flock` is global: if a future 02:30 ks4 pull overruns past 03:30, the local run aborts loudly for that night instead of overlapping. Replicas sit **stopped** in project `backup` with `boot.autostart=false` (the script enforces it). Containers refresh in seconds; the homeassistant **VM re-sends its full volume** unless the source has snapshots (`snapshots.schedule`) to diff against. `jellyfin-server` / `jellyfin-client` are also rebuildable from [jellyfin-server.md](jellyfin-server.md) / [jellyfin-client.md](jellyfin-client.md); the media library lives on `usb4t/media` (originals on the NAS `//192.168.0.10/Shared`). ## 1. Install Debian 13 (trixie) Netinst ISO, manual partitioning of the new sda (GPT/UEFI): | Part | Size | Type / FS | Mount | |------|---------|-----------|-------------| | sda1 | ~1 GB | EFI vfat | `/boot/efi` | | sda2 | ~2 GB | ext4 | `/boot` | | sda3 | ~47 GB | ext4 | `/` | | sda4 | ~1 GB | swap | — | | sda5 | rest (~427 GB) | **leave unformatted** (ZFS later) | — | - Tasks: only "SSH server" + standard utilities (no desktop). - Sources: `main contrib non-free-firmware` (contrib is required for `zfs-dkms`). ## 2. Base system ```sh apt update && apt full-upgrade -y apt install -y \ firmware-intel-graphics firmware-iwlwifi firmware-realtek \ firmware-sof-signed intel-microcode \ linux-headers-amd64 zfs-dkms zfsutils-linux zfs-zed \ cifs-utils curl vim htop ripgrep sysstat dmidecode pciutils usbutils ``` Static network — `/etc/network/interfaces` (ifupdown): ``` source /etc/network/interfaces.d/* auto lo iface lo inet loopback allow-hotplug enp1s0 iface enp1s0 inet static address 192.168.0.3 netmask 255.255.255.0 gateway 192.168.0.2 dns-nameservers 1.1.1.1 9.9.9.9 ``` Restore `/root/.ssh/authorized_keys` (3 keys; one is `id_rsa_claude.pub` from this repo). ## 3. ZFS pools ```sh # instance pool on the new SSD ls -l /dev/disk/by-id/ | grep sda5 zpool create data /dev/disk/by-id/-part5 # USB pool (backups + media) survived — just import it zpool import usb4t ``` (If the old sda still works and only the OS was reinstalled elsewhere: `zpool import data` instead — the instances survive.) `usb4t` reference (as created 2026-08: whole disk, `ashift=12`, `compression=zstd`, `atime=off`, `xattr=sa`, `acltype=posixacl`; datasets `usb4t/backup` → `/backup` with `quota=1T`, subdatasets `nuc`/`ks4`, and `usb4t/media` → `/srv/media`). ## 4. Incus (Zabbly stable repo) ```sh mkdir -p /etc/apt/keyrings curl -fsSL https://pkgs.zabbly.com/key.asc -o /etc/apt/keyrings/zabbly.asc cat > /etc/apt/sources.list.d/zabbly-incus-stable.sources < /etc/sudoers.d/incus <<'EOF' ## Allow harmless incus commands to be called through sudo ## without a password. ## ## CAUTION: Any syntax error introduced here will break sudo. ## Always edit/validate with: visudo -cf /etc/sudoers.d/incus ## ## Note: "incus config *" also covers config set/edit and device add, ## which can escalate (e.g. security.privileged, host disk mounts). ## Trim to "config show/get" if strict read-only is wanted. ## Cmnd alias specification Cmnd_Alias C_INCUS = \ /usr/bin/incus config, /usr/bin/incus config *, \ /usr/bin/incus list, /usr/bin/incus list *, \ /usr/bin/incus info, /usr/bin/incus info *, \ /usr/bin/incus version, \ /usr/bin/incus top, \ /usr/bin/incus monitor, /usr/bin/incus monitor *, \ /usr/bin/incus snapshot list, /usr/bin/incus snapshot list *, \ /usr/bin/incus image list, /usr/bin/incus image list *, \ /usr/bin/incus image info *, \ /usr/bin/incus profile list, /usr/bin/incus profile show *, \ /usr/bin/incus project list, /usr/bin/incus project info *, \ /usr/bin/incus network list, /usr/bin/incus network list *, \ /usr/bin/incus network show *, /usr/bin/incus network info *, \ /usr/bin/incus storage list, /usr/bin/incus storage show *, \ /usr/bin/incus storage info *, \ /usr/bin/incus storage volume list, /usr/bin/incus storage volume list *, \ /usr/bin/incus operation list, /usr/bin/incus operation show *, \ /usr/bin/incus remote list, \ /usr/bin/incus warning list, /usr/bin/incus warning show * ## allow julien to use harmless incus commands without a password julien ALL = (root) NOPASSWD: C_INCUS EOF chmod 0440 /etc/sudoers.d/incus visudo -cf /etc/sudoers.d/incus # must print "parsed OK" ``` ## 5. Restore instances The replicas live on `usb4t/backup/nuc` and survived. Re-register them with the fresh Incus, then copy back onto the rebuilt `data` pool: ```sh # recreate the containers' home first (section 4), then: incus project create backup -c features.images=false -c features.profiles=false incus admin recover # point it at pool nucbackup (zfs, source=usb4t/backup/nuc) # copy each replica back to the default project / SSD pool: incus copy blocky blocky --project backup --target-project default -s data incus config set blocky boot.autostart=true incus start blocky # … same for privoxy, homeassistant, jellyfin-* ``` (jellyfin-* can alternatively be rebuilt from [jellyfin-server.md](jellyfin-server.md) / [jellyfin-client.md](jellyfin-client.md); `/srv/media` is already there once `usb4t` is imported.) ## 6. Nightly automation | When | What | Where | |-------|------|-------| | 03:00 | instance snapshots (`snapshots.schedule` on the default profile, expiry 7d) | incus | | 03:30 | replicate all instances to the USB pool (`incus-copy.sh -p backup -s nucbackup`) | `/etc/cron.d/incus-copy` → `/var/log/incus-copy.log` | | 05:00 | apt dist-upgrade all running containers (`incus-container-upgrade.sh`; VMs and non-apt containers skipped; jellyfin pinned to the 10.11 series in-container) | `/etc/cron.d/incus-container-upgrade` → `/var/log/incus-container-upgrade.log` | The ordering is deliberate: snapshot → backup → upgrade, so a broken upgrade is always one snapshot-restore away and the replicas predate it. Both logs rotate monthly (`/etc/logrotate.d/incus-*`). ## 7. Post-install checklist - [ ] `zpool status` healthy (both `data` and `usb4t`), `incus list` shows expected instances - [ ] incus ordered after ZFS mounts (instances bind-mount `/srv/media`): `/etc/systemd/system/incus.service.d/after-zfs.conf` with `[Unit]` / `After=zfs-mount.service zfs.target` - [ ] `boot.autostart=true` on blocky, privoxy, jellyfin-* - [ ] Timezone `Europe/Paris` on host and in every container (`incus exec -- timedatectl set-timezone Europe/Paris`; the install scripts in the per-container docs include it) - [ ] Host boots to `multi-user.target`, nothing grabs the GPU (required by the jellyfin-client kiosk) - [ ] Jellyfin web at `http://192.168.0.5:8096`, kiosk UI on HDMI, sound on the Pioneer, "Pioneer A-70" visible in Spotify Connect - [ ] LAN DNS: clients use blocky at `192.168.0.254` (host itself uses public `1.1.1.1` + `9.9.9.9`, never blocky, to avoid a bootstrap loop) - [ ] Optional (only if QSV low-power encoders are wanted): `echo 'options i915 enable_guc=3' > /etc/modprobe.d/i915.conf && update-initramfs -u`