Applied live to the 5 running containers; install scripts and the post-install checklist now include the timedatectl step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
9.5 KiB
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 pooldata(all Incus instance disks)sdb: 4 TB WD Red (USB enclosure) — ZFS poolusb4t: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(static192.168.0.3/24, gw192.168.0.1)
⚠️ 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 (deployed at
/root/scripts/ on nuc; set up 2026-08-09):
# 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-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 forzfs-dkms).
2. Base system
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.1
dns-nameservers 192.168.0.1 1.1.1.1
Restore /root/.ssh/authorized_keys (3 keys; one is id_rsa_claude.pub
from this repo).
3. ZFS pools
# instance pool on the new SSD
ls -l /dev/disk/by-id/ | grep sda5
zpool create data /dev/disk/by-id/<new-ssd-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)
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 <<EOF
Enabled: yes
Types: deb
URIs: https://pkgs.zabbly.com/incus/stable
Suites: trixie
Components: main
Architectures: amd64
Signed-By: /etc/apt/keyrings/zabbly.asc
EOF
apt update && apt install -y incus
Initialize with the existing pool and the macvlan network (this is what gives every instance a real LAN IP):
cat <<EOF | incus admin init --preseed
storage_pools:
- name: data
driver: zfs
config:
source: data # use the existing/just-created zpool
networks:
- name: macvlan
type: macvlan
config:
parent: enp1s0
profiles:
- name: default
devices:
eth0:
name: eth0
network: macvlan
type: nic
root:
path: /
pool: data
type: disk
EOF
Known macvlan quirk: the host cannot talk to its own instances (and
vice versa) over macvlan — management is via incus exec, and other LAN
hosts reach them normally.
Let julien run harmless incus commands (list/info/config/show…)
without a password — mutating ones (exec, start/stop, delete)
still prompt:
cat > /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:
# 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-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 statushealthy (bothdataandusb4t),incus listshows expected instances- incus ordered after ZFS mounts (instances bind-mount
/srv/media):/etc/systemd/system/incus.service.d/after-zfs.confwith[Unit]/After=zfs-mount.service zfs.target boot.autostart=trueon blocky, privoxy, jellyfin-*- Timezone
Europe/Parison host and in every container (incus exec <ct> -- 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 uses192.168.0.1+1.1.1.1to 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