This repository is the central nervous system for my homelab and laptop provisioning. It manages the lifecycle of Proxmox VMs through a "Define once, Deploy anywhere" workflow.
.
├── terraform/
│ ├── brownfunk/ # Hardware definition for the main cluster
│ │ ├── main.tf
│ │ └── variables.tf
│ ├── myNewNode/ # Sandbox/Secondary node definitions
│ └── [Go to Terraform Docs](./terraform/README.md)
│
├── ansible/
│ ├── inventory/ # Host definitions
│ │ ├── brownfunk.ini # Generated by TF (Main fleet)
│ │ └── myNewNode.ini # Generated by TF (Experimental nodes)
│ ├── collections/ # Local Ansible modules (Git ignored)
│ ├── tasks/ # Reusable logic (Docker, LUKS, etc.)
│ └── [Go to Ansible Docs](./ansible/README.md)
│
├── terraform.tfvars # Global Secrets & API Keys (Git ignored)
└── .gitignore # Protects state, inventory, and secreto
Terraform creates the VMs, Ansible prepares them, and the docker compose stacks come from a separate repo:
lbesnard/fleet-compose (machines/<project>-<vm_type>/docker-compose, secrets encrypted with its own vault key).
| VM (terraform = ansible host = stack name) | what runs there |
|---|---|
brownfunk-nas / beefunk-nas |
samba, rsync-server, scrutiny (next to the disks) |
brownfunk-media / beefunk-media |
traefik, pihole, wireguard, the arr stack, nextcloud... everything else; also the Tailscale subnet router |
jellyfin (LXC) |
Jellyfin; traefik on the media VM routes to it (traefik/dynamic_tls.yml) |
How it fits together (setup_homelabs.yml):
- NAS play:
tasks/storage_setup.ymlunlocks/mounts the passthrough disks at/media/<name>,tasks/nfs_server_nas.ymlexports the folders ofstorage_map[project_dir], then fleet-compose is synced anddocker compose upruns. - Media play:
tasks/nfs_client_media.ymlmounts everystorage_mapkey at/mnt/nfs/<key>, pihole's local DNS (05-lan.conf) is generated, fleet-compose is synced and compose is started. - Sync (
tasks/fleet_compose_sync.yml): the VM clones fleet-compose and runs itsinstall.sh --machine <project>-<type>, which decrypts the secrets and fetches vendored repos. The fleet-compose vault password is stored encrypted in this repo (vars/vault.yml:fleet_compose_vault_pass) and written to~/.config/fleet-compose/vault-passon the VM; nothing to export:
ansible-playbook -i inventories/beefunk.ini setup_homelabs.yml --ask-vault-pass(Fleet-Control's own vault, vars/vault.yml and vars/paths.yml, has a different password from fleet-compose's.)
There is no -net VM: pihole, traefik, wireguard and the Tailscale subnet router all run on the media VM. The traefik routes to the Jellyfin LXC and to the NAS (scrutiny) are tracked in fleet-compose/machines/<project>-media/docker-compose/traefik/dynamic_tls.yml, not generated.
vars/vault.yml->disk_configs.<project>.<by-id path>:name,parent_path(/media),fstype, optionalluks_password. Nested per project because two docks can report the same/dev/disk/by-idstring. Every passthrough disk interraform/<project>/variables.tfmust have an entry; the sanity play checks it before doing anything. A disk may be partitioned or LUKS/ext4/xfs on the whole disk, both are detected.vars/paths.yml->storage_map.<project>.<key>: folders on the NAS that are exported over NFS and mounted on the media VM at/mnt/nfs/<key>. The*_PATHvariables of the fleet-compose<project>-media/.envare/mnt/nfs/<key>/...; those of<project>-nas/.envare the real/media/<disk>/...paths. When you add a folder to a compose file, add its key here.
IPs are hard-coded in several places (terraform vm_fleet/lxc_fleet, the generated inventory, docs, and in fleet-compose: .env, pihole 02-custom.conf, traefik/dynamic_tls.yml, prometheus/alloy targets...). One script finds and changes them in both repos:
scripts/change-ip.sh --list # every IP found, with counts and files
scripts/change-ip.sh 192.168.1.202 192.168.1.50 # dry run: shows each changed line
scripts/change-ip.sh 192.168.1.202 192.168.1.50 --applyIt works for any address (VM/LXC, Tailscale, gateway, subnet), matches whole addresses only, skips the legacy single-host stacks unless --legacy, edits the plaintext secrets and re-encrypts them (./fleet encrypt), and never commits. After --apply: terraform apply for a VM/LXC address, then review/commit/push both repos. The fleet-compose checkout is ../fleet-compose (or --compose DIR / $FLEET_COMPOSE_DIR).
Navigate to the specific node folder in Terraform. Applying this will trigger the provider to build the VM and use the .tftpl to write the .ini file into the Ansible inventory folder.
cd terraform/brownfunk
terraform init
terraform applyEnsure the local collections are installed into the project (required once, or whenever requirements.yml changes).
cd ../../ansible
ansible-galaxy collection install -r requirements.yml -p ./collectionsExecute the playbook against the specific inventory generated in step 1.
ansible-playbook -i inventory/brownfunk.ini setup_homelabs.yml --ask-vault-pass # or --vault-password-file ansible/.vault_pass| Command | Result |
|---|---|
terraform plan |
Check what hardware changes are pending |
terraform refresh |
Sync local state with actual Proxmox status |
terraform apply -compact-warnings |
Clean output during VM creation |
| Command | Result |
|---|---|
ansible-inventory -i inventory/brownfunk.ini --list |
Debug the generated host groups |
ansible-playbook ... --limit "brownfunk-nas*" |
Only run tasks on the NAS node |