Disaster Recovery¶
Full rebuild from a clean Proxmox 8 install. Assumes the OS disk (/dev/sdc) was wiped; the data disks (/dev/sdb 2TB and /dev/sda 1TB) are intact with all service data.
Estimated time: 3–4 hours (mostly waiting for downloads and service startup)
Pre-flight Checklist¶
- [ ] Proxmox 8 freshly installed on the OS disk (
/dev/sdc) - [ ] Run
lsblkto confirmsda(1TB) andsdb(2TB) are detected - [ ]
proxmox-adminrepo cloned or on a USB drive - [ ]
scripts/.envfilled in - copy from.env.exampleand fill all values (KOMODO_* secrets, Cloudflare tokens, feeder config, all API keys) - [ ] Tailscale auth key ready: tailscale.com/admin/settings/keys → generate ephemeral key
- [ ] Pi-hole password
- [ ] Subnet
192.168.50.0/24, gateway192.168.50.1
If data disks are also lost: service configs under
$DATA_ROOT/config/need to be restored from backup. Media library is re-downloadable. Minecraft worlds ($DATA_ROOT/minecraft/volumes/) are irreplaceable - back these up.
Scripts reference¶
All scripts live in scripts/ and are idempotent.
| Script | Purpose | Run when |
|---|---|---|
host-prep.sh |
Storage mounts, pvesm pools, dir tree, shared groups, Zigbee udev | Once on bare host |
common.sh |
Shared functions - sourced by CT scripts, not run directly | - |
ct<id>-<name>.sh |
Per-CT: users, chown, conf patches, start, Periphery install | After CT is created |
Set KOMODO_CORE_ADDR=ws://<komodo-ip>:9120 before running any CT script.
Override Periphery version with PERIPHERY_VERSION=<tag> if needed (default: latest).
Phase 1 - Proxmox Host Foundation (~10 min)¶
1.1 Open a root shell on the Proxmox node.
1.2 Clone the repo:
apt-get install -y git
git clone https://github.com/<your-username>/homelab /opt/proxmox-admin
cd /opt/proxmox-admin
1.3 Run host prep. Verify disk devices first with lsblk -o NAME,SIZE,MODEL, then:
# Adjust DATA_DISK / MINECRAFT_DISK if your devices differ from the defaults (/dev/sdb1, /dev/sda1)
DATA_DISK=/dev/sdb1 MINECRAFT_DISK=/dev/sda1 bash scripts/host-prep.sh
1.4 Verify:
pvesm status # data + minecraft pools listed
ls $DATA_ROOT/config/ # directory tree present
getent group media iot ultrafeeder # shared groups created
Phase 2 - Create LXCs + CT 100 offline setup (~30 min)¶
Use the Proxmox community helper scripts for all containers. Open the Proxmox shell and run the script for each CT below. Enter the custom values shown when prompted - don't accept defaults blindly.
Download the Debian 13 template before starting:
After creating CT 100, immediately run its setup script (offline only - CT 100 is started in Phase 6):
CT 100 - management (Docker)¶
| Setting | Value |
|---|---|
| CT ID | 100 |
| Hostname | management |
| Cores | 2, RAM 2048MB, Disk 8GB |
| IP | 192.168.50.100/24, GW 192.168.50.1 |
CT 101 - pihole¶
| Setting | Value |
|---|---|
| CT ID | 101 |
| Hostname | pihole |
| Cores | 4, CPU units 2048, RAM 1024MB, Disk 4GB |
| IP | 192.168.50.101/24 |
CT 200 - ultrafeeder (Docker)¶
| Setting | Value |
|---|---|
| CT ID | 200, Hostname ultrafeeder |
| Cores | 2, RAM 2048MB, Disk 8GB |
| IP | 192.168.50.200/24 |
CT 201 - iot (Docker)¶
| Setting | Value |
|---|---|
| CT ID | 201, Hostname iot |
| Cores | 2, RAM 1024MB, Disk 8GB |
| IP | 192.168.50.201/24 |
CT 202 - monitoring (Docker)¶
| Setting | Value |
|---|---|
| CT ID | 202, Hostname monitoring |
| Cores | 2, RAM 1024MB, Disk 8GB |
| IP | 192.168.50.202/24 |
CT 300 - media-arr (Docker)¶
| Setting | Value |
|---|---|
| CT ID | 300, Hostname media-arr |
| Cores | 4, CPU limit 2, RAM 4096MB, Disk 16GB |
| IP | 192.168.50.30/24 |
CT 301 - media-server (Docker)¶
| Setting | Value |
|---|---|
| CT ID | 301, Hostname media-server |
| Cores | 4, CPU limit 3, RAM 4096MB, Disk 8GB |
| IP | 192.168.50.31/24 |
CT 302 - media-dl (Docker)¶
| Setting | Value |
|---|---|
| CT ID | 302, Hostname media-dl |
| Cores | 2, CPU limit 2, RAM 1024MB, Disk 8GB |
| IP | 192.168.50.32/24 |
CT 400 - game-panel¶
bash <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/pterodactyl-panel.sh)
| Setting | Value |
|---|---|
| CT ID | 400, Hostname game-panel |
| Cores | 2, RAM 1024MB, Swap 512MB, Disk 8GB |
| IP | 192.168.50.40/24 |
CT 401 - minecraft-wings¶
bash <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/pterodactyl-wings.sh)
| Setting | Value |
|---|---|
| CT ID | 401, Hostname minecraft-wings |
| Cores | 6, RAM 20480MB, Disk 16GB |
| IP | 192.168.50.41/24 |
CT 600 - personal-apps (Docker)¶
| Setting | Value |
|---|---|
| CT ID | 600, Hostname personal-apps |
| Cores | 2, RAM 2048MB, Disk 4GB |
| IP | 192.168.50.60/24 |
Phase 3 - Run CT Scripts (~30 min)¶
Each CT script patches the LXC conf, sets directory ownership, starts the CT, and installs Periphery. Periphery agents will connect to Komodo Core once it's running (Phase 6).
# Once: fill in the root env file before running any CT script
cp scripts/.env.example scripts/.env
nano scripts/.env # fill in all values
cd /opt/proxmox-admin
bash scripts/ct101-pihole.sh # pihole first - DNS up before others
bash scripts/ct200-ultrafeeder.sh
bash scripts/ct201-iot.sh
bash scripts/ct202-monitoring.sh
bash scripts/ct300-media-arr.sh
bash scripts/ct301-media-server.sh
bash scripts/ct302-media-dl.sh
bash scripts/ct600-personal-apps.sh
# ct400 and ct401 are handled in Phase 8 (Pterodactyl)
CT 100 (management) was patched in Phase 2 and is started manually in Phase 6.
Each CT script reads scripts/.env, installs Periphery with the correct KOMODO_CORE_ADDR, and pre-deploys compose files and .env files to each stack directory on the host. Stack files are in place before Komodo boots in Phase 6.
Phase 4 - Pi-hole (CT 101) (~15 min)¶
CT 101 was started by ct101-pihole.sh in Phase 3. Configure it now before deploying other services - DNS must be up for *.home.example.com resolution.
4.1 Set listening mode so Tailscale clients (100.x.x.x) can query:
pct exec 101 -- sed -i 's/listeningMode = .*/listeningMode = "ALL"/' /etc/pihole/pihole.toml
pct exec 101 -- pihole restartdns
4.2 Configure Unbound. Push the config files:
pct exec 101 -- apt-get install -y unbound
# home-internal.conf - wildcard redirect for the internal domain
pct exec 101 -- bash -c 'cat > /etc/unbound/unbound.conf.d/home-internal.conf << EOF
server:
local-zone: "home.example.com." redirect
local-data: "home.example.com. A 192.168.50.100"
EOF'
# homelab.conf - performance tuning (name is legacy, the .homelab zone is retired)
pct exec 101 -- bash -c 'cat > /etc/unbound/unbound.conf.d/homelab.conf << EOF
server:
num-threads: 2
outgoing-range: 256
num-queries-per-thread: 1024
prefetch: yes
prefetch-key: yes
serve-expired: yes
edns-buffer-size: 1232
EOF'
# z-performance.conf (must sort after pi-hole.conf to override num-threads: 1)
pct exec 101 -- bash -c 'cat > /etc/unbound/unbound.conf.d/z-performance.conf << EOF
server:
num-threads: 2
so-reuseport: yes
EOF'
pct exec 101 -- systemctl enable --now unbound
4.3 In Pi-hole admin UI (http://192.168.50.101/admin):
- Upstream DNS → set to 127.0.0.1#5335 (Unbound only)
- Remove all external forwarders
4.4 Update router DHCP settings:
- LAN → DHCP → DNS Server 1: 192.168.50.101
- DNS Server 2: 1.1.1.1 (fallback)
4.5 Verify:
See pihole for full config reference.
Phase 5 - Docker Daemon Config (~5 min)¶
All Docker LXCs need MTU 1450 to prevent fragmentation issues on this network (ISP MTU). Apply to each Docker CT - they're all running now:
for ct in 200 201 202 300 301 302 600; do
pct exec $ct -- bash -c 'mkdir -p /etc/docker && cat > /etc/docker/daemon.json << EOF
{
"log-driver": "journald",
"mtu": 1450
}
EOF
systemctl restart docker'
echo "CT $ct: daemon configured"
done
Troubleshooting: if Docker image pulls time out or fail with connection errors, add
"registry-mirrors": ["https://mirror.gcr.io"]todaemon.json. This ISP occasionally blocks Cloudflare R2 (Docker Hub's CDN).
Phase 6 - Komodo Core Bootstrap (CT 100) (~15 min)¶
6.1 Copy the bootstrap compose and fill in secrets:
pct exec 100 -- mkdir -p /opt/komodo
pct push 100 stacks/management/komodo/compose.yaml /opt/komodo/compose.yaml
pct push 100 stacks/management/komodo/compose.env.example /opt/komodo/compose.env
# Fill in secrets - values are in scripts/.env:
# pct enter 100 → then edit /opt/komodo/compose.env (copy KOMODO_* vars from .env)
6.2 Configure Docker daemon on CT 100:
pct exec 100 -- bash -c 'mkdir -p /etc/docker && cat > /etc/docker/daemon.json << EOF
{
"log-driver": "journald",
"mtu": 1450
}
EOF
systemctl restart docker'
6.3 Start Komodo:
pct exec 100 -- bash -c 'cd /opt/komodo && docker compose --env-file compose.env -f compose.yaml up -d'
6.4 Wait ~30 seconds, open http://192.168.50.100:9120, create the admin account.
6.5 Add all servers (Settings → Servers → Add). Use wss:// for the address. Periphery was pre-installed in Phase 3 - servers should turn green within ~30 seconds of being added.
| Server | Address |
|---|---|
| management (Local) | wss://192.168.50.100:8120 |
| pihole | wss://192.168.50.101:8120 |
| ultrafeeder | wss://192.168.50.200:8120 |
| iot | wss://192.168.50.201:8120 |
| monitoring | wss://192.168.50.202:8120 |
| media-arr | wss://192.168.50.30:8120 |
| media-server | wss://192.168.50.31:8120 |
| media-dl | wss://192.168.50.32:8120 |
| game-panel | wss://192.168.50.40:8120 (after Phase 8) |
| personal-apps | wss://192.168.50.60:8120 |
Enable each server after adding (created as enabled: false by default).
Phase 7 - Deploy Stacks via Komodo (~30 min)¶
The CT scripts (Phase 3) pre-deployed compose files and .env files to each stack directory. In Komodo, register each stack in "files on host" mode pointing to the existing files and deploy - no manual file copying needed. See komodo for the stack creation workflow.
Deploy in this order (Traefik first, cloudflared depends on Traefik):
Management stacks (Periphery root: /data/config/management/komodo/periphery):
1. traefik
2. cloudflared
3. glance, filebrowser, node-exporter-local
All other stacks (Periphery root: /data/komodo, order doesn't matter):
| Server | Stacks |
|---|---|
| monitoring | monitoring, node-exporter-monitoring |
| ultrafeeder | ultrafeeder, node-exporter-ultrafeeder |
| iot | iot, node-exporter-iot |
| media-arr | media-arr, node-exporter-media-arr |
| media-server | media-server, node-exporter-media-server |
| media-dl | media-dl, node-exporter-media-dl |
| personal-apps | actual-budget |
After deploying monitoring, push the monitoring config files (not managed by Komodo):
pct push 202 stacks/monitoring/prometheus.yml /data/prometheus.yml
pct push 202 stacks/monitoring/pve.yml /data/pve-exporter/pve.yml
pct exec 202 -- chmod 644 /data/prometheus.yml /data/pve-exporter/pve.yml
pve-exporter token: if the PVE API token was lost with the OS disk, recreate it:
pveum user token add pve-exporter@pve metrics --privsep 0. Assign PVEAuditor role at/. See monitoring.
Phase 8 - Pterodactyl Panel + Wings (CT 400/401) (~30 min)¶
Run the CT scripts first - they patch the conf, start the CTs, and install Periphery:
export KOMODO_CORE_ADDR=ws://192.168.50.100:9120
bash scripts/ct400-game-panel.sh
bash scripts/ct401-minecraft-wings.sh
The helper scripts ran the full Pterodactyl/Wings install during CT creation (Phase 2). Now restore data and connect Wings.
8.1 Restore Panel data (if $DATA_ROOT/config/panel/ has the backup):
The install symlinked /opt/pterodactyl-panel/storage → /data/storage and .env → /data/.env, so Panel is already using the restored data. Restart services:
8.2 Restore Wings config. The install already created Wings; configure it using the token from the panel:
If/data/pterodactyl/config.yml exists from backup, Wings uses it immediately. Otherwise, create the node in the panel and run the configure command it generates.
8.3 Fix Wings systemd - prevent startup failure when Tailscale isn't ready:
pct exec 401 -- bash -c '
mkdir -p /etc/systemd/system/wings.service.d
cat > /etc/systemd/system/wings.service.d/tailscale.conf << EOF
[Service]
After=tailscaled.service
ExecStartPre=/bin/sleep 10
EOF
systemctl daemon-reload && systemctl restart wings'
Phase 9 - Tailscale (~15 min)¶
The community helper scripts add lxc.cgroup2.devices.allow + lxc.mount.entry entries for TUN - these don't work. The CT scripts added the correct dev0 entry, but you need to remove the bad entries added by the helper scripts first.
For each Tailscale CT (100, 101, 400, 401):
vmid=100 # repeat for 101, 400, 401
pct stop $vmid
# Remove bad entries if the helper script added them
sed -i '/lxc.cgroup2.devices.allow: c 10:200/d' /etc/pve/lxc/$vmid.conf
sed -i '/lxc.mount.entry: \/dev\/net\/tun/d' /etc/pve/lxc/$vmid.conf
# Confirm dev0 is present (added by the CT script)
grep 'dev/net/tun' /etc/pve/lxc/$vmid.conf
pct start $vmid
pct exec $vmid -- bash <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/add-tailscale-lxc.sh)
Authenticate each:
| CT | Command |
|---|---|
| 100 | tailscale up --advertise-routes=192.168.50.0/24 --accept-dns=false --accept-routes=false |
| 101 | tailscale up --accept-routes=false --accept-dns=false |
| 400 | tailscale up --accept-routes=false --accept-dns=false |
| 401 | tailscale up --accept-routes=false --accept-dns=false |
After CT 100 authenticates: Tailscale admin → Machines → management → Edit route settings → enable 192.168.50.0/24.
After CT 101 authenticates: Tailscale admin → DNS → Global nameserver: <pihole-ts-ip>, Override local DNS: on.
Tailscale IPs change on re-auth. Update Wings node FQDN in the panel if CT 401's IP changed (old:
<wings-ts-ip>). Also updateallowed_originsin Wings config if CT 400's IP changed.
Phase 10 - Verify (~10 min)¶
# All CTs running
for ct in 100 101 200 201 202 300 301 302 400 401 600; do
printf "CT %-4s %s\n" $ct "$(pct status $ct)"
done
# DNS
dig home.example.com @192.168.50.101
# Core services
curl -sf http://192.168.50.100:9120/health # Komodo
curl -sf http://192.168.50.100:8080/ping # Traefik
curl -sf http://192.168.50.31:8096/health # Jellyfin
Open https://glance.home.example.com → Services page - all monitors should go green.
Key Takeaways¶
- All scripts are idempotent - re-run any phase that partially fails
- Data disks survive the rebuild -
$DATA_ROOT/config/has all service state - Komodo secrets must be in a password manager - not stored in repo
- Tailscale IPs change on re-auth - update Wings node FQDN and
allowed_origins - CT scripts handle conf patching, start, and Periphery install - run one per CT after creation
- Docker MTU 1450 is required; registry mirror (
https://mirror.gcr.io) is an optional fallback for pull failures - See lxc inventory for full CT spec reference