Config for a single-box Proxmox homelab, collected off the live machine.
  • Python 46.6%
  • Shell 46.2%
  • CSS 2.7%
  • Ruby 1.7%
  • sed 1.6%
  • Other 1.2%
Find a file
Motaphe 78ce4e8242 Add MIT license; make the pre-commit gate fail closed
The gate had two checks that could silently no-op. gitleaks being absent
printed a warning but left fail=0, and private_patterns returning nothing
collapsed check 3 to the Porkbun key prefix alone. On a workstation clone
with neither, two of three checks were inert and the commit still looked
gated. A check that cannot run is now a failure, overridable on purpose
with HOMELAB_ALLOW_DEGRADED=1 for docs and tooling.

Also whitelist three curl-auth-user hits in the paperless README, where
the password half of the -u pair is a two-letter placeholder. Installing
gitleaks made these block every commit. The .gitleaksignore comment
describes them rather than quoting them, because quoting made that file
trip the rule it was suppressing.

Note in architecture.md that private/ is the only thing here git does not
protect, and needs a backup off this host.
2026-09-03 12:42:04 -04:00
.githooks Add MIT license; make the pre-commit gate fail closed 2026-09-03 12:42:04 -04:00
bin homelab: initial config snapshot 2026-09-02 22:06:37 -04:00
docs Add MIT license; make the pre-commit gate fail closed 2026-09-03 12:42:04 -04:00
hosts homelab: initial config snapshot 2026-09-02 22:06:37 -04:00
pve homelab: initial config snapshot 2026-09-02 22:06:37 -04:00
transforms homelab: initial config snapshot 2026-09-02 22:06:37 -04:00
.gitignore homelab: initial config snapshot 2026-09-02 22:06:37 -04:00
.gitleaks.toml homelab: initial config snapshot 2026-09-02 22:06:37 -04:00
.gitleaksignore Add MIT license; make the pre-commit gate fail closed 2026-09-03 12:42:04 -04:00
hosts.tsv homelab: initial config snapshot 2026-09-02 22:06:37 -04:00
LICENSE Add MIT license; make the pre-commit gate fail closed 2026-09-03 12:42:04 -04:00
manifest.tsv homelab: initial config snapshot 2026-09-02 22:06:37 -04:00
README.md Add MIT license; make the pre-commit gate fail closed 2026-09-03 12:42:04 -04:00

homelab

Config for a single-box Proxmox homelab — one i9-10900 with 64 GB RAM running nine LXCs and a Home Assistant VM, about 100 containers between them.

This repo is the change log. Everything here is collected off the live machine by bin/sync.sh, so the history is a record of what actually changed and when, not a set of files someone remembered to copy.

The box

milotron — Dell Precision 3240 Compact, Proxmox VE 9.2, Debian 13. Two ZFS pools: rpool (mirrored NVMe, boot + container roots) and miloarmy (5-disk raidz2, 27 TB raw) for bulk data.

                    internet
                       │
                  Caddy (CT100) ── 45 vhosts on *.motaphe.dev, DNS-01 certs
                       │            CrowdSec at the edge; private vhosts are
                       │            LAN + tailnet only
        ┌──────────────┼──────────────┬───────────────┐
        │              │              │               │
   personal(101)  media(102)   dashboard(105)   games(109)
   60 containers  25 containers  12 containers   Terraria
        │              │
        └── /data on miloarmy ──┘   hardlink-friendly single tree,
                                    Intel iGPU passed through to both

   adguard(104)   headscale(107)   exitnode(108)   hermesagent(106)   haos(103)
   DNS + DoT      tailnet control  ProtonVPN       agent              Home Assistant
                  plane + DERP     exit nodes
Guest Role Notable
CT100 caddy Reverse proxy Single ingress. internal_only snippet restricts private vhosts to LAN + 100.64.0.0/10
CT101 personal App host Immich, Nextcloud, Vaultwarden, Forgejo, Paperless, n8n, Matrix, Karakeep, SearXNG, Open WebUI
CT102 media Media automation Jellyfin, Navidrome, the *arr stack, qBittorrent + slskd behind Gluetun
VM103 haos Home Assistant OS
CT104 adguard DNS LAN resolver, split-horizon *.motaphe.dev, DoT with ClientID auth
CT105 dashboard Monitoring Uptime Kuma, Beszel, Scrutiny, Dozzle, Healthchecks, Homepage, UniFi
CT106 hermesagent Agent
CT107 headscale Tailnet Self-hosted control plane + embedded DERP
CT108 exitnode VPN egress Four regional ProtonVPN exit nodes on the tailnet
CT109 games Game server Terraria/TShock

How this repo works

Config lives on ten different guests. Rather than ten repos, one repo on the Proxmox host collects from all of them — unprivileged LXC datasets are mounted on the host whether or not the container is running, so pve can already read the whole fleet.

homelab            # collect, review, commit, push
homelab diff       # what changed since the last commit
homelab restore caddy [--apply]

manifest.tsv is the whole design. It's an allowlist: a file is collected only if a rule names it. Directory rules are depth-1 and must specify what to include — there's no way to write "everything under here" — and anything in the repo that no rule produced gets deleted on the next sync. That matters because the compose directories sit right next to hundreds of thousands of files of app state.

.env files are never copied. For each one, sync generates a .env.example from the live file with the key names and comments intact and the values stripped. Generated rather than hand-written, because the hand-written ones had already drifted — the old media-stack example was missing eight keys that the running stack depends on.

A pre-commit hook blocks the commit if a secret gets through anyway: filename deny-list, then gitleaks, then a grep for a few values specific to this network. It blocks rather than quietly redacting, because a secret sitting in a config file is worth finding out about. A check that can't run counts as a failure too — without the gitleaks binary, or without private/, the gate refuses rather than reporting a pass it didn't earn (HOMELAB_ALLOW_DEGRADED=1 to commit docs or tooling from a clone that has neither).

Some files legitimately contain something that can't be published — a WAN IP, an API key with no env indirection, a hardware address. Those go through a sed transform before they're written into the repo.

Those transforms come in two kinds, and the split matters. transforms/ holds the ones that describe the shape of a secret (password: followed by anything) — safe to publish, and committed. private/ holds the ones that have to name a literal value in order to replace it. Publishing those would republish exactly what they exist to remove, so private/ is gitignored and stays on the host. The gate derives its patterns from private/ at runtime, which means adding a scrub rule automatically extends the gate instead of the two drifting apart.

Layout

manifest.tsv      the allowlist
hosts.tsv         the fleet
bin/              sync, restore, the wrapper
transforms/       pattern-based redactions (published)
private/          value-bearing redactions (gitignored, never leaves the host)
pve/              hypervisor config, LXC/VM definitions
hosts/<name>/     mirrors each guest's own paths
docs/             architecture, restore, runbook

Not in here

Secrets, obviously — but also app state, databases, media, Tailscale node identities, SSH host keys, and the certificate store. docs/architecture.md has the reasoning; the short version is that this repo tracks the things a human wrote.