Automate Your Homelab with Ansible and Proxmox: Complete Guide
Why Homelabs Are Perfect for Ansible Practice
If you want to get genuinely good at Ansible, you need an environment where you can break things without consequences. A homelab running Proxmox VE gives you exactly that:
- Zero blast radius - Destroy and rebuild VMs at will; nobody pages you at 3 AM
- Real infrastructure patterns - Provisioning, inventory management, configuration drift, backups — the same problems enterprises face, at miniature scale
- Free API access - Proxmox VE ships with a full REST API out of the box, no license required
- Rapid iteration - Clone an LXC container in seconds, test your playbook, tear it down, repeat
- Portfolio material - "I automated my entire homelab lifecycle with Ansible" is a great interview story
Manually clicking through the Proxmox web UI to create your fifth Ubuntu container is exactly the kind of repetitive toil Ansible was built to eliminate. By the end of this guide, you'll provision and configure infrastructure with a single command.
The Proxmox Ansible Modules
Proxmox support in Ansible historically lived in community.general. The dedicated community.proxmox collection now houses the actively developed modules, so prefer it for new work:
# Install the collections
ansible-galaxy collection install community.proxmox
ansible-galaxy collection install community.general
# The modules talk to the Proxmox API via the proxmoxer library
pip install proxmoxer requests
The modules you'll use most:
community.proxmox.proxmox- Create and manage LXC containerscommunity.proxmox.proxmox_kvm- Create and manage QEMU/KVM virtual machinescommunity.proxmox.proxmox_snap- Snapshot managementcommunity.proxmox.proxmox_template- Upload/manage OS templates and ISOscommunity.proxmox.proxmox_vm_info- Query VM factscommunity.proxmox.proxmox(inventory plugin) - Dynamic inventory straight from the API
Note: If you're on an older Ansible install, the same modules exist under community.general.proxmox* names. The examples below use community.proxmox; swap the namespace if needed.
Setting Up an API Token on Proxmox
Never automate with your root password. Proxmox API tokens give you revocable, scoped credentials that work cleanly with Ansible.
1. Create a Dedicated User and Token
Run these on your Proxmox host (or via Datacenter → Permissions in the UI):
# Create an automation user in the PVE realm
pveum user add ansible@pve --comment "Ansible automation"
# Create an API token for that user
# --privsep 0 means the token inherits the user's permissions
pveum user token add ansible@pve automation --privsep 0
# Grant the user the permissions it needs
pveum acl modify / --users ansible@pve --roles PVEVMAdmin,PVEDatastoreUser,PVEPoolAdmin
The token add command prints the secret once — copy it immediately. Your credentials are now:
- Token ID:
ansible@pve!automation - Token Secret: the UUID that was printed
2. Store Credentials with Ansible Vault
# Create an encrypted vars file
ansible-vault create group_vars/all/vault.yml
---
vault_proxmox_host: "192.168.1.10"
vault_proxmox_user: "ansible@pve"
vault_proxmox_token_id: "automation"
vault_proxmox_token_secret: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
Then reference them from an unencrypted vars file so your playbooks stay readable:
# group_vars/all/main.yml
---
proxmox_api_host: "{{ vault_proxmox_host }}"
proxmox_api_user: "{{ vault_proxmox_user }}"
proxmox_api_token_id: "{{ vault_proxmox_token_id }}"
proxmox_api_token_secret: "{{ vault_proxmox_token_secret }}"
proxmox_node: "pve"
Dynamic Inventory with the Proxmox Plugin
Static inventory files rot the moment you clone a new container. The Proxmox inventory plugin queries the API at runtime, so every VM and container is automatically in inventory — with groups built from status, tags, and pools.
Create inventory/proxmox.yml (the filename must end in proxmox.yml or proxmox.yaml):
---
plugin: community.proxmox.proxmox
url: https://192.168.1.10:8006
user: ansible@pve
token_id: automation
token_secret: "{{ lookup('env', 'PROXMOX_TOKEN_SECRET') }}"
validate_certs: false
# Gather facts about each guest (IP addresses, config, etc.)
want_facts: true
# Build groups automatically
group_prefix: proxmox_
want_proxmox_nodes_ansible_host: false
# Group guests by their Proxmox tags (e.g. tag "docker" -> group proxmox_docker)
keyed_groups:
- key: proxmox_tags_parsed
separator: ""
prefix: proxmox_tag
- key: proxmox_status
separator: ""
prefix: proxmox_status
# Only manage running guests, and use the guest's first IP as ansible_host
filters:
- proxmox_status == "running"
compose:
ansible_host: proxmox_ipconfig0.ip | default(proxmox_net0.ip) | default(proxmox_name) | regex_replace('/.*', '')
Test it:
export PROXMOX_TOKEN_SECRET="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
# List everything the plugin discovered
ansible-inventory -i inventory/proxmox.yml --list
# See the group tree
ansible-inventory -i inventory/proxmox.yml --graph
Now a container tagged docker in the Proxmox UI automatically lands in the proxmox_tag_docker group — tag-driven configuration with zero inventory editing.
Cloning LXC Containers from Templates
LXC containers are the homelab workhorse: they boot in seconds and use a fraction of a VM's resources. First download a template on the Proxmox host:
pveam update
pveam available | grep ubuntu-24
pveam download local ubuntu-24.04-standard_24.04-2_amd64.tar.zst
Then create containers with the proxmox module:
---
- name: Provision LXC container
hosts: localhost
gather_facts: false
tasks:
- name: Create Ubuntu 24.04 container
community.proxmox.proxmox:
api_host: "{{ proxmox_api_host }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ proxmox_api_token_id }}"
api_token_secret: "{{ proxmox_api_token_secret }}"
node: "{{ proxmox_node }}"
vmid: 201
hostname: web01
ostemplate: "local:vztmpl/ubuntu-24.04-standard_24.04-2_amd64.tar.zst"
storage: local-lvm
cores: 2
memory: 2048
swap: 512
disk: "local-lvm:8"
netif:
net0: "name=eth0,bridge=vmbr0,ip=192.168.1.201/24,gw=192.168.1.1"
pubkey: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
unprivileged: true
onboot: true
tags: "web,ansible-managed"
state: present
- name: Start the container
community.proxmox.proxmox:
api_host: "{{ proxmox_api_host }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ proxmox_api_token_id }}"
api_token_secret: "{{ proxmox_api_token_secret }}"
vmid: 201
state: started
Cloning VMs with proxmox_kvm
For full virtual machines, the best workflow is a cloud-init template. Build one once (Ubuntu cloud image + qm template), then clone it endlessly:
---
- name: Clone VM from cloud-init template
hosts: localhost
gather_facts: false
tasks:
- name: Clone template to new VM
community.proxmox.proxmox_kvm:
api_host: "{{ proxmox_api_host }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ proxmox_api_token_id }}"
api_token_secret: "{{ proxmox_api_token_secret }}"
node: "{{ proxmox_node }}"
clone: ubuntu-2404-template # name of the template
name: k8s-node01
newid: 301
full: true # full clone, not linked
storage: local-lvm
timeout: 300
- name: Configure resources and cloud-init
community.proxmox.proxmox_kvm:
api_host: "{{ proxmox_api_host }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ proxmox_api_token_id }}"
api_token_secret: "{{ proxmox_api_token_secret }}"
node: "{{ proxmox_node }}"
vmid: 301
cores: 4
memory: 8192
ciuser: ansible
sshkeys: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
ipconfig:
ipconfig0: "ip=192.168.1.31/24,gw=192.168.1.1"
tags: "k8s,ansible-managed"
update: true
- name: Start the VM
community.proxmox.proxmox_kvm:
api_host: "{{ proxmox_api_host }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ proxmox_api_token_id }}"
api_token_secret: "{{ proxmox_api_token_secret }}"
node: "{{ proxmox_node }}"
vmid: 301
state: started
Post-Provision Configuration
Provisioning is only half the job. Once guests are up, a second play (running against the guests themselves over SSH) handles users, keys, packages, and Docker:
---
- name: Baseline configuration for all managed guests
hosts: proxmox_tag_ansible_managed
become: true
vars:
admin_user: homelab
base_packages:
- vim
- htop
- curl
- git
- unattended-upgrades
tasks:
- name: Create admin user
ansible.builtin.user:
name: "{{ admin_user }}"
groups: sudo
shell: /bin/bash
create_home: true
- name: Deploy SSH key
ansible.posix.authorized_key:
user: "{{ admin_user }}"
key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
- name: Passwordless sudo for admin user
ansible.builtin.copy:
dest: "/etc/sudoers.d/{{ admin_user }}"
content: "{{ admin_user }} ALL=(ALL) NOPASSWD:ALL\n"
mode: "0440"
validate: "visudo -cf %s"
- name: Install base packages
ansible.builtin.apt:
name: "{{ base_packages }}"
state: present
update_cache: true
cache_valid_time: 3600
- name: Install Docker on tagged guests
hosts: proxmox_tag_docker
become: true
tasks:
- name: Install Docker via geerlingguy role
ansible.builtin.include_role:
name: geerlingguy.docker
vars:
docker_users:
- homelab
Notice how the dynamic inventory groups (proxmox_tag_ansible_managed, proxmox_tag_docker) drive everything. Want Docker on a container? Add the docker tag in Proxmox and re-run the playbook.
Full Worked Example: Provision and Configure 3 Containers
Let's put it all together: one playbook that creates three containers from a loop, waits for SSH, refreshes inventory, and configures them end to end.
Project Layout
homelab/
├── ansible.cfg
├── inventory/
│ └── proxmox.yml # dynamic inventory plugin config
├── group_vars/
│ └── all/
│ ├── main.yml
│ └── vault.yml # encrypted credentials
└── site.yml
site.yml
---
# ============================================
# Play 1: Provision containers on Proxmox
# ============================================
- name: Provision homelab containers
hosts: localhost
gather_facts: false
vars:
containers:
- { vmid: 210, hostname: web01, ip: "192.168.1.210", tags: "web,ansible-managed" }
- { vmid: 211, hostname: web02, ip: "192.168.1.211", tags: "web,ansible-managed" }
- { vmid: 212, hostname: app01, ip: "192.168.1.212", tags: "docker,ansible-managed" }
tasks:
- name: Create containers
community.proxmox.proxmox:
api_host: "{{ proxmox_api_host }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ proxmox_api_token_id }}"
api_token_secret: "{{ proxmox_api_token_secret }}"
node: "{{ proxmox_node }}"
vmid: "{{ item.vmid }}"
hostname: "{{ item.hostname }}"
ostemplate: "local:vztmpl/ubuntu-24.04-standard_24.04-2_amd64.tar.zst"
storage: local-lvm
cores: 2
memory: 1024
disk: "local-lvm:8"
netif:
net0: "name=eth0,bridge=vmbr0,ip={{ item.ip }}/24,gw=192.168.1.1"
pubkey: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
unprivileged: true
onboot: true
tags: "{{ item.tags }}"
state: present
loop: "{{ containers }}"
- name: Start containers
community.proxmox.proxmox:
api_host: "{{ proxmox_api_host }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ proxmox_api_token_id }}"
api_token_secret: "{{ proxmox_api_token_secret }}"
vmid: "{{ item.vmid }}"
state: started
loop: "{{ containers }}"
- name: Wait for SSH on each container
ansible.builtin.wait_for:
host: "{{ item.ip }}"
port: 22
timeout: 120
loop: "{{ containers }}"
- name: Refresh dynamic inventory so new guests are visible
ansible.builtin.meta: refresh_inventory
# ============================================
# Play 2: Baseline every managed guest
# ============================================
- name: Baseline configuration
hosts: proxmox_tag_ansible_managed
become: true
remote_user: root
tasks:
- name: Install base packages
ansible.builtin.apt:
name: [vim, htop, curl, git, python3]
state: present
update_cache: true
- name: Create homelab user with SSH key
ansible.builtin.user:
name: homelab
groups: sudo
shell: /bin/bash
- name: Deploy authorized key
ansible.posix.authorized_key:
user: homelab
key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
# ============================================
# Play 3: Role-specific configuration
# ============================================
- name: Configure web servers
hosts: proxmox_tag_web
become: true
remote_user: root
tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Ensure nginx is running
ansible.builtin.service:
name: nginx
state: started
enabled: true
- name: Configure Docker hosts
hosts: proxmox_tag_docker
become: true
remote_user: root
roles:
- geerlingguy.docker
Run the whole thing:
ansible-playbook -i inventory/proxmox.yml site.yml --ask-vault-pass
One command: three containers created, started, baselined, and configured for their roles. Run it again and Ansible reports ok everywhere — nothing changes because everything already matches the desired state.
Snapshot and Backup Automation
Snapshots before risky changes are a homelab superpower. Automate them with proxmox_snap:
---
- name: Snapshot before maintenance
hosts: localhost
gather_facts: false
vars:
target_vmids: [210, 211, 212]
snap_name: "pre_maintenance_{{ lookup('pipe', 'date +%Y%m%d_%H%M') }}"
tasks:
- name: Create snapshots
community.proxmox.proxmox_snap:
api_host: "{{ proxmox_api_host }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ proxmox_api_token_id }}"
api_token_secret: "{{ proxmox_api_token_secret }}"
vmid: "{{ item }}"
snapname: "{{ snap_name }}"
description: "Automated pre-maintenance snapshot"
state: present
loop: "{{ target_vmids }}"
- name: Roll back if needed (state: rollback)
ansible.builtin.debug:
msg: "To roll back: set state: rollback with the same snapname"
For real backups, drive vzdump on the Proxmox host itself and schedule it with cron:
---
- name: Configure scheduled backups on the Proxmox host
hosts: proxmox_hosts
become: true
tasks:
- name: Nightly vzdump of all managed guests
ansible.builtin.cron:
name: "vzdump homelab guests"
minute: "0"
hour: "2"
job: "vzdump 210 211 212 --storage backup-nas --mode snapshot --compress zstd --quiet 1"
Tips and Gotchas
Idempotency with the Proxmox API
- The
proxmoxandproxmox_kvmmodules match guests by vmid and/or name — always pin both so re-runs find the existing guest instead of failing or duplicating proxmox_kvmonly applies config changes whenupdate: trueis set; creation options alone are ignored for existing VMs- Clone tasks are not retry-safe if a previous run half-finished — check with
proxmox_vm_infoor guard with a preliminary query when building robust pipelines - Run everything twice as a test: the second run should be all
ok, nochanged
VMID Management
- Reserve ranges: e.g. 100-199 for templates, 200-299 for LXC, 300-399 for VMs — encode them in your vars file so playbooks stay readable
- Never let the API auto-assign IDs in automation; explicit
vmidvalues keep runs deterministic and idempotent - Keep the vmid-to-hostname mapping in a single data structure (like the
containerslist above) — it doubles as documentation
General Advice
- Set
validate_certs: falseonly for self-signed homelab certs — or better, deploy a real internal CA with Ansible as your next project - Tags are your inventory. Establish a tagging convention early (
ansible-managed, role tags, environment tags) - Increase module
timeoutfor full clones on slow storage; the default can expire mid-clone - Keep the token secret out of git: Vault-encrypt it or inject it via environment variable
Next Steps
Once provisioning and configuration are automated, keep climbing:
- Templates as code - Automate building your cloud-init VM templates with Packer or a dedicated Ansible playbook
- GitOps your homelab - Put everything in git and trigger playbook runs from CI (or AWX/Ansible Automation Platform)
- Monitoring - Deploy Prometheus and Grafana with Galaxy roles onto containers your playbook creates
- Kubernetes - Clone three VMs and bootstrap a k3s cluster — see our Kubernetes topic
- Vault everything - Level up secret handling with our Ansible Vault guide
The pattern you've built here — API-driven provisioning, tag-based dynamic inventory, layered configuration plays — is exactly how production cloud automation works. Your homelab is just the rehearsal space.
Pro Tip
Add a destroy.yml playbook that stops and removes guests by vmid (state: absent). Being able to tear down and rebuild your entire lab in minutes is the ultimate test that your automation is complete — and the ultimate confidence booster.