repo/README.md
Ezequiel C. 48c472fe6f Add cloudflare-ddns service
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-11 00:29:53 +02:00

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:

  1. OIDC (true SSO) — the app delegates login to Authelia. portainer uses this (client portainer is pre-registered in Authelia).
  2. Forward-auth (proxy gate) — the app has no OIDC, so NPM asks Authelia to authorize each request via an auth_request block before forwarding. Used by multistreaming and 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: postgres first, then forgejo with --env PG_PASSWORD=<value> taken from ~/.homelab/services/postgres/.env.
  • Update FORGEJO_ROOT_URL / FORGEJO_SSH_DOMAIN to the public domain once NPM proxies git.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 docker command available on your PATH.

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:

  1. It lists services from services/catalog.json (one request). If that file is missing it falls back to the GitHub git/trees API.
  2. Each service's metadata.json is fetched from https://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

  1. Create services/<id>/metadata.json (the folder name must equal the id).
  2. Fill in the metadata schema — see services/README.md for a walkthrough of every field. Add any extra files the service needs (a config file, a Dockerfile, …) alongside metadata.json; the installer copies them into the deploy directory, so reference them with relative bind mounts in compose.volumes.
  3. Regenerate the catalog: cd installer && bun run catalog.
  4. 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" }
  ]
}
  • compose is a standard Docker Compose service definition (image, ports, volumes, environment, …). The installer wraps it in a generated docker-compose.yml.
  • volumes / networks are optional top-level named volumes/networks to declare.
  • env declares variables the installer should resolve for you. Use ${NAME} in compose to reference them — the installer writes the resolved values to a .env file 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:...) in compose.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.