Homelab

Automate Your Homelab with Ansible and Proxmox: Complete Guide

Teach me Ansible | 2026-08-05 | 24 min read

Your homelab is the perfect Ansible training ground. Learn how to fully automate a Proxmox VE environment — API tokens, dynamic inventory, cloning containers and VMs from templates, post-provision configuration, and snapshot automation — all with real, working playbooks.

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 containers
  • community.proxmox.proxmox_kvm - Create and manage QEMU/KVM virtual machines
  • community.proxmox.proxmox_snap - Snapshot management
  • community.proxmox.proxmox_template - Upload/manage OS templates and ISOs
  • community.proxmox.proxmox_vm_info - Query VM facts
  • community.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 proxmox and proxmox_kvm modules match guests by vmid and/or name — always pin both so re-runs find the existing guest instead of failing or duplicating
  • proxmox_kvm only applies config changes when update: true is 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_info or guard with a preliminary query when building robust pipelines
  • Run everything twice as a test: the second run should be all ok, no changed

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 vmid values keep runs deterministic and idempotent
  • Keep the vmid-to-hostname mapping in a single data structure (like the containers list above) — it doubles as documentation

General Advice

  • Set validate_certs: false only 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 timeout for 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.