Automating Proxmox VMs with Ansible: From Manual Chaos to Orchestrated Bliss 🚀
Remember the good old days when you’d manually SSH into each VM to update packages, configure services, or install new software? Yeah, me neither. That’s why I automated my entire Proxmox homelab with Ansible—and now I’m here to show you how to do the same.
In this guide, I’ll walk you through transforming your manual VM management into a streamlined, repeatable automation pipeline. We’ll cover everything from SSH key setup to creating playbooks that can provision entire application stacks.
Why Automate? (The “Before” Horror Story)
Let me paint you a picture of my pre-automation life:
- 3 VMs → 15 minutes of manual updates
- 5 VMs → 30 minutes of repetitive SSH sessions
- 10 VMs → “I’ll do it tomorrow” (spoiler: never happened)
Sound familiar? Here’s what I was dealing with:
# The old way (don't do this)
ssh user@vm1 "sudo apt update && sudo apt upgrade -y"
ssh user@vm2 "sudo apt update && sudo apt upgrade -y"
ssh user@vm3 "sudo apt update && sudo apt upgrade -y"
# ... repeat until your coffee gets cold
The problem: This approach doesn’t scale. At all. Plus, it’s error-prone, time-consuming, and frankly, boring.
The solution: Ansible automation that lets you run one command to update all VMs simultaneously.
The Foundation: SSH Keys (Your New Best Friends)
Before we dive into Ansible magic, we need to set up SSH keys. Think of this as the “trust handshake” between your control machine and your VMs.
Step 1: Generate Your SSH Key
# Generate a new SSH key (I prefer ed25519 for security + speed)
ssh-keygen -t ed25519 -C "your-email@example.com"
# Or if you want to be extra secure with a passphrase:
ssh-keygen -t ed25519 -C "your-email@example.com" -f ~/.ssh/id_ed25519
Pro Tip: If you’re using this for automation, you might want to skip the passphrase for convenience. Just make sure your private key is secure and your network is trusted.
Step 2: Push Your Key to All VMs
Here’s where Ansible comes in handy. Create your first playbook:
playbooks/setup-ssh-keys.yml
---
- name: Setup SSH keys for all VMs
hosts: all
become: true
tasks:
- name: Ensure .ssh directory exists
file:
path: /home/{{ ansible_user }}/.ssh
state: directory
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: '0700'
- name: Add SSH public key to authorized_keys
authorized_key:
user: "{{ ansible_user }}"
state: present
key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
Run it with:
ansible-playbook -i inventory.ini playbooks/setup-ssh-keys.yml --ask-pass
What this does: Copies your public key to all VMs, enabling passwordless SSH (and Ansible) access.
Your Ansible Inventory: The Command Center
Think of your inventory as the “phone book” for your VMs. It tells Ansible where to find your hosts and how to connect to them.
inventory.ini
[proxmox_vms]
vm1 ansible_host=192.168.1.10 ansible_user=ubuntu
vm2 ansible_host=192.168.1.11 ansible_user=ubuntu
vm3 ansible_host=192.168.1.12 ansible_user=ubuntu
[webservers]
vm1
vm2
[dbservers]
vm3
[all:vars]
ansible_python_interpreter=/usr/bin/python3
Pro Tip: Group your VMs logically. You can target specific groups like webservers
or dbservers
in your playbooks.
Essential Playbooks for Every Homelab
Now let’s create some playbooks that you’ll actually use. These are battle-tested and ready for production.
1. The “Keep Everything Updated” Playbook
playbooks/update-all.yml
---
- name: Update all VMs
hosts: all
become: true
tasks:
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
- name: Upgrade all packages
apt:
upgrade: yes
autoremove: yes
- name: Install security updates
apt:
upgrade: dist
autoremove: yes
Run it with:
ansible-playbook -i inventory.ini playbooks/update-all.yml
Result: All your VMs get updated in parallel. What used to take 30 minutes now takes 2 minutes.
2. The “Secure by Default” Playbook
playbooks/harden-ssh.yml
---
- name: Harden SSH configuration
hosts: all
become: true
tasks:
- name: Backup original SSH config
copy:
src: /etc/ssh/sshd_config
dest: /etc/ssh/sshd_config.backup
remote_src: yes
- name: Disable password authentication
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PasswordAuthentication'
line: 'PasswordAuthentication no'
state: present
- name: Disable root login
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PermitRootLogin'
line: 'PermitRootLogin no'
state: present
- name: Enable public key authentication
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PubkeyAuthentication'
line: 'PubkeyAuthentication yes'
state: present
- name: Set SSH port (optional - change 22 to your preferred port)
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?Port'
line: 'Port 22'
state: present
- name: Restart SSH service
service:
name: ssh
state: restarted
⚠️ Warning: Run this after you’ve successfully set up SSH keys, or you’ll lock yourself out!
3. The “Install My Favorite Tools” Playbook
playbooks/install-tools.yml
---
- name: Install essential tools
hosts: all
become: true
tasks:
- name: Install common packages
apt:
name:
- htop
- curl
- wget
- git
- vim
- net-tools
- tree
- unzip
- fail2ban
- ufw
state: present
update_cache: yes
- name: Configure fail2ban
service:
name: fail2ban
state: started
enabled: yes
- name: Enable UFW firewall
ufw:
state: enabled
policy: deny
direction: incoming
rule: allow
port: ssh
proto: tcp
4. The “Monitor Everything” Playbook
playbooks/setup-monitoring.yml
---
- name: Setup basic monitoring
hosts: all
become: true
tasks:
- name: Install monitoring tools
apt:
name:
- htop
- iotop
- nethogs
- nload
state: present
- name: Create monitoring script
copy:
dest: /usr/local/bin/system-check.sh
content: |
#!/bin/bash
echo "=== System Status ==="
echo "Uptime: $(uptime)"
echo "Memory: $(free -h | grep Mem)"
echo "Disk: $(df -h / | tail -1)"
echo "Load: $(cat /proc/loadavg)"
mode: '0755'
- name: Add to crontab for regular checks
cron:
name: "System monitoring"
minute: "*/5"
job: "/usr/local/bin/system-check.sh >> /var/log/system-check.log 2>&1"
Advanced: VM Provisioning with Ansible
Now let’s get fancy. Here’s how to provision a complete application stack:
Web Server Stack Playbook
playbooks/provision-webserver.yml
---
- name: Provision web server
hosts: webservers
become: true
tasks:
- name: Install nginx
apt:
name: nginx
state: present
update_cache: yes
- name: Start and enable nginx
service:
name: nginx
state: started
enabled: yes
- name: Configure nginx
template:
src: templates/nginx.conf.j2
dest: /etc/nginx/sites-available/default
backup: yes
notify: restart nginx
- name: Install PHP (if needed)
apt:
name:
- php-fpm
- php-mysql
- php-curl
state: present
- name: Start PHP-FPM
service:
name: php8.1-fpm
state: started
enabled: yes
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
Database Server Playbook
playbooks/provision-database.yml
---
- name: Provision database server
hosts: dbservers
become: true
tasks:
- name: Install MySQL
apt:
name: mysql-server
state: present
update_cache: yes
- name: Start MySQL
service:
name: mysql
state: started
enabled: yes
- name: Secure MySQL installation
mysql_user:
name: root
host: localhost
password: "{{ mysql_root_password }}"
login_unix_socket: /var/run/mysqld/mysqld.sock
state: present
- name: Create application database
mysql_db:
name: "{{ db_name }}"
state: present
login_user: root
login_password: "{{ mysql_root_password }}"
- name: Create application user
mysql_user:
name: "{{ db_user }}"
password: "{{ db_password }}"
priv: "{{ db_name }}.*:ALL"
host: '%'
state: present
login_user: root
login_password: "{{ mysql_root_password }}"
Pro Tips and Best Practices
1. Use Variables for Flexibility
Create a group_vars/all.yml
file:
---
# Database configuration
db_name: myapp
db_user: appuser
db_password: "{{ vault_db_password }}"
# Application settings
app_port: 8080
app_env: production
# Security settings
ssh_port: 22
fail2ban_enabled: yes
2. Implement Idempotency
Always write playbooks that can be run multiple times safely:
- name: Ensure user exists
user:
name: appuser
shell: /bin/bash
home: /home/appuser
state: present
3. Use Tags for Selective Execution
- name: Update system packages
apt:
upgrade: yes
tags: update
- name: Configure firewall
ufw:
rule: allow
port: "{{ item }}"
loop: [22, 80, 443]
tags: security
Run specific tasks:
ansible-playbook -i inventory.ini playbooks/main.yml --tags "update,security"
4. Implement Error Handling
- name: Install package with error handling
block:
- name: Install nginx
apt:
name: nginx
state: present
rescue:
- name: Log error
debug:
msg: "Failed to install nginx on {{ inventory_hostname }}"
- name: Send notification
mail:
host: localhost
to: admin@example.com
subject: "Ansible failure on {{ inventory_hostname }}"
body: "Package installation failed"
The “After” Bliss
Here’s what my automation workflow looks like now:
# Update all VMs
ansible-playbook -i inventory.ini playbooks/update-all.yml
# Provision a new web server
ansible-playbook -i inventory.ini playbooks/provision-webserver.yml
# Check system status across all VMs
ansible all -i inventory.ini -m shell -a "uptime && free -h"
# Backup all databases
ansible dbservers -i inventory.ini -m shell -a "mysqldump --all-databases > /backup/$(date +%Y%m%d).sql"
Time saved: Hours per week
Errors reduced: 90%
Coffee consumption: Still high, but now it’s for enjoyment, not stress relief
Troubleshooting Common Issues
“Permission Denied” Errors
# Check SSH key permissions
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
# Test SSH connection
ssh -i ~/.ssh/id_ed25519 user@vm1
“Host Key Verification Failed”
# Add host keys to known_hosts
ssh-keyscan -H vm1 >> ~/.ssh/known_hosts
# Or disable host key checking (less secure)
ansible-playbook -i inventory.ini playbook.yml --ssh-common-args='-o StrictHostKeyChecking=no'
“Python Not Found”
# Add to your inventory
[all:vars]
ansible_python_interpreter=/usr/bin/python3
What’s Next?
Now that you have the basics, here are some advanced topics to explore:
- Ansible Vault for secure credential management
- Dynamic inventories for cloud environments
- Ansible Tower/AWX for web-based management
- Custom modules for Proxmox-specific tasks
- CI/CD integration with GitHub Actions
Your Turn!
Ready to automate your homelab? Start with the SSH key setup and basic update playbook. Once you see how much time you save, you’ll be hooked.
Questions? Comments? Horror stories about manual VM management? Drop them in the comments below. Let’s build the most automated homelab community ever! 🤖
Automated with ❤️ using Ansible, served from my Proxmox homelab.