djuntgen@juntgen.com
← all posts

Building a Homelab with AI · part 2

Why Infrastructure as Code? From Manual Configs to Git-Driven Homelab

#homelab#iac#git#ansible#portainer#gitops

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.