This is Part 2 of the Building a Homelab with AI series. Previously: Claude Code | Next: Ansible Bootstrap
The Problem
I have a confession. For years, my homelab was held together by tribal knowledge and muscle memory. Need to change a reverse proxy rule? SSH into the box, edit the config, restart the service, move on. Need to add a new container? Spin it up in Portainer, click through the UI, forget exactly what options I set three months later.
It worked. Right up until it didn’t. A Proxmox host reboot, a failed disk, a “what port was that service on again?” moment at 11 PM — and suddenly I’m piecing together configurations from memory and old terminal scrollback.
I’ve been a software engineer for 26 years. I would never ship production code without version control. So why was I treating my infrastructure like scratch paper?
The Vision
The goal was simple: Git as the single source of truth for everything in the homelab. Every configuration file, every playbook, every compose stack — versioned, documented, and reproducible.
The stack I landed on:
- Ansible for server configuration — user accounts, SSH hardening, package installation, service deployment
- Docker Compose for application stacks, managed through Portainer’s GitOps feature
- Git tying it all together, with meaningful commit messages that tell the story of every change
This isn’t about scale. I’m not running a fleet of hundreds of servers. I have a Proxmox hypervisor (pve), a Docker host (docker-host), a dev machine (dev), and a handful of service containers. But IaC isn’t about scale — it’s about reproducibility and documentation. When future-me asks “why is this configured this way?”, the Git log should have the answer.
The Repo Structure
I spent some time thinking about how to organize the repository. I wanted a clean separation of concerns:
/home/<user>/homelab/
├── stacks/ # Docker Compose stacks (Portainer GitOps)
│ ├── caddy/
│ │ ├── docker-compose.yml
│ │ └── Caddyfile
│ ├── openwebui/
│ │ └── docker-compose.yml
│ └── portainer/
│ └── docker-compose.yml
├── ansible/
│ ├── ansible.cfg
│ ├── inventory/hosts.yml
│ ├── playbooks/
│ ├── roles/
│ └── group_vars/
├── docs/
└── README.md
The stacks/ directory is purely for Portainer. Each subdirectory maps to a stack that Portainer watches and auto-deploys. The ansible/ directory is purely for server configuration. The docs/ directory captures anything that can’t be automated yet — manual Proxmox setup steps, network diagrams, architecture decisions.
This separation matters. When Portainer polls the repo for changes to stacks/openwebui/docker-compose.yml, it doesn’t need to care about Ansible playbooks. When I’m running ansible-playbook against my fleet, I’m not touching Docker Compose files. Clean boundaries, clear ownership.
Enter Claude Code
Here’s the twist: I did this entire project as a conversation with Claude Code, Anthropic’s AI CLI tool. Claude Opus 4.6 was my pair programmer for the whole thing — planning the repo structure, writing Ansible playbooks, debugging errors, reviewing security, and generating documentation.
Every AI-assisted commit in the repo includes Co-Authored-By: Claude Opus 4.6 <[email protected]> in the commit message. It’s a record of how the work was done, and honestly, it was one of the most productive infrastructure sessions I’ve had.
But that’s a story for the Claude Code post. First, let’s talk about bootstrapping a fleet of LXC containers with Ansible.
Why This Matters
Even if you never have a disaster, IaC pays for itself in clarity. Every git log entry is a decision record. Every playbook is documentation that also happens to be executable. Every compose file is a contract for what a service needs to run.
If you’re running a homelab and you haven’t started codifying your infrastructure, I hope this series gives you the push. It’s not as hard as it sounds — especially with a good AI pair programmer.