10 KiB
homelab
A common, reusable repository for running services on a personal homelab.
Every service lives in its own folder under services/ and is described by a
metadata.json file (plus any extra files the service needs, such as a config file). The
installer/ directory contains a Bun + TypeScript
terminal UI (TUI) that reads the catalog straight from GitHub (used as the CDN) and installs or
updates services with Docker.
homelab/
├── services/ # one folder per service
│ ├── catalog.json # generated index used by the installer (CDN listing)
│ ├── nginx-proxy-manager/ # reverse proxy dashboard (NPM)
│ │ └── metadata.json
│ ├── cloudflare-ddns/ # keeps ezequielcf.dev + *.ezequielcf.dev on the public IP
│ │ └── metadata.json
│ ├── authelia/ # SSO / IdP (login portal, OIDC, forward-auth)
│ │ ├── metadata.json
│ │ ├── configuration.yml
│ │ └── users_database.yml
│ ├── lldap/ # lightweight LDAP user store
│ │ └── metadata.json
│ ├── postgres/ # shared PostgreSQL (database for Forgejo & future apps)
│ │ └── metadata.json
│ ├── forgejo/ # self-hosted Git (web + SSH), uses postgres
│ │ └── metadata.json
│ ├── multistreaming/ # self-built: OBS ingest -> multi-platform re-streaming
│ │ ├── metadata.json
│ │ ├── Dockerfile
│ │ ├── src/
│ │ └── web/
│ ├── portainer/ # Docker + network manager (VLANs, static IPs)
│ │ └── metadata.json
│ └── ...
├── installer/ # Bun + TypeScript TUI
│ ├── src/
│ └── scripts/
└── docs/
└── metadata-schema.json
Identity stack (SSO)
Every web service logs in through the same IdP (authelia), which authenticates
users against lldap and sits behind nginx-proxy-manager:
browser ──▶ nginx-proxy-manager ──▶ authelia ──LDAP──▶ lldap
Two integration modes, depending on what each app supports:
- OIDC (true SSO) — the app delegates login to Authelia.
portaineruses this (clientportaineris pre-registered in Authelia). - Forward-auth (proxy gate) — the app has no OIDC, so NPM asks Authelia to authorize each
request via an
auth_requestblock before forwarding. Used bymultistreamingand lldap's web UI.
| Service | SSO mode |
|---|---|
| Portainer | OIDC (native) |
| multistreaming panel | forward-auth (no OIDC) |
| lldap web UI | forward-auth (no OIDC) |
| Nginx Proxy Manager | its own login (no OIDC; keep port 81 restricted) |
Authelia also exposes OIDC discovery at https://auth.example.com/.well-known/openid-configuration
for any future OIDC-capable app. Install order: lldap, authelia, nginx-proxy-manager:
bun run src/index.ts install lldap authelia nginx-proxy-manager
Git (Forgejo + PostgreSQL)
forgejo is the self-hosted git server, backed by the shared postgres
service. Both join the homelab docker network; PostgreSQL publishes no host port (docker-internal only).
- Web UI:
http://10.0.0.5:3000(LAN only) — first visit creates the admin account. - Git over SSH:
ssh://git@10.0.0.5:2222/<owner>/<repo>.git(container SSH on host port 2222; host sshd keeps port 22). - Registration is disabled; users come through the identity stack (Authelia SSO) later.
- Install order:
postgresfirst, thenforgejowith--env PG_PASSWORD=<value>taken from~/.homelab/services/postgres/.env. - Update
FORGEJO_ROOT_URL/FORGEJO_SSH_DOMAINto the public domain once NPM proxiesgit.example.com.
Network & DMZ topology
portainer is the Docker/network manager: from its dashboard you can create
macvlan/ipvlan networks (each mapped to a host VLAN via its parent interface) and assign each
container a static IP. A typical setup keeps only the reverse proxy in the DMZ and everything
else on separate VLANs:
internet ──▶ DMZ VLAN (e.g. 10.0.10.0/24)
└── nginx-proxy-manager (the only public entrypoint)
│
├─▶ services VLAN (10.0.20.0/24) ── multistreaming, …
└─▶ identity VLAN (10.0.30.0/24) ── authelia, lldap
To make a VLAN usable as a macvlan parent, the tagged sub-interface must first exist on the host
(e.g. eth0.10, configured in /etc/network/interfaces or netplan — outside Docker's scope).
Then create the network in Portainer and attach services to it with the IPs you want.
Quick start
# 1. Install Bun: https://bun.sh
# 2. Install the installer's dependencies
cd installer
bun install
# 3. Launch the TUI
bun run src/index.ts
The TUI lets you browse the catalog, install services, update them, uninstall them, and inspect
what is currently running. Services are deployed with docker compose into ~/.homelab/services/<id>/
(a docker-compose.yml and a .env are generated from each metadata.json).
Requirement: Docker must be installed and the
dockercommand available on yourPATH.
Non-interactive CLI
The same app works as a plain CLI:
bun run src/index.ts list
bun run src/index.ts info nginx-proxy-manager
bun run src/index.ts install nginx-proxy-manager
bun run src/index.ts update # update everything installed
bun run src/index.ts update nginx-proxy-manager # update one service
bun run src/index.ts uninstall nginx-proxy-manager
bun run src/index.ts status
Useful flags:
| Flag | Meaning |
|---|---|
--local <path> |
Read services from a local folder instead of GitHub (handy while developing a new service). |
--owner/--repo/--branch |
Override the GitHub source (also available as HOMELAB_OWNER, HOMELAB_REPO, HOMELAB_BRANCH). |
--env KEY=VALUE |
Provide an environment value non-interactively (repeatable). |
--yes |
Never prompt; use defaults or provided --env values. |
--dry-run |
Print what would happen (including the generated compose file) without touching Docker. |
--force |
Re-run an update even if versions already match. |
--verbose |
Stream docker compose output instead of only showing failures. |
How the CDN works
The installer treats this GitHub repository as its content delivery network:
- It lists services from
services/catalog.json(one request). If that file is missing it falls back to the GitHubgit/treesAPI. - Each service's
metadata.jsonis fetched fromhttps://raw.githubusercontent.com/<owner>/<repo>/<branch>/services/<id>/metadata.json.
Pointing the installer at your own fork is just a matter of setting --owner/--repo (or the
HOMELAB_* environment variables). For a private repository, export a GitHub token and the
installer authenticates its API and raw requests automatically: HOMELAB_GITHUB_TOKEN (or
GITHUB_TOKEN / HOMELAB_TOKEN). The repository must be pushed — the installer reads from GitHub,
not from your local checkout.
Forgejo (self-hosted): set HOMELAB_API_BASE and HOMELAB_RAW_BASE (any value other than the
GitHub defaults switches the installer to Forgejo URL/API patterns). Example — the homelab's own
Forgejo at http://10.0.0.5:3000 (repo Homelab/repo, branch master):
export HOMELAB_OWNER=Homelab HOMELAB_REPO=repo HOMELAB_BRANCH=master
export HOMELAB_API_BASE=http://10.0.0.5:3000/api/v1
export HOMELAB_RAW_BASE=http://10.0.0.5:3000
bun run src/index.ts list
Adding a new service means adding a folder plus its metadata.json and regenerating the catalog:
cd installer
bun run catalog
Adding a service
- Create
services/<id>/metadata.json(the folder name must equal theid). - Fill in the metadata schema — see
services/README.mdfor a walkthrough of every field. Add any extra files the service needs (a config file, aDockerfile, …) alongsidemetadata.json; the installer copies them into the deploy directory, so reference them with relative bind mounts incompose.volumes. - Regenerate the catalog:
cd installer && bun run catalog. - Commit and push. The installer will now offer the new service.
Service metadata at a glance
metadata.json is the single source of truth for a service. The essential shape:
{
"id": "nginx-proxy-manager",
"name": "Nginx Proxy Manager",
"description": "Reverse proxy with a web dashboard",
"version": "1.0.0",
"category": "network",
"compose": {
"image": "jc21/nginx-proxy-manager:2.14.0",
"container_name": "nginx-proxy-manager",
"restart": "unless-stopped",
"ports": ["80:80", "443:443", "81:81"],
"volumes": ["npm_data:/data", "npm_letsencrypt:/etc/letsencrypt"]
},
"volumes": { "npm_data": {}, "npm_letsencrypt": {} },
"env": [
{ "name": "TZ", "label": "Timezone", "default": "UTC" }
]
}
composeis a standard Docker Compose service definition (image, ports, volumes, environment, …). The installer wraps it in a generateddocker-compose.yml.volumes/networksare optional top-level named volumes/networks to declare.envdeclares variables the installer should resolve for you. Use${NAME}incomposeto reference them — the installer writes the resolved values to a.envfile next to the compose file.- Any other files in the service folder (e.g.
configuration.yml,users_database.yml) are copied into the deploy directory, so you can mount them with relative paths (./configuration.yml:...) incompose.volumes.
See docs/metadata-schema.json for the complete, machine-readable schema.
License
This repository is a personal template. Service entries reference their upstream projects; each service's own license applies to the software it deploys.