Part of digitaleraluhut — the platform layer. Provides the k3s cluster that homelab-apps deploys onto, and the networking that exposes local-ai endpoints to the internet.
Type-safe Kubernetes homelab with Pulumi. Zero inbound ports via Cloudflare Tunnel. Everything version-controlled.
# Preview infrastructure changes
npm run preview
# Deploy
npm run up
# Tear down
npm run destroy
# Type-check code
npm run type-check
# Lint code style
npm run lintInternet → Cloudflare Edge → Encrypted Tunnel → k3s Cluster → Your Apps
(HTTPS) (TLS, DDoS) (outbound-only) (no ports)
Key Components:
- Cloudflare Tunnel: Secure internet exposure without open ports
- cert-manager: Automatic TLS certificates (Let's Encrypt)
- Traefik Gateway API: HTTP(S) routing and load balancing with ForwardAuth
- External Secrets: Sync secrets from Pulumi ESC/Vault/AWS
homelab/ ← Pulumi project (root)
├── src/index.ts ← Orchestrator entry point
├── Pulumi.yaml ← Pulumi config
├── package.json ← Dependencies & scripts
│
└── packages/ ← Reusable libraries
├── core/
│ ├── components/ ← ExposedWebApp, HomelabContext
│ ├── config/ ← Centralized Pulumi config
│ └── infrastructure/ ← Cloudflare, cert-manager, ingress, secrets
│
├── stacks/
│ └── base-infra/ ← Orchestrates core infrastructure
│
└── demo-apps/
└── hello-world/ ← Demo / smoke-test application (extensible pattern)
- 🔒 Secure: No inbound ports, outbound-only tunnel
- 📝 Type-Safe: Pulumi + TypeScript instead of YAML
- 🧩 Modular: Reusable components and stacks
- 📦 Infrastructure-as-Code: Git-versioned, reproducible
- 🚀 Production-Ready: Tested patterns (OAuth, storage, secrets)
For real personal apps (e.g. LobeHub, Vaultwarden, Gitea) use the dedicated
digitaleraluhut/homelab-apps monorepo.
Why a separate repo?
- One repo → one secret set (
KUBECONFIG,PULUMI_ACCESS_TOKEN, Tailscale OAuth) - No need to fork upstream projects just to carry deployment code
- Clean separation: this repo is the framework and contains demo/smoke-test apps,
homelab-appsis your real apps
Structure:
homelab-apps/
├── apps/
│ └── lobehub/ ← each app is a Pulumi project
│ ├── Pulumi.yaml
│ ├── Pulumi.dev.yaml
│ └── src/index.ts ← uses @mrsimpson/homelab-core-components
└── .github/workflows/
└── deploy-lobehub.yml ← calls deploy-to-cluster.yml@main (this repo)
Bootstrap in one command:
# from this repo
./scripts/setup-homelab-apps.shAdding a new app: Use the deploy-homelab-app skill in homelab-apps/skills/ — load it in your AI agent and say "add a new app to homelab-apps". It covers workspace scaffolding, RBAC setup, CI workflow, and security considerations.
The packages/demo-apps/ folder contains lightweight demo / smoke-test framework examples:
mkdir -p packages/demo-apps/my-app/src
# Create: package.json, tsconfig.json, src/index.ts
npm installUse @mrsimpson/homelab-core-components:
import { createHomelabContextFromStack } from "@mrsimpson/homelab-core-components";
const homelab = createHomelabContextFromStack(homelabStack);
homelab.createExposedWebApp("my-app", { ... });Create new stack in packages/stacks/my-stack/ following the base-infra pattern.
Or extend packages/core/infrastructure/ for reusable modules.
- homelab-apps skills - AI agent skills for adding and configuring apps (deploy, database, oauth, secrets)
- OAuth2-Proxy Authentication - GitHub-based authentication system
- OAuth2-Proxy Examples - Step-by-step usage examples
- ADRs - Architecture decisions
- How-To Guides - Setup, deployment, operations
- Security Review - Security assessment
| What | Mechanism | Tracked in git? |
|---|---|---|
| Pulumi stack config (passwords, tokens, keys) | Pulumi Cloud AES-256-GCM encryption (secure: values) |
No — Pulumi.*.yaml is gitignored |
| Infrastructure secrets at rest (SOPS) | Age-encrypted homelab-config/pulumi-config.enc.yaml |
Yes — ciphertext only |
| OAuth2-proxy user email lists | Pulumi config (oauth2-proxy:usersEmails, oauth2-proxy:developersEmails) |
No — stored in Pulumi Cloud |
| Age private key | Never committed — lives in ~/.sops-backup/ |
No — *.age is gitignored |
# 1. Install dependencies
./homelab-config/install-dependencies.sh # pulumi, sops, age, kubectl, helm
# 2. Authenticate with Pulumi Cloud (restores all encrypted stack config)
pulumi login
pulumi stack select dev
# 3. Restore the Age key for SOPS decryption
# Standard location: ~/.config/sops/age/keys.txt
# See: https://github.com/getsops/sops?tab=readme-ov-file#22encrypting-using-age
mkdir -p ~/.config/sops/age
cp /path/to/your/backup/pulumi-homelab.age ~/.config/sops/age/keys.txt
# 4. Restore encrypted infrastructure config
./homelab-config/restore-config.sh
# 5. Set user email lists (first time only — already in Pulumi Cloud on subsequent machines)
pulumi config set --secret oauth2-proxy:usersEmails '["you@example.com"]' --stack dev
pulumi config set --secret oauth2-proxy:developersEmails '["you@example.com"]' --stack devThis homelab provides two independent authentication systems:
Protect routes with GitHub authentication for:
- External developers and contributors
- Public/semi-public applications
- Email-based allowlists
See: OAuth2-Proxy Guide
Protect routes with local user accounts for:
- Internal tools and dashboards
- Service accounts and automation
- MFA and advanced policies
Note: Routes use one or the other, never both. Choose based on your needs.
With OAuth2-Proxy (GitHub users):
filters:
- type: ExtensionRef
extensionRef:
group: traefik.io
kind: Middleware
name: forwardauth-oauth2-users
namespace: oauth2-proxy # Note: different namespace!With Authelia (Local accounts):
filters:
- type: ExtensionRef
extensionRef:
group: traefik.io
kind: Middleware
name: forwardauth-authelia
namespace: traefik-system- Node.js >=24.0.0
- k3s cluster (see
bootstrap/install-k3s.sh) - Cloudflare account (for tunnel and DNS)
- Pulumi account (for state management)
MIT