Update host NIC/interfaces (gw + DNS to public 1.1.1.1/9.9.9.9, no container dependency), fix transmission-bt kill-switch routes, and add a note that DHCP instances self-heal while static ones (privoxy, transmission-bt) must be updated by hand. Also spell out the macvlan test-from-inside/external-host rule. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
275 lines
10 KiB
Markdown
275 lines
10 KiB
Markdown
# 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`](../scripts/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/<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)
|
|
|
|
```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 <<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):
|
|
|
|
```sh
|
|
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. (So test a container's LAN service from inside
|
|
the container or from an external LAN host — never by pinging its IP from
|
|
the nuc or a sibling container; that always fails by design.)
|
|
|
|
LAN gateway note: the router/gateway is **`192.168.0.2`** (migrated from
|
|
`192.168.0.1`, 2026-08 — `.1` is gone). DHCP-configured instances pick the
|
|
new gateway up automatically; **statically-configured ones must be updated
|
|
by hand.** Current static holdouts: privoxy
|
|
(`/etc/systemd/network/eth0.network`, `Gateway=`) and transmission-bt
|
|
(netplan `routes: via:` + the WG kill-switch `/32`). Symptom of a missed
|
|
one: the service is up and its port answers, but nothing it fetches works.
|
|
|
|
Let `julien` run harmless incus commands (list/info/config/show…)
|
|
without a password — mutating ones (`exec`, `start/stop`, `delete`)
|
|
still prompt:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```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 <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 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`
|