Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEczyvRjfakkiDNkbZA7PR
6.6 KiB
Homelab Ansible
Playbooks for a Proxmox homelab: Rocky Linux 9 and Ubuntu LXC containers, Ubuntu VMs and the Proxmox host itself. Everything is run unattended from Semaphore.
Layout
ansible.cfg defaults for local runs (Semaphore passes its own inventory/key)
collections/requirements.yml collections Semaphore installs before each run
inventories/all.yml the homelab inventory (groups below)
inventories/new-lxc.yml throwaway inventory for a freshly created container
playbooks/*.yml one self-contained playbook per task, no roles
Inventory groups
| Group | Members | Notes |
|---|---|---|
ubuntu |
Ubuntu containers and VMs | apt plays |
rocky |
Rocky Linux 9 containers | dnf plays |
proxmox |
pve |
the Proxmox host |
lxc |
containers only | subset of ubuntu/rocky |
vms |
QEMU VMs only | subset of ubuntu/rocky, currently empty |
A host may sit in several groups. ansible_host is defined once under the OS
group. critical_services on a host entry feeds service_check.yml.
Placeholders to fill in (grep PLACEHOLDER inventories/all.yml): the Proxmox
address, the first Ubuntu VM, and the service lists for webserver, bot and
storage.
Semaphore
- Inventory type File, path
inventories/all.yml. - Collections in
collections/requirements.ymlare installed automatically. - One task template per playbook. Parameters go in as extra vars (JSON).
Playbooks
| Playbook | Hosts | Purpose / extra vars |
|---|---|---|
ping.yml |
all | connectivity check |
health_check.yml |
all | disk and RAM report, warns above 85 % disk |
update_system.yml |
ubuntu, rocky | apt dist-upgrade / dnf update |
bootstrap_lxc.yml |
ubuntu, rocky | base packages, root SSH key, sshd basics |
add_root_key.yml |
all | add a personal public key to root |
time-region.yml |
ubuntu, rocky | timezone, NTP on VMs, NTP disabled in LXC |
new_lxc.yml |
localhost | create a Rocky container via the Proxmox API |
backup_docker.yml |
docker | rsync compose files and volumes to storage |
proxmox-backup.yml |
proxmox | vzdump selected guests, keep last N |
discover_guests.yml |
proxmox | list every guest on the host and flag those missing from the inventory; adopt=true pushes the Ansible key into unmanaged running containers |
update_pihole_blocklists.yml |
pihole | pihole -up and gravity update |
ssh_harden.yml |
ubuntu, rocky | sshd drop-in (key-only root login) and fail2ban sshd jail. ssh_disable_password_auth, ssh_max_auth_tries |
reboot_check.yml |
ubuntu, rocky | report reboot-required; reboot when do_reboot=true, skipping reboot_exclude (default [docker]) |
auto_updates.yml |
ubuntu, rocky | unattended-upgrades / dnf-automatic for security updates, no auto reboot |
service_check.yml |
ubuntu, rocky | check critical_services per host; restart_stopped=true starts stopped ones |
vm_guest_agent.yml |
vms | install and enable qemu-guest-agent |
user_setup.yml |
ubuntu, rocky | non-root admin with sudo and SSH keys. admin_user, admin_sudo_nopasswd, admin_ssh_keys |
Conventions: FQCN module names, become: true at play level, OS handled by
targeting the ubuntu / rocky groups in separate plays, reachability check
as the first play of anything that touches many hosts, ASCII-only reports.
Notes
ssh_harden.ymlonly disables password login on hosts where root already has an authorized key. It writes/etc/ssh/sshd_config.d/00-ansible.conf, which takes precedence oversshd_configand cloud-init drop-ins.reboot_check.ymlnever reboots hosts inreboot_exclude; the default excludesdockerbecause Semaphore most likely runs there.discover_guests.ymlis the answer to "did I forget to add a container". Run it after creating anything in the Proxmox UI; withadopt=trueit drops the Ansible key into the new container so bootstrap can reach it. Paste the printed snippet into the inventory.auto_updates.ymlandupdate_system.ymloverlap. Use the first for hands-off security patches, the second for full upgrades on demand.
Semaphore schedules
One template per row. Cron times are in Semaphore's own timezone (UTC inside
the container unless TZ is set). Reports run in the morning, anything that
restarts services or reboots runs at night. Semaphore does not chain templates,
so the gaps between related runs are deliberate.
Daily:
| Template | Playbook | Cron | Extra vars |
|---|---|---|---|
| Health check | health_check.yml |
0 7 * * * |
|
| Service check | service_check.yml |
5 7 * * * |
|
| Discover guests | discover_guests.yml |
10 7 * * * |
|
| Reboot check (report) | reboot_check.yml |
15 7 * * * |
|
| Docker backup | backup_docker.yml |
0 2 * * * |
Weekly:
| Template | Playbook | Cron | Extra vars |
|---|---|---|---|
| System update | update_system.yml |
0 3 * * 0 |
|
| Reboot check (reboot) | reboot_check.yml |
30 3 * * 0 |
{"do_reboot": true} |
| Proxmox backup | proxmox-backup.yml |
0 1 * * 6 |
|
| Pi-hole update | update_pihole_blocklists.yml |
0 4 * * 0 |
Monthly (drift check, also run after adding a host):
| Template | Playbook | Cron | Extra vars |
|---|---|---|---|
| SSH hardening | ssh_harden.yml |
0 5 1 * * |
|
| Auto updates | auto_updates.yml |
30 5 1 * * |
On demand only: bootstrap_lxc.yml, add_root_key.yml, time-region.yml,
new_lxc.yml, user_setup.yml, vm_guest_agent.yml, ping.yml.
Before enabling: backup_docker.yml stops every container on the docker host
while it copies volumes. If Semaphore itself runs as a container there, move
it or exclude it from the stop step first.
Adding a new container
- Create it in the Proxmox UI (or with
new_lxc.yml). - Run the Discover guests template with
{"adopt": true}. It pushes the Ansible key into the container and prints an inventory snippet. - Paste the snippet into
inventories/all.ymlunder the group it names, and add the name underlxc(orvms). - Run
bootstrap_lxc.yml, thenssh_harden.ymlandauto_updates.yml.
Local checks
uv venv .venv && uv pip install --python .venv/bin/python ansible ansible-lint yamllint
.venv/bin/ansible-galaxy collection install -r collections/requirements.yml
.venv/bin/ansible-inventory --graph
.venv/bin/ansible-playbook --syntax-check playbooks/*.yml
.venv/bin/yamllint . && .venv/bin/ansible-lint