Debian 13 rebuild guide
This checklist rebuilds the server from a minimal Debian 13 installation.
Ansible converges host configuration but does not partition or format disks,
write /etc/fstab or snapraid.conf, install Docker or ansible-core, create
the admin user, or create .env files.
1. Base system
- [ ] Install Debian 13, set the hostname, locale, and timezone, and enable the
main,contrib,non-free, andnon-free-firmwareAPT components. - [ ] Create the
homelab_useradmin user and configure key-only SSH. Keep the current session open until a second login succeeds. - [ ] Install Docker Engine, the Compose plugin,
ansible-core, mergerfs, and SnapRAID. Docker must create thedockergroup before Ansible runs. - [ ] Install
firmware-intel-graphics,intel-media-va-driver-non-free,vainfo, andintel-gpu-toolsfor the Intel Arc GPU. See Host facts.
2. Storage
- [ ] Identify disks through
/dev/disk/by-id/; do not rely on/dev/sdX. - [ ] Restore
/etc/fstabandsnapraid.conffrom the private recovery copies when surviving disks retain their by-id identities. Otherwise reconstruct them from the current disk mapping and the storage reference. Verify every mapping before mounting anything. Do not format existing data, parity, or cache filesystems during a normal rebuild. - [ ] Mount the five XFS data disks, the XFS parity disk, and the existing btrfs
cache. Confirm mergerfs is mounted at
/mnt/userand btrfs at/mnt/cache. - [ ] Create
/var/snapraid. Preserve existing appdata and media ownership; if the cache is being restored, restore appdata before deploying containers. - [ ] If the parity disk was replaced, verify every disk mapping before running
the first full
snapraid sync. Never format a surviving data disk.
Empty disks only (greenfield / different hardware)
Skip this block on any rebuild where the disks already hold data, parity, or
appdata. Run it only when building on genuinely blank disks (a from-scratch
install on completely different hardware). mkfs destroys everything on the
target device, so confirm each by-id path is an empty disk before formatting.
- [ ] Format the five data disks and the parity disk as XFS, and the cache as
btrfs. Adjust the disk count to the new hardware and address every device
by its
/dev/disk/by-id/path. If the data-disk count is not five, also overridestorage_assert_branches(default/mnt/disk1../mnt/disk5inansible/roles/storage_assert/defaults/main.yml) in your site variables to the new branch list, and match the mergerfsx-systemd.requires-mounts-forentries in/etc/fstab. Otherwisehost.ymlfails the pool-membership assertion:
# data disks + parity: XFS. Repeat for each real by-id path.
sudo mkfs.xfs /dev/disk/by-id/ata-YOUR-DATA-DISK-1
# ... one mkfs.xfs per data disk ...
sudo mkfs.xfs /dev/disk/by-id/ata-YOUR-PARITY-DISK
# cache: btrfs
sudo mkfs.btrfs /dev/disk/by-id/nvme-YOUR-CACHE-DISK
- [ ] Create the mountpoints, then write
/etc/fstabfrom the storage reference template and mount everything:
Create exactly one /mnt/diskN per data disk, no more: the mergerfs glob
/mnt/disk* treats every such directory as a branch, so a leftover empty
mountpoint would join the pool from the OS filesystem and fail (or corrupt)
the membership assertion. For a five-disk build:
sudo mkdir -p /mnt/disk{1,2,3,4,5} /mnt/parity /mnt/cache /mnt/user
# fill in the REPLACE-WITH-BY-ID entries in /etc/fstab, then:
sudo systemctl daemon-reload
sudo mount -a
- [ ] Create
snapraid.conffrom the reference skeleton, create the SnapRAID content directory, then run the first fullsnapraid syncto build parity from the empty data disks:
sudo mkdir -p /var/snapraid
sudo snapraid sync
Then continue with the appdata ownership steps below.
The cache contains live appdata and must never be reformatted. Ansible's
preflight checks stop if /mnt/user is not mergerfs or /mnt/cache is not
btrfs. Restic restores preserve appdata ownership. Without a restore, create
each bind source with the UID/GID specified by its Compose service instead of
letting Docker create a root-owned directory.
3. Repository and private configuration
- [ ] Clone the repository to
/opt/homelab. This path is required by the playbooks and systemd units. - [ ] Copy each example environment file and add the escrowed values:
cp .env.example .env
cp monitoring/.env.example monitoring/.env
cp actual/.env.example actual/.env
chmod 600 .env monitoring/.env actual/.env
- [ ] Restore the private non-secret site variables from escrow:
mkdir -p ansible/inventory/group_vars/homelab
cp ansible/inventory/group_vars/homelab/site.yml.example \
ansible/inventory/group_vars/homelab/site.yml
chmod 600 ansible/inventory/group_vars/homelab/site.yml
Replace every example value before convergence. This ignored file contains the operator account, network endpoints, SSH port, hardware identifiers, and public service URLs used to render host and monitoring configuration.
- [ ] Confirm the three files are owned by
homelab_user. Keep their escrowed copies current when credentials change.
4. Converge and deploy
Run host convergence locally as root:
cd /opt/homelab
git pull
cd ansible
sudo ansible-playbook -c local playbooks/host.yml
host.yml installs the remaining host packages, validates storage and network
prerequisites, configures backups and monitoring, installs systemd units,
creates the shared proxy network, and applies Docker, SSH, and host hardening.
It does not deploy containers. A Docker configuration change is rejected while
containers are running unless the operator explicitly passes
-e allow_docker_restart=true.
If host.yml added homelab_user to the docker group, end the login session and
connect again before deploying. Confirm id -nG includes docker.
Deploy all four Compose projects without sudo:
cd /opt/homelab/ansible
ansible-playbook -c local playbooks/deploy.yml
Use -e compose_deploy_pull=true after a merged image update. The retired
install.sh is available in Git history but is not a deployment path.
5. Verification
- [ ] Re-run
host.yml; it should report no changes. - [ ] Confirm the root, monitoring, and Actual Compose projects are healthy.
- [ ] Confirm Cloudflare ingress, Caddy, and the expected local service URLs.
- [ ] Run a Plex hardware transcode and verify activity with
intel_gpu_top. - [ ] Confirm all maintenance and monitoring timers are enabled.
- [ ] Follow the disaster recovery rehearsal to restore a representative application from restic and verify its state, ownership, startup, and cleanup before treating the rebuild as complete.