# ironicbadger/infra: a self-hosted homelab that lives in a public repository

> ironicbadger/infra is the Ansible, Just and Docker Compose configuration behind one person's self-hosted infrastructure, published openly. It covers a media server with ZFS replication over Tailscale, a floating VIP edge pair of Raspberry Pi and ZimaBlade, and Proxmox VM provisioning, with secrets handled by SOPS and age.

**ironicbadger/infra** — 99.7% less leaked credentials

- Repository: https://github.com/ironicbadger/infra
- Stars: 841 · Forks: 48
- Language: Python
- License: GPL-3.0
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/ironicbadger-infra

## A personal infrastructure repo published in the open

ironicbadger/infra is described as the living, breathing source of truth for one person's self-hosted infrastructure, run in the open because open source matters. The README opens with a running joke that also happens to be a security posture: the project started at zero leaked credentials, and each time a leak has occurred the counter gets decremented by one.

That framing matters for how you should read the repository. It is not a product and it is not a framework. It is a real environment expressed as Ansible playbooks, Just recipes, Docker Compose files and Nix declarations, with the author's own hardware, hostnames, IP ranges and storage pools in it. That is exactly what makes it useful as a reference, and exactly what makes it unusable as a drop-in.

The technologies in use are listed plainly: Ansible for configuration management, Docker Compose for container orchestration through a docker-compose-generator role, SOPS with age for secret encryption, Tailscale as the mesh VPN, ZFS with zrepl for storage and replication, Proxmox VM provisioning for creating VMs and setting clone-time metadata, and Just as the task runner.

The audience is a homelab operator or a self-hosting enthusiast who wants to see how the pieces fit together in a real deployment rather than in a tutorial. It is also a good study for anyone writing their own infra repo, because the layout decisions are visible and deliberate.

The primary language is recorded as Python, which reflects the Ansible side of the repository, and the licence is GPL-3.0. The last push was on 2026-10-22.

## Two sites, ZFS pools and replication over Tailscale

The architecture diagram in the README is the fastest way to understand the scale of the setup. It is a mermaid flowchart with two subgraphs, one labelled Home and one labelled Off-site.

At home there are two machines. c137 is a media and storage server with a ZFS pool named rust of 164TB, and ms01 is an app server running Caddy, DNS and Home Assistant. Off-site there are three: a VPS called ktz-cloud in the United States, igloo in Canada with a ZFS pool named tank of 62TB, and snowball in the United Kingdom.

The edges between them are the interesting part. c137 replicates to igloo and to snowball over zrepl via Tailscale, and c137 has a bidirectional link with ktz-cloud over the same path. So the media server is the source of truth for the bulk data, and the off-site machines hold copies reachable over the mesh VPN rather than over the open internet.

Using Tailscale as the transport for ZFS replication is a specific decision with a specific consequence. Replication traffic is not exposed to the public internet, and the private addresses are stable without opening ports on the home connection. The cost is that the replication path depends on the mesh being up.

The storage numbers also tell you something about the design. A 164TB pool at home and a 62TB pool in Canada are not scratch space. This is a media library with off-site backup, which is the kind of problem a home-lab setup is usually trying to solve and rarely gets right.

## Roles come from three places, and the submodules are pinned

The roles philosophy is the part of this repository that generalises best, because the same three-way split works for any Ansible project of this size.

Reusable roles are sourced as submodules under a naming scheme with the author's prefix, maintained in separate repositories and pinned to specific commits. That last detail is the important one. A pinned submodule means the role version in use is recorded in the repository history, so a change upstream cannot silently alter your infrastructure.

Community roles come from Ansible Galaxy, listed in a requirements.yaml file, with an example given of the geerlingguy.docker role. They are installed with a Just recipe.

Local roles live in the roles directory alongside the submodules, and the README gives the reasoning: these are project-specific roles not useful elsewhere, with zrepl and ktz-server-welcome named as examples. That is a sensible test. If a role is only meaningful in this environment, keeping it in this repository avoids polluting a shared one with a role nobody else can use.

The repository root shows the full picture: a .sops.yaml file for the encryption configuration, an ansible.cfg, a hosts.ini inventory, a justfile, a requirements.yaml, a run.yaml main playbook, a playbooks/ directory, roles/, group_vars/ and host_vars/, a services/ directory, a nix/ directory for Proxmox declarations, a talos/ directory for the cluster, and cloud/, dev/, docs/, files/ and scripts/ directories. There is also a renovate.json, which means the dependencies are updated by automation rather than by hand.

## The Just recipes are the whole user interface

Everything an operator does goes through the justfile, and the recipes are few enough to read in one sitting.

```bash
just reqs              # Install galaxy dependencies
just run <host> <tags> # Run playbook on host
just compose <host>    # Deploy docker-compose services
just vm <command>      # Create/list Proxmox VMs
just sops <file>       # Edit encrypted secrets
just sub-update        # Update git submodules
```

The recipes are thin wrappers over ansible-playbook, and the justfile in the repository shows the underlying commands. The run recipe invokes the main playbook with a host limit and optional tags, and the compose recipe runs a playbook with the compose tag. Both run with become enabled, which is what you would expect for a configuration management tool acting on remote hosts.

The secrets recipes are the ones to look at closely.

```bash
sops FILE='group_vars/all.sops.yaml':
    EDITOR='code --wait' sops {{FILE}}
```

The sops recipe defaults to a specific encrypted file and opens it in an editor through SOPS, and there is a separate read-only recipe that decrypts to standard output without opening an editor. The default file lives under group_vars, and the repository's .sops.yaml file holds the encryption configuration.

The prerequisite list is short: Ansible, SOPS with an age keyfile at a specific path under the user's config directory, and SSH access to the target hosts. That age keyfile path is worth noting, because it is where your decryption key lives, and getting it right is the difference between the encryption working and every run failing.

## The core edge pair: a floating VIP over two small machines

The core nodes are the most reusable design in the repository, and they are described in enough detail to be worth reading carefully.

Caddy and AdGuardHome are configured to run using keepalived with a floating virtual IP address. The table gives the arrangement: core-pi5 at 10.42.0.5 as the primary on a Raspberry Pi 5, core-zima at 10.42.0.6 as the backup on a ZimaBlade, and the VIP itself at 10.42.0.53.

The services on that pair are AdGuard Home for local DNS resolution on port 53 with a web UI on 3000, Caddy as the reverse proxy with automatic TLS, keepalived for VRRP failover, Chrony for NTP time synchronisation, and an AdGuard Home Sync service that replicates configuration from the primary to the backup.

That last item is the one that separates this from a naive two-node setup. DNS configuration that lives on a Raspberry Pi and has to be identical on a backup needs synchronisation, and a config sync job is how you avoid a failover that changes your resolver settings.

The recipes for the core are tag-selectable, which is the pattern you want for a service tier like this.

```bash
just core                 # Run full core playbook
just core --tags caddy    # Run only caddy role
just core --tags network  # Run only network role
just core --tags adguard  # Run only adguard role
```

The core-network role supports two backends, selected through a network_backend variable: systemd-networkd for Debian and NetworkManager for Raspberry Pi OS. The role removes dhcpcd packages and writes a static resolv.conf, which is the correct treatment for machines that hand out their own DNS through the floating IP.

## Services are generated per host from a directory layout

The services directory is where the Docker Compose configuration lives, and the layout is the point.

```
└── services/             # Docker Compose configs per host
    └── <hostname>/
        └── <##-category>/
            ├── compose.yaml
            └── config-<app>/
```

Each host has a directory, each service category within that host has a numeric prefix so the directories sort in a deliberate order, and each category holds a compose.yaml plus a config-<app> subdirectory for the application's own configuration files.

Services are organized under services/<hostname>/<##-category>/, and the docker-compose-generator role merges these into a single compose file at deployment time. So the deployed artefact is one compose file per host, but the source of truth is a tree of small files that a human can read.

That pattern is worth stealing even if you never deploy this repository. Splitting a large compose file into per-application directories, ordered by a numeric prefix, and generating the merged result at deploy time makes the difference between a file nobody dares to edit and a set of files that diff cleanly.

The rest of the repository follows the same logic of a wide surface with a small number of entry points. Beyond the services, there is an nix/ directory for Proxmox VM declarations and a clone helper, and the README is explicit that the NixOS host definitions and deployments live in a separate repository, ironicbadger/nix-config, with this one only creating the Proxmox VM shell for those hosts. The Talos cluster is described as infrastructure-only, using three local-disk VMs with external Ceph app storage, with its inventory, access and recovery instructions in the talos/m90q directory.

## Where ironicbadger/infra is the wrong tool

The first wrong-tool case is any expectation of a supported product. There are no GitHub releases, the repository has no issue tracker policy, and the code of conduct or support commitments are absent. The last push was on 2026-10-22, so the work is current, but current is not the same as maintained in the sense an enterprise buyer means. The maintainer is one person.

The second case is trying to run it as written. hosts.ini, the group_vars, the 10.42.0.x addressing, the ZFS pool names rust and tank, the Proxmox cluster and the Tailscale network are all specific to hardware you do not have. There is no configuration layer that abstracts them, and no provisioning path for a fresh machine beyond the Proxmox VM creation. Anyone adopting this is adopting a reference, not a deployment.

The third case is a team that needs an audit trail or a review workflow. The secrets workflow is good, with SOPS and age and encrypted group_vars, but the repository's own README is a joke about leaking credentials, and the git history of a homelab repo is not the same artifact as a change-controlled infrastructure codebase.

The fourth case is a workload that does not fit the shape. The stack assumes a specific set of machines with specific roles, from a Raspberry Pi and a ZimaBlade at the edge to a large storage server at home. A project with a dozen application servers and a different topology gets nothing from this layout.

On licensing, the project is GPL-3.0, which is a real constraint for a corporate infrastructure repo, since it is a copyleft licence: if you distribute a modified version, you must release it under GPL-3.0 and keep the copyright notice. Internal, undistributed use is not affected, but any shared fork carries the obligations.

## Conclusion

ironicbadger/infra suits a homelab operator who wants a worked reference for running Ansible against a mixed Raspberry Pi, ZimaBlade and Proxmox setup with Tailscale-based replication, and the Just recipes make the entry points obvious. It does not suit a team looking for a supported platform, because it is one person's infrastructure expressed in their own conventions, and it does not suit a copy-paste deployment, since the host inventory, addresses and the Proxmox and Tailscale wiring are specific to this hardware. Before adopting any part of it, read the repository's own remark that it started at zero leaked credentials and count down, then check the hosts.ini inventory against your own machines and treat the core-pi5 and core-zima pair as a design to copy rather than a configuration to run.

## FAQ

### What is ironicbadger/infra?

It is the public source of truth for one person's self-hosted infrastructure, expressed as Ansible playbooks, Just recipes, Docker Compose service directories, Nix Proxmox declarations and SOPS encrypted secrets, covering a home media server, an off-site VPS and a keepalived edge pair.

### How do I run a playbook in ironicbadger/infra?

Through the justfile recipes. just run <host> <tags> runs the main playbook against one host, just compose <host> deploys the Docker Compose services, and just reqs installs the Ansible Galaxy dependencies from requirements.yaml.

### How are secrets handled in ironicbadger/infra?

With SOPS and age, with the age keyfile expected at ~/.config/sops/age/keys.txt and encryption configuration in .sops.yaml. The just sops recipe opens an encrypted file in an editor through SOPS, and a separate recipe decrypts to standard output read-only.

### What is the core edge node setup in ironicbadger/infra?

Caddy and AdGuard Home run on two machines, a Raspberry Pi 5 as primary and a ZimaBlade as backup, with keepalived providing a floating VIP at 10.42.0.53. The pair also runs Chrony for time sync and an AdGuard Home Sync job that replicates configuration from the primary to the backup.

### How does ironicbadger/infra replicate data off site?

The media and storage server c137 replicates with zrepl over Tailscale to the Canadian machine igloo, which has a 62TB ZFS pool, and to the UK machine snowball. The home server has a 164TB ZFS pool named rust and the off-site pool is named tank.

### What license is ironicbadger/infra released under?

The project is licensed under GPL-3.0, with the LICENSE file at the root of the repository. That is a copyleft licence: distributing a modified version means releasing it under GPL-3.0, while internal undistributed use is unaffected.

## Sources

- [ironicbadger/infra on GitHub](https://github.com/ironicbadger/infra)
- [Issues](https://github.com/ironicbadger/infra/issues)
- [License: GPL-3.0](https://github.com/ironicbadger/infra/blob/main/LICENSE)
- [README](https://github.com/ironicbadger/infra/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ironicbadger-infra
