- Shell 48.4%
- Go 31.3%
- Go Template 17.7%
- Python 1.6%
- Dockerfile 1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .forgejo/workflows | ||
| chat-archive | ||
| dns | ||
| dns-defensive | ||
| dns-gram | ||
| dns-personal | ||
| docs | ||
| forgejo | ||
| foundation | ||
| internal | ||
| netbird | ||
| netbird-archive | ||
| photo-archive | ||
| scripts | ||
| services | ||
| site-partner | ||
| sites | ||
| tools/schedules | ||
| .gitignore | ||
| .sops.yaml | ||
| CLAUDE.md | ||
| go.mod | ||
| go.sum | ||
| PROJECT_PLAN.md | ||
| README.md | ||
| retry-up.sh | ||
| TASKS.md | ||
vaderlab-infra
My home network and the things I self-host, managed as code.
Four machines — two in a closet at home, two free VMs in Oracle's cloud — joined into one private network by a self-hosted NetBird overlay. Once they're on that overlay they behave like one system: I can SSH to any of them from anywhere, stream my own media, push work to the GPU box from a laptop, and route traffic out through my house when I'm on someone else's wifi. A few public websites and my email DNS ride along on the same infrastructure.
Nothing here is clicked into existence. Cloud resources are Pulumi programs in Go, services are Docker Compose stacks with rendered config, secrets are sops+age, and CI runs on a self-hosted Forgejo. The repo is as much the point as the network is — it's a deliberate exercise in learning networking and sysadmin by building the real thing rather than a tutorial version of it.
The estate
┌─────────────────────────────────┐
home closet │ OCI (always-free ARM, $0) │
┌──────────────┐ │ ┌───────────────────────────┐ │
│ pearllab │ │ │ vaderlab-netbird │ │
│ Intel N150 │◄────┼──┤ NetBird control plane │ │
│ mini PC │ │ │ Pocket ID (OIDC), coturn │ │
│ │ │ └───────────────────────────┘ │
│ Traefik │ │ ┌───────────────────────────┐ │
│ Forgejo │ │ │ vaderlab-sites │ │
│ Immich │ │ │ public static sites │ │
│ Plex │ │ └───────────────────────────┘ │
│ backups │ └─────────────────────────────────┘
└──────┬───────┘
│ NetBird overlay (WireGuard) ┌─────────────────────────┐
┌──────┴───────┐ │ AWS │
│ pearlbox │ │ Route 53 — 4 domains │
│ RTX 3080 │ │ SES — outbound mail │
│ llama.cpp │ │ S3 — offsite backups │
│ Open WebUI │ │ CloudFront — one site │
│ Immich ML │ └─────────────────────────┘
└──────────────┘
| Box | What it is | Detail |
|---|---|---|
| pearllab | Home mini PC. Always-on anchor: exit node, media server, backup destination, and host for the home service layer behind a shared Traefik. | services/pearllab/ |
| pearlbox | Home inference box. RTX 3080 running llama.cpp behind llama-swap, Open WebUI, speech-to-text, and Immich's ML workloads. In a closet, overlay-only. | services/pearlbox/ |
| vaderlab-netbird | OCI VM. The NetBird control plane and Pocket ID — the critical dependency of everything else. | netbird/ · services/netbird/ |
| vaderlab-sites | OCI VM. Public static sites behind Traefik. No public SSH; reached over the overlay. | sites/ · services/sites/ |
The two cloud boxes sit at exactly Oracle's always-free ceiling and are designed
to bill $0. Why they're two boxes and not one, and what happens when that
free tier changes, is docs/oci-free-tier.md.
How it's built
- Cloud resources — Pulumi programs in Go (Go 1.26), one module, thirteen
projects. Conventions, stack naming, secret and state handling:
docs/pulumi-conventions.md. - Services — Docker Compose per box, with
.tmplconfig rendered by arender.shthat injects decrypted secrets at deploy time. Nothing rendered is committed. - Secrets — sops + age for service
credentials (
secrets.enc.yaml); Pulumi Cloud's own encryption for stack config. Exposure playbook:docs/playbooks/. - CI — self-hosted Forgejo Actions on pearllab:
go vetandgo teston every push (.forgejo/workflows/ci.yml). - Provisioning the home boxes is still by hand, deliberately. The READMEs are written as precise manual procedures because a procedure performed once and written down is the specification for the Ansible role that replaces it.
Where things are
Pulumi projects
Each directory is one project with its own state, config and blast radius. Every
Pulumi.yaml carries a description: saying what it owns and why it's separate.
foundation/ |
Spend budget + alert rules. Deploy first; it outlives what it watches. |
netbird/ |
Control-plane VM and its VCN. |
sites/ |
Public-sites VM and its VCN. |
dns/ dns-personal/ dns-defensive/ dns-gram/ |
Route 53, one project per domain so an error in one can't reach another's records. |
email/ |
SES sending identity and a send-only IAM user. |
forgejo/ |
Forgejo's S3 backup bucket and its scoped IAM users. |
netbird-archive/ photo-archive/ chat-archive/ |
Offsite S3 archives, one per service, each with a push-only user that cannot delete. |
site-partner/ |
A partner's site end to end: S3, CloudFront, ACM, Route 53. |
internal/ |
Shared builders the projects are assembled from — ocivm, ocinet, ocibudget, cloudinit, freetier, awsdomain, maildns, mailsender. |
Services and operations
services/ |
Compose stacks per box: netbird, pearllab (incl. Forgejo, Immich), pearlbox (incl. parakeet STT), observability, sites. |
scripts/backup/ |
Encrypted backups of the network definition — peers, policies, passkeys. |
scripts/overlay/ |
Why Docker must not start before the overlay address exists, and the systemd fix. |
scripts/takeout/ |
Google Takeout audit and reconciliation for the Immich migration. |
tools/schedules/ |
Inventory of every scheduled job across the four boxes. Spec, not yet built. |
docs/playbooks/ |
Incident procedures. |
Start here
| If you want | Read |
|---|---|
| What's open and what's done, across everything | TASKS.md — the map, kept current |
| Why the overlay is built the way it is | PROJECT_PLAN.md — goals and accumulated rationale, not status |
| To work on this with an AI agent | CLAUDE.md — standing constraints |
| To stand the cloud side up from scratch | foundation/, then netbird/, then sites/ |
Two conventions worth knowing before reading anything else. READMEs live next
to the thing they describe and record why a value is what it is, not just
what it is — the reasoning is the part that isn't recoverable from the code.
And TASKS.md is the only file tracking status; anything here describing
current state is a bug in this file.