Automate Proxmox VE with Ansible Full VM Playbooks
Provision Proxmox VMs and LXC containers with Ansible using community.general and API tokens. Get repeatable, zero-touch VM deployments in under 90 seconds.
On this page
Ansible turns a Proxmox node — or a full three-node cluster — into reproducible, version-controlled infrastructure. By the end of this guide you'll have working playbooks that provision VMs and LXC containers on Proxmox VE 9.x using the community.general Ansible collection, a node-level configuration play you can run on every new host, and a structure you can commit to git and replay after a disaster.
Key Takeaways
- Collection to use:
community.general8.x shipsproxmox_kvmandproxmoxmodules covering full VM and LXC lifecycle - Auth method: API tokens (not root password) are the correct approach — scoped, revocable, and logged in the Proxmox audit trail
- Idempotency: Both modules are idempotent; re-running a playbook will not clone duplicate VMs
- Cloud-Init VMs: Combine
proxmox_kvmwith a Cloud-Init template for zero-touch VM deployment in under 90 seconds on NVMe storage - Node config: A separate OS-level play handles repo configuration, user accounts, and sysctl tuning on every new host automatically
Why Ansible Beats Clicking Through the Proxmox UI
The Proxmox web UI is excellent for one-off tasks — but the moment you're standing up a third K3s node or rebuilding a host after drive failure, manual UI work becomes a liability. You miss a CPU setting, forget to enable the QEMU guest agent, or pick the wrong storage pool. Ansible makes that impossible by turning your infrastructure into a YAML file you commit to git.
The practical payoff: I rebuilt an entire three-node homelab from scratch in about 20 minutes after a botched ZFS experiment, running a single ansible-playbook site.yml. Every VM came up with the right CPU topology, the right network bridge, and Cloud-Init pre-populated with my SSH keys.
That said, Ansible for Proxmox is not Terraform. It doesn't maintain remote state, so if you delete a VM manually and re-run the playbook, Ansible will try to create it again. For homelab scale this is fine. For production, combining Ansible with Terraform's Proxmox provider gives you both declarative provisioning and state tracking.
Prerequisites: Modules, API Tokens, and Inventory Setup
Installing the community.general Collection
ansible-galaxy collection install community.general
You need community.general 8.0.0 or later for the proxmox_kvm and proxmox (LXC) modules. Check your installed version:
ansible-galaxy collection list | grep community.general
Install the Python dependency on the machine running Ansible — not the Proxmox host itself:
pip install proxmoxer requests
Creating a Proxmox API Token
Never store your root password in a playbook. Create a dedicated API token instead:
pveum user add ansible@pve --comment "Ansible automation"
pveum role add AnsibleRole --privs "Datastore.AllocateSpace,Datastore.Audit,Pool.Allocate,Sys.Audit,Sys.Console,Sys.Modify,VM.Allocate,VM.Audit,VM.Clone,VM.Config.CDROM,VM.Config.CPU,VM.Config.Cloudinit,VM.Config.Disk,VM.Config.HWType,VM.Config.Memory,VM.Config.Network,VM.Config.Options,VM.Migrate,VM.Monitor,VM.PowerMgmt,SDN.Use"
pveum aclmod / -user ansible@pve -role AnsibleRole
pveum user token add ansible@pve automation --privsep 0
That last command outputs the token secret — copy it now, you won't see it again. Store it in Ansible Vault immediately:
ansible-vault create group_vars/all/vault.yml
# vault.yml (encrypted at rest)
vault_proxmox_token_id: "ansible@pve!automation"
vault_proxmox_token_secret: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
Inventory Structure
A minimal inventory for a single-node setup:
[proxmox]
pve01 ansible_host=192.168.1.10
[proxmox:vars]
ansible_user=root
ansible_ssh_private_key_file=~/.ssh/id_ed25519
For the API-based Proxmox modules, ansible_host is used as api_host. Ansible calls the Proxmox REST API from your workstation — it does not SSH into the node to run proxmox_kvm tasks. SSH is only used for OS-level configuration plays.
How to Provision VMs with the proxmox_kvm Module
This playbook creates a Ubuntu 24.04 VM from a Cloud-Init template. It assumes you have a Cloud-Init-ready template at VMID 9000 — setting up that base template is part of building a full private cloud on Proxmox.
# playbooks/create_vm.yml
---
- name: Provision Ubuntu VM on Proxmox
hosts: localhost
gather_facts: false
vars:
api_host: "192.168.1.10"
api_token_id: "{{ vault_proxmox_token_id }}"
api_token_secret: "{{ vault_proxmox_token_secret }}"
node: "pve01"
template_vmid: 9000
new_vmid: 101
vm_name: "ubuntu-worker-01"
vm_memory: 4096
vm_cores: 2
storage: "local-lvm"
ipconfig: "ip=192.168.1.101/24,gw=192.168.1.1"
ssh_keys: "ssh-ed25519 AAAAC3Nz your@key"
tasks:
- name: Clone VM from Cloud-Init template
community.general.proxmox_kvm:
api_host: "{{ api_host }}"
api_token_id: "{{ api_token_id }}"
api_token_secret: "{{ api_token_secret }}"
node: "{{ node }}"
name: "{{ vm_name }}"
vmid: "{{ new_vmid }}"
clone: "{{ template_vmid }}"
full: true
storage: "{{ storage }}"
timeout: 300
state: present
- name: Configure VM hardware and Cloud-Init
community.general.proxmox_kvm:
api_host: "{{ api_host }}"
api_token_id: "{{ api_token_id }}"
api_token_secret: "{{ api_token_secret }}"
node: "{{ node }}"
vmid: "{{ new_vmid }}"
memory: "{{ vm_memory }}"
cores: "{{ vm_cores }}"
ipconfig:
ipconfig0: "{{ ipconfig }}"
sshkeys: "{{ ssh_keys }}"
ciuser: "ubuntu"
update: true
- name: Start VM
community.general.proxmox_kvm:
api_host: "{{ api_host }}"
api_token_id: "{{ api_token_id }}"
api_token_secret: "{{ api_token_secret }}"
node: "{{ node }}"
vmid: "{{ new_vmid }}"
state: started
Run it:
ansible-playbook playbooks/create_vm.yml --vault-password-file ~/.vault_pass
On NVMe-to-NVMe (local-lvm to local-lvm on the same node), a full 32 GB clone completes in under 90 seconds. On HDD-backed storage, plan for 3-5 minutes and set timeout accordingly.
Deploying Multiple VMs with a Loop
The real payoff comes when you define your fleet in a variables file and loop over it:
# vars/vms.yml
vms:
- name: k3s-master-01
vmid: 101
ip: "192.168.1.101"
memory: 4096
cores: 2
- name: k3s-worker-01
vmid: 102
ip: "192.168.1.102"
memory: 8192
cores: 4
- name: k3s-worker-02
vmid: 103
ip: "192.168.1.103"
memory: 8192
cores: 4
- name: Clone and configure all VMs
community.general.proxmox_kvm:
api_host: "{{ api_host }}"
api_token_id: "{{ api_token_id }}"
api_token_secret: "{{ api_token_secret }}"
node: "{{ node }}"
name: "{{ item.name }}"
vmid: "{{ item.vmid }}"
clone: "{{ template_vmid }}"
full: true
storage: "{{ storage }}"
memory: "{{ item.memory }}"
cores: "{{ item.cores }}"
ipconfig:
ipconfig0: "ip={{ item.ip }}/24,gw=192.168.1.1"
sshkeys: "{{ ssh_keys }}"
ciuser: ubuntu
state: present
timeout: 300
loop: "{{ vms }}"
Three K3s nodes provisioned with one loop. Once they're up, the K3s Kubernetes cluster setup guide on Proxmox picks up exactly where this leaves off.
Provisioning LXC Containers with the proxmox Module
The community.general.proxmox module handles LXC lifecycle. The interface differs from proxmox_kvm — you specify a template from a storage pool rather than cloning a VMID.
First, download the template on the Proxmox node:
pveam update
pveam available | grep debian-12
pveam download local debian-12-standard_12.7-1_amd64.tar.zst
Then the Ansible tasks:
- name: Create Debian 12 LXC container
community.general.proxmox:
api_host: "{{ api_host }}"
api_token_id: "{{ api_token_id }}"
api_token_secret: "{{ api_token_secret }}"
node: "{{ node }}"
vmid: 200
hostname: "monitoring-01"
ostemplate: "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst"
storage: local-lvm
disk: 8
memory: 1024
swap: 512
cores: 2
netif:
net0: "name=eth0,bridge=vmbr0,ip=192.168.1.200/24,gw=192.168.1.1"
password: "{{ vault_lxc_root_password }}"
pubkey: "{{ ssh_keys }}"
unprivileged: true
features:
- nesting=1
state: present
- name: Start LXC container
community.general.proxmox:
api_host: "{{ api_host }}"
api_token_id: "{{ api_token_id }}"
api_token_secret: "{{ api_token_secret }}"
node: "{{ node }}"
vmid: 200
state: started
Setting unprivileged: true and nesting=1 is the right default for containers that will run Docker — running Docker inside LXC containers on Proxmox covers the additional lxc.apparmor.profile and keyctl settings you'll apply after the container first starts.
Gotcha: If your Proxmox node uses a self-signed certificate, proxmoxer will refuse the API connection with a verification error. Install a proper TLS certificate or pass validate_certs: false in each module call for automation running entirely on a trusted internal network.
Automating Node-Level Configuration Over SSH
Ansible shines at the OS layer too. This play handles the common Proxmox post-install tasks every node needs:
# playbooks/configure_node.yml
---
- name: Configure Proxmox node base settings
hosts: proxmox
become: false
tasks:
- name: Disable enterprise repo
ansible.builtin.copy:
dest: /etc/apt/sources.list.d/pve-enterprise.list
content: |
# deb https://enterprise.proxmox.com/debian/pve bookworm pve-enterprise
- name: Add no-subscription repo
ansible.builtin.apt_repository:
repo: "deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription"
state: present
filename: pve-no-subscription
- name: Update all packages
ansible.builtin.apt:
update_cache: true
upgrade: dist
- name: Set swappiness for VM host
ansible.posix.sysctl:
name: vm.swappiness
value: "10"
sysctl_file: /etc/sysctl.d/99-proxmox.conf
reload: true
- name: Install QEMU guest agent
ansible.builtin.apt:
name: qemu-guest-agent
state: present
For SSH hardening, fail2ban, and Proxmox firewall rules, keep a separate harden_node.yml. Running it via Ansible means every new node gets an identical security baseline automatically — exactly the defense-in-depth approach that hardening Proxmox with firewall, fail2ban, and SSH config describes.
Structuring a site.yml That Ties Everything Together
Once you have individual playbooks, a top-level site.yml composes them in the correct order:
# site.yml
---
- import_playbook: playbooks/configure_node.yml
- import_playbook: playbooks/harden_node.yml
- import_playbook: playbooks/create_vms.yml
- import_playbook: playbooks/create_lxcs.yml
Run order matters. Configure and harden the node before creating workloads. If the node play reconfigures network bridges, a VM created before that step completes will start with no network interface.
# Dry run with diff output first
ansible-playbook site.yml --check --diff --vault-password-file ~/.vault_pass
# Apply
ansible-playbook site.yml --vault-password-file ~/.vault_pass
Common Pitfalls to Avoid
VMID conflicts: If a playbook targets a VMID already in use, proxmox_kvm fails with a confusing API error rather than a clear message. Check pvesh get /nodes/pve01/qemu before assigning VMIDs in automation, or reserve a dedicated range above 200 exclusively for Ansible-managed workloads.
Clone timeout on slow storage: The default timeout for proxmox_kvm is 30 seconds. A full clone to HDD-backed storage will time out and leave a partial VM. Set timeout: 300 as a minimum — even NVMe-to-NVMe can push past 90 seconds for a 100 GB disk.
SSH host key collisions: When a VM is rebuilt with the same IP, Ansible will refuse SSH because the host key changed. Add this to ansible.cfg:
[defaults]
host_key_checking = False
Or use the known_hosts module to explicitly clear stale entries before connecting.
API privilege scope: The role above is broad — intentionally so for a homelab. For production, the minimum set for VM and LXC management is: VM.Allocate, VM.Config.CPU, VM.Config.Memory, VM.Config.Disk, VM.Config.Network, VM.Config.Cloudinit, VM.PowerMgmt, Datastore.AllocateSpace, Datastore.Audit, SDN.Use.
Proxmox VE 9.1 and community.general 9.0: The proxmox_kvm module gained scsi_discard support and improved Cloud-Init disk handling in community.general 9.0. If you're on Proxmox VE 9.1 and see unexpected disk configuration behavior, upgrade the collection before debugging your playbook.
Conclusion
With these playbooks committed to git, standing up a new VM or LXC container on Proxmox takes one command and under two minutes — no UI clicks, no config drift, no forgotten settings. The logical next step is adding VLAN and bridge configuration to your node-level play (Proxmox's ifupdown2 config in /etc/network/interfaces maps cleanly to Ansible's template module), then tagging your VMs with Proxmox pools so you can filter by environment in the dashboard. From there, your infrastructure is a pull request.