Skip to content

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, and non-free-firmware APT components.
  • [ ] Create the homelab_user admin 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 the docker group before Ansible runs.
  • [ ] Install firmware-intel-graphics, intel-media-va-driver-non-free, vainfo, and intel-gpu-tools for the Intel Arc GPU. See Host facts.

2. Storage

  • [ ] Identify disks through /dev/disk/by-id/; do not rely on /dev/sdX.
  • [ ] Restore /etc/fstab and snapraid.conf from 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/user and 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 override storage_assert_branches (default /mnt/disk1../mnt/disk5 in ansible/roles/storage_assert/defaults/main.yml) in your site variables to the new branch list, and match the mergerfs x-systemd.requires-mounts-for entries in /etc/fstab. Otherwise host.yml fails 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/fstab from 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.conf from the reference skeleton, create the SnapRAID content directory, then run the first full snapraid sync to 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.