Ansible for the Homelab — Part 2: Your First Playbook
In Part 1 we installed Ansible and built an inventory. Now let's put it to work on something you'll actually run every time you stand up a new machine.
Every time I spin up a new LXC container or VM, the same manual steps happen. That's exactly the kind of thing Ansible is built to eliminate - but there's a catch.
The Bootstrap Problem
Ansible connects to hosts using a dedicated ansible user with an SSH key. On a brand new machine that user doesn't exist yet. This is the chicken-and-egg problem of homelab automation: you need Ansible to set up the machine, but you need the machine set up before Ansible can touch it.
The solution is a two-phase approach.
Phase 1: Manual Prep
A few things have to happen by hand before Ansible can reach the new host. These are one-time steps per machine.
Open up SSH access temporarily:
vi /etc/ssh/sshd_config
# Set: PermitRootLogin yes
# PasswordAuthentication yes
systemctl restart ssh
Register it on the network:
- Add a DNS rewrite in AdGuard so the hostname resolves
- Fix the DHCP lease in your router so the IP stays stable
Create the ansible user:
I keep a small script - files/create-ansible-user.sh - that installs sudo and zsh, creates the ansible user with passwordless sudo, and sets up the .ssh directory. Copy it over and run it as root:
scp files/create-ansible-user.sh root@{hostname}.lan:/tmp/
ssh root@{hostname}.lan "chmod +x /tmp/create-ansible-user.sh && /tmp/create-ansible-user.sh"
Copy the Ansible SSH key:
cat ~/.ssh/ansible_ed25519.pub | ssh root@{hostname}.lan \
"cat > /home/ansible/.ssh/authorized_keys && \
chmod 600 /home/ansible/.ssh/authorized_keys && \
chown ansible:ansible /home/ansible/.ssh/authorized_keys"
At this point the machine can talk to Ansible. Phase 1 is done.
Phase 2: Run the Playbook
Add the new host to the appropriate group in inventory.yml, then verify Ansible can reach it:
ansible {hostname}.lan -m ping
A pong means you're ready. Now run the bootstrap playbook:
ansible-playbook playbooks/bootstrap.yml --limit {hostname}.lan
The --limit flag targets a single host from your inventory - important when you don't want to accidentally bootstrap everything at once.
The playbook takes it from here. It updates apt, installs the core toolset (git, zsh, curl, jq, fzf, eza, starship, zoxide, uv), sets the timezone from inventory, distributes SSH keys, and pulls in dotfiles via stow. When it finishes, the machine has a consistent environment that matches every other host in the homelab.
Verify the final result:
ssh {hostname}
If that works, root password login can be locked down again - the machine is fully under Ansible's control from here on.
What's Next
Once you have a handful of playbooks the project root starts getting cluttered. Part 3 covers roles and structure - how to organize your playbooks so they stay manageable as the homelab grows.