Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

HomeLab: A Typed, Plugin-Driven Meta-Orchestrator for Local Infrastructure

"You can't build HomeLab without GitLab, and you can't run GitLab without HomeLab. We solve the chicken-and-egg with a 200-line bash bootstrap script — and then HomeLab takes over forever after."


What this series is

This series designs HomeLab in public.

HomeLab is a CLI-first meta-orchestrator that stands up local infrastructure end-to-end: Packer images, Vagrant VMs, Docker (or Podman) hosts, Docker Compose stacks, Traefik routing, TLS certificates, DNS entries, and a fully configured GitLab instance with runners. It is the missing piece between "I want a homelab" and "my homelab is up, my CI is green, and my docs site is live".

It does not yet exist as code. The spec lives in HomeLab/doc/PLAN.md and a few sibling design documents in the FrenchExDev monorepo. This series is the blueprint that turns those documents into a real .NET 10 library + CLI — written before the first line of production code, so the design can be argued, broken and rebuilt in the open.

This is spec-driven blogging. The same pattern as the Ops DSL Ecosystem series, the Typed Docker series, and the GitLab Docker Compose series: design first, build later, and let the design absorb every painful question along the way.


The thesis

A homelab is not a script. It is a typed declaration of an environment, fed through a deterministic pipeline, observed by a typed event bus, and executed by typed binary wrappers. Every artifact — every line of HCL, every line of YAML, every Vagrantfile, every gitlab.rb — is generated. Nothing is hand-written. Drift is eliminated by construction.

The four non-negotiable architectural constraints are:

  1. Thin CLI → fat lib. HomeLab.Cli is a System.CommandLine shell. Every verb is one method call into HomeLab (the lib). No business logic in the CLI project. This makes E2E tests = CLI invocations, and unit tests = direct lib calls — the two test surfaces collapse to one.
  2. SOLID, DRY, pipeline, events, plugins. The lib is built around an explicit IHomeLabPipeline of six stages (Validate → Resolve → Plan → Generate → Apply → Verify), an IHomeLabEventBus that publishes domain events at every meaningful step, and an IHomeLabPlugin contract that lets third-party NuGets add machine kinds, compose contributors, DNS providers, TLS providers, container engines, and Ops.Dsl sub-DSLs without forking HomeLab.
  3. [Injectable] everywhere. HomeLab's DI container is generated, not hand-written. Every service, stage, plugin, contributor, binary wrapper and event subscriber is declared with [Injectable(...)] from FrenchExDev.Net.Injectable, and the source generator emits the entire AddHomeLab(this IServiceCollection) composition root at compile time. The cross-cutting concerns (logging, retries, timing, audit, chaos faults) are layered with [InjectableDecorator], not by hand-rolled Decorate<T,D>() calls.
  4. Built on Ops.Dsl. HomeLab consumes the Ops.Dsl M3 vocabulary — OpsTarget, OpsProbe, OpsThreshold, OpsPolicy, OpsEnvironment, OpsSchedule, OpsExecutionTier, OpsRequirementLink — and the eight sub-DSLs that matter for a homelab: Ops.Infrastructure, Ops.Deployment, Ops.Observability, Ops.Configuration, Ops.Resilience, Ops.Security, Ops.Networking, Ops.DataGovernance. Both HomeLab and Ops.Dsl are spec-only today; the series is honest about this and treats it as a feature: we are co-designing two libraries that will compile against each other.

Eat your own dog food: there is no fictional project

Most "build a homelab" series ship with a toy demo — a fake todo app, an invented documentation platform, a contrived microservice. This series doesn't. The thing HomeLab stands up is the FrenchExDev development environment itself.

We call it DevLab, and it contains:

  • A real GitLab Omnibus instance that hosts the FrenchExDev.Net monorepo (the same repo HomeLab lives in)
  • Real GitLab runners that build and test HomeLab on every push
  • A real Vagrant box registry that hosts the very .box files HomeLab produces with homelab packer build
  • A real NuGet feed (baget) that ships FrenchExDev.Net.HomeLab.nupkg
  • A real docs site — this very blog — served by Traefik over a self-signed wildcard cert
  • Postgres for GitLab, MinIO for CI artifacts and box storage, Meilisearch for the docs search index
  • Prometheus + Grafana + Loki + Alertmanager for observability
  • PiHole for DNS so internal services can resolve gitlab.frenchexdev.lab

The five dogfood loops:

Loop 1: Source     GitLab in DevLab hosts the HomeLab repo →
                    runner in DevLab builds HomeLab.dll

Loop 2: Packages   Runner publishes nupkg → baget in DevLab →
                    next dotnet restore pulls from a feed HomeLab is hosting

Loop 3: Boxes      homelab packer build → .box → homelab box publish →
                    Vagrant.Registry in DevLab → next homelab vos up
                    pulls from a registry HomeLab is hosting

Loop 4: Backups    Backup framework backs up the GitLab that holds
                    HomeLab's source. Periodic restore-into-throwaway-lab
                    proves we can rebuild ourselves from a tarball.

Loop 5: Docs       This blog is hosted in DevLab. The documentation of
                    HomeLab is served by HomeLab.

And the bootstrap paradox? A 200-line Stage-0 bash script stands up just enough GitLab to compile the first HomeLab binary. From the moment HomeLab v0.1 boots, the bash script is dead and HomeLab takes over forever after. We never touch it again.


Three topologies, one config field

HomeLab supports the same DevLab in three topologies, picked by a single topology field in config-homelab.yaml:

Topology VMs What it's for
single 1 Dev iteration. Everything (GitLab + runners + Postgres + MinIO + Traefik + obs + PiHole) in one VM, one compose file. Cheap, fast, the default.
multi 4 "Real" DevLab. gateway (Traefik + PiHole), platform (GitLab + runners + baget), data (Postgres + MinIO + Meilisearch), obs (Prometheus + Grafana + Loki + Alertmanager). One compose file per VM.
ha ~10 GitLab Reference Architecture on Omnibus, without Kubernetes. 2× Rails behind HAProxy, Gitaly Cluster + Praefect, Patroni Postgres, Redis Sentinel, Consul, shared MinIO. Yes, HA GitLab works without K8s.

A fourth topology, ha-k8s, is sketched as a future plugin against an as-yet-unwritten K8s.Dsl C# library. When K8s.Dsl lands, HomeLab gains it without touching the core — exactly the point of the plugin contract.

Multi-instance is also first-class: you can run a dev-single, a prod-multi and an ha-stage HomeLab on the same workstation simultaneously. Each instance gets its own subnet, its own VM-name prefix, its own DNS namespace, its own cert namespace, and its own docker-network space. Two HomeLabs cannot accidentally collide. CI even spins up a fresh pr-1234 instance for every pull request to validate the change end-to-end.


The FrenchExDev toolbelt

HomeLab does not reinvent anything. Every cross-cutting concern is solved by an existing in-house library from the FrenchExDev_i2 monorepo:

Concern Library What HomeLab gets
DI Injectable Generated AddHomeLab(), decorator chains via [InjectableDecorator]
Errors Result, Result<T>, Result<T,TError> No exceptions for control flow; every stage returns Result<T>
Builders Builder ([Builder]) Async, validated, cycle-safe config builders
Validation Guard Boundary defensiveness
Time Clock (IClock, FakeClock) Cert expiry, backup schedules, cost wall-clock — testable
State FiniteStateMachine VM lifecycle, cert lifecycle, deployment lifecycle
Sagas Saga Bootstrap + dogfood loops as compensable transactions
Reactive Reactive (IEventStream<T>) Event bus internals, hot-reload of config-homelab.yaml
Optionality Options (Option<T>) Optional config slots without null
Mapping Mapper YAML DTO → domain model without reflection
Mediator Mediator CLI verb dispatch into the lib
Outbox Outbox Reliable event delivery to plugins / audit
Wrappers BinaryWrapper ([BinaryWrapper("docker")]) Docker, Podman, Packer, Vagrant, Git, mkcert
CI YAML GitLab.Ci.Yaml DevLab's .gitlab-ci.yml is generated, not hand-written
Versions Alpine.Version Packer base image pinning + drift detection
Tracing Requirements HomeLab features traced via [ForRequirement]
Quality QualityGate (dotnet quality-gate test) Coverage / mutation / dashboard the dev-loop ships under
Domain Ddd + Entity.Dsl Lab, Machine, Service, Cert, DnsEntry aggregates
M3 Dsl The Ops.Dsl substrate
HTTP HttpClient Typed clients for PiHole, GitLab, the box registry

Part 11 of this series is the dedicated tour: every library, with one runnable code block per library showing how HomeLab uses it.


Architecture cheat-sheet

┌────────────────────────────────────────────────────────────────────┐
│                      HomeLab.Cli (thin)                            │
│  System.CommandLine — one verb = one HomeLabRequest                │
└───────────────────────────────┬────────────────────────────────────┘
                                │ HomeLabRequest
                                v
┌────────────────────────────────────────────────────────────────────┐
│                       HomeLab (lib)                                │
│  IHomeLabPipeline                                                  │
│   └─ IHomeLabStage[]                                               │
│       0  Validate    (config + Ops.Dsl constraints)                │
│       1  Resolve     (load plugins, merge local overrides)         │
│       2  Plan        (Ops.Dsl → IR: planned actions, DAG)          │
│       3  Generate    (Packer HCL, Vagrantfile, compose, traefik)   │
│       4  Apply       (binary wrappers: packer build, vagrant up…)  │
│       5  Verify      (probes from Ops.Observability)               │
│                                                                    │
│  IHomeLabEventBus  ──────►  loggers, progress, audit, plugins      │
│  IPluginHost       ──────►  IHomeLabPlugin discovery                │
│  AddHomeLab(IServiceCollection)  ◄─ generated by [Injectable] SG   │
│  Decorators (logging/timing/retry/audit/chaos) via                 │
│    [InjectableDecorator]                                            │
│  Result<T> everywhere; no exceptions for control flow              │
└─────┬──────────────────────────────────────────────────────────────┘
      │ uses
      v
┌────────────────────────────────────────────────────────────────────┐
│                          Ops.Dsl (M2)                              │
│  Built on FrenchExDev.Net.Dsl (M3)                                 │
│  Sub-DSLs HomeLab depends on:                                      │
│    Ops.Infrastructure  Ops.Deployment    Ops.Observability         │
│    Ops.Configuration   Ops.Resilience    Ops.Security              │
│    Ops.Networking      Ops.DataGovernance                          │
└────────────────────────────────────────────────────────────────────┘

How to read this series

Architects should read Act I + Act II + part 30 (topology composition) + part 56 (roadmap) for the vision, the lib internals, the topology picker and the implementation phases.

Developers should read Act II + Act III + Act V + Act VI for the architecture, the tool wrappers, the compose authoring and the DevLab walk-through.

SRE / Platform engineers should read Act IV (host VMs), Act VI (DevLab), and Act VII (operational depth) for everything between "the box exists" and "the lab is running in production".

Plugin authors should read Act II part 10 + Act IX (parts 53–54).

Everyone should read Part 06 — the dogfood pivot is the spine of the rest of the series.


Prerequisites


⬇ Download