Ansible¶
Infrastructure-as-code for the homelab. Manages configuration, package installation, service deployment, and ongoing maintenance across all hosts from a single control repo.
Repo: rampantlemming/homelab-ansible
Control node¶
Ansible runs from a dedicated LXC container on proxfold.
| Property | Value |
|---|---|
| Container ID | 104 |
| Hostname | control |
| IP | 192.168.1.245 |
| OS | Debian 12 (bookworm) |
| Type | Unprivileged LXC |
| Cores | 1 |
| Memory | 512 MB |
| Storage | 8G root disk |
| Features | nesting=1 |
Note
The control node is in the inventory under lxc_containers with ansible_connection: local, so site.yml applies common, security, auto_updates, and beszel_agent to it like any other host without trying to SSH from CT104 to itself. See playbooks/control.yml for the role list and Drift Detection for how the daily timer runs from this CT.
A WSL box is the primary cold-start control node for disaster recovery (CT104 doesn't exist until proxfold is rebuilt and its backup restored). Both pull from the same GitHub remote and run the same playbooks. See WSL control node bootstrap.
What it manages¶
| Host | Group | Role |
|---|---|---|
| proxfold (192.168.1.250) | proxmox |
Proxmox VE host — ZFS, NFS, GPU drivers, NUT, PBS client |
| arrstack (192.168.1.252) | virtual_machines |
Docker VM — media stack, MediaBot |
| n8n (192.168.1.248) | virtual_machines |
Docker host VM — n8n + Hawser (Phase 5C) |
| plex (192.168.1.230) | lxc_containers |
Plex Media Server LXC |
| pbs (192.168.1.246) | lxc_containers |
Proxmox Backup Server LXC (Phase 5A) |
| beszel (192.168.1.247) | lxc_containers |
Beszel monitoring hub LXC (Phase 5B) |
| edge (192.168.1.244) | lxc_containers |
Caddy reverse proxy LXC (Phase 5D) |
| vintage (192.168.1.235) | lxc_containers |
Vintage Story dedicated server LXC (host-level only) |
| control (192.168.1.245) | lxc_containers |
Ansible control node LXC — ansible_connection: local |
Repo structure¶
homelab-ansible/
├── ansible.cfg # Ansible configuration
├── requirements.yml # Galaxy collection dependencies
├── inventory/
│ ├── hosts.yml # Host inventory and groups
│ ├── group_vars/all/
│ │ ├── vars.yml # Shared variables for all hosts
│ │ └── vault.yml # Encrypted secrets (Ansible Vault)
│ └── host_vars/
│ ├── proxfold.yml
│ ├── arrstack.yml
│ ├── n8n.yml
│ ├── plex.yml
│ ├── pbs.yml
│ ├── beszel.yml
│ ├── edge.yml # Phase 5D
│ ├── vintage.yml
│ └── control.yml
├── playbooks/
│ ├── site.yml # Master playbook (imports the host playbooks below)
│ ├── proxmox-host.yml
│ ├── arrstack.yml
│ ├── plex.yml
│ ├── pbs.yml
│ ├── beszel.yml
│ ├── n8n.yml
│ ├── edge.yml # Phase 5D
│ ├── vintage.yml
│ ├── control.yml
│ └── auto-updates.yml
├── rebuild/ # Proxmox auto-install kit (answer + ISO builder)
├── drift-detection/ # Daily timer + drift-check.sh wrapper for CT104
├── stacks/ # Docker Compose files deployed via Dockhand
│ ├── arrstack/
│ ├── korrosync/
│ └── n8n/
└── roles/
├── common/ # Base system config (all hosts)
├── security/ # SSH hardening, Fail2ban (all hosts)
├── auto_updates/ # Unattended-upgrades wrapper (all hosts)
├── proxmox/ # PVE host baseline + PBS client + notifications
├── docker/ # Docker CE installation
├── zfs/ # ZFS tuning, scrub timer, ZED Discord webhook
├── nvidia/ # GPU drivers, NVENC patch, IPMI fan fix
├── nfs/ # NFS server and client
├── nut/ # Network UPS Tools
├── pbs/ # Proxmox Backup Server (Phase 5A)
├── beszel_hub/ # Beszel hub LXC install (Phase 5B)
├── beszel_agent/ # Per-host Beszel metrics agent (Phase 5B)
├── hawser/ # Dockhand remote-host agent (Phase 5C)
├── caddy/ # Caddy reverse proxy on edge LXC (Phase 5D)
├── arrstack/ # Media stack Docker Compose deployment
└── plex/ # Plex Media Server installation
Prerequisites¶
Galaxy collections required:
| Collection | Min version | Purpose |
|---|---|---|
community.general |
9.0.0 | General admin modules |
ansible.posix |
1.5.0 | POSIX system modules |
community.docker |
3.0.0 | Docker management |
Running playbooks¶
# Full run — all hosts, all roles
ansible-playbook playbooks/site.yml
# Dry run — show what would change, no modifications
ansible-playbook playbooks/site.yml --check --diff
# Single host
ansible-playbook playbooks/site.yml --limit proxfold
# Single role via tag
ansible-playbook playbooks/site.yml --tags zfs
# Combine — single host, single role
ansible-playbook playbooks/site.yml --limit arrstack --tags docker
Secrets¶
Secrets are stored in group_vars/all/vault.yml encrypted with Ansible Vault. To edit:
The vault password is stored locally and excluded from git via .gitignore. See Variables & Vault for the full list of vaulted variables.
Linting & quality gates¶
ansible-lint runs as a pre-commit hook on every commit (.pre-commit-config.yaml). The active profile is set in .ansible-lint.
| Profile | State | Notes |
|---|---|---|
basic |
retired | Initial profile (Phase 3D); flipped to moderate 2026-04-25 |
moderate |
retired | Held briefly on 2026-04-25 between the basic→moderate flip and the production ratchet |
production |
active | Reached 2026-04-25 after the no-changed-when sweep, no-handler citations on inline apt-cache refreshes, risky-shell-pipe rewrite, and FQCN canonicalisation |
Existing rule violations sit in .ansible-lint-ignore so the gate is non-regressive — new violations fail the hook, existing ones are tolerated until intentionally addressed. The current baseline is dominated by var-naming[no-role-prefix] on inventory-scoped variables (see Ansible and Galaxy conventions below).
Regenerate the baseline after a bulk fix sweep:
Don't bypass the hook with --no-verify. If a hook fails, fix the underlying issue or extend the baseline with a citation. The # noqa: <rule> annotation on a task name is preferred for one-off justified exceptions — the rationale stays next to the code (see roles/docker/tasks/main.yml and roles/plex/tasks/main.yml for the inline-not-handler pattern, or roles/nfs/tasks/main.yml for the deliberately-mode-less NFS mount-point dir).
Ansible and Galaxy conventions¶
One ansible-lint convention is deliberately not enforced and lives in the baseline:
var-naming[no-role-prefix]— variables ininventory/host_vars/andinventory/group_vars/aren't role-prefixed because they're inventory-scoped, not role-scoped. ansible-lint's view assumes Galaxy-distributable roles where every variable belongs to one role; in this repo the inventory is the source of truth and a single variable (e.g.zfs_pool) feeds multiple roles by design.
Revisit if the role set is ever broken out for Galaxy distribution.
Handler names follow Title case to match ansible-lint's name[casing] rule (renamed from lowercase across the role set on 2026-04-25). Use the same casing in any notify: reference, since Ansible matches handlers by exact-string.