When your homelab grows past a handful of services, a single git repository starts to feel like a junk drawer. Every service’s config, scripts, and docs are in the same commit history, every change touches multiple unrelated services, and cloning the whole thing just to work on one component is wasteful.
I went through this with my homelab. Here’s the pattern I landed on.
The Problem with Monorepos for Homelab
A monorepo made sense initially — one place for everything, simple history. But after adding a dozen services, the friction started:
- A Caddy config change and a Proxmox script change ended up in the same commit
- Cloning the repo on a new machine pulled hundreds of megabytes of irrelevant files
- Sharing one service’s config with someone else meant sharing everything
- Searching git history for “what changed in Nextcloud” returned noise from every other service
The fix isn’t a monorepo vs polyrepo debate. It’s about giving each service its own history while keeping a unified view of the whole.
The Registry Pattern
The structure I use:
homelab-registry/ ← the "source of truth" repo
├── .gitmodules
├── submodules/
│ ├── caddy/ ← git submodule → service-caddy.git
│ ├── nextcloud/ ← git submodule → service-nextcloud.git
│ ├── network/ ← git submodule → service-network.git
│ ├── proxmox/ ← git submodule → service-proxmox.git
│ └── sentinel/ ← git submodule → service-sentinel.git
Each service is an independent repository. The registry contains nothing except submodule pointers. A commit to the registry says “at this moment in time, these are the known-good versions of all services.”
How it Works in Practice
Day-to-day work on a service: navigate into the submodule, work normally, commit and push to that service’s repo. The registry is unaffected until you explicitly update its pointer.
Marking a stable infrastructure state: from the registry root, git add submodules/caddy submodules/nextcloud && git commit -m "after TLS migration". Now anyone can check out that registry commit and get exactly those versions of both services.
Finding what changed in one service: cd submodules/caddy && git log --oneline — clean history of only Caddy changes.
Pulling latest everywhere:
git submodule update --remote --merge
git add .
git commit -m "sync all services to latest"
Splitting an Existing Monorepo
If you’re starting from an existing repo, git subtree split preserves history:
# In the monorepo
git subtree split --prefix=caddy -b split-caddy
# In the new service repo
git init --initial-branch=main
git remote add origin http://forgejo.internal/forgejo/service-caddy.git
git pull ../homelab split-caddy
git push -u origin main
The split branch carries only the commits that touched the caddy/ directory, nothing else.
Naming Convention
I settled on:
- Forgejo repo names:
service-<name>(e.g.service-caddy,service-nextcloud) - Submodule path in registry:
submodules/<name> - Folder name matches the original monorepo directory name
Consistent naming means the git submodule add command is always the same pattern:
git submodule add http://forgejo.internal/forgejo/service-<name>.git submodules/<name>
What the Registry Doesn’t Contain
Deliberately nothing service-specific:
- No Docker images
- No secrets or credentials
- No build artifacts
- No deployed configuration
It’s purely a coordination layer. Each service repo is independently useful without the registry — you can clone service-caddy directly and work on it without needing to know about any other service.
Trade-offs
What you gain: isolated history per service, ability to version-tag services independently, smaller clones for targeted work, clean separation of concerns.
What you give up: atomic cross-service commits. If Caddy and Nextcloud need a coordinated change, you make two commits in two repos and one registry commit linking them. This is a bit more ceremony.
Submodule UX: git submodule commands have a learning curve. The main ones I use daily are git submodule update --init --recursive (initial setup) and git submodule update --remote (pull all services to latest). Once those two are muscle memory, the friction disappears.
For a homelab with 10+ services and ongoing active development, the registry pattern has been worth the setup cost. The ability to look at a service’s git log and see only that service’s history is alone worth the migration.