Skip to content

canesin/homelab

Repository files navigation

homelab

Opinionated "single host" homelab bootstrap that:

  • Replaces nginx with Traefik (ports 80/443)
  • Runs Pi-hole as LAN DNS (port 53) with *.example.lan wildcard pointing at this host
  • Installs a homelab CLI to manage host-local Traefik routes with metadata, TTLs, and JSON output
  • Provisions an isolated coder user for running AI coding agents safely
  • Runs everything as rootless Podman containers — no Docker required

Repo Layout

homelab/
  cli/homelab.sh         # Standalone CLI (installed to /usr/local/bin/homelab)
  systemd/               # homelab-gc.timer + .service (TTL garbage collection)
  vagrant/provision.sh   # Vagrant smoke test provisioner
  setup-homelab.sh       # Main setup: Traefik + Pi-hole + Ollama + CLI install
  setup-coder.sh         # Coder user provisioning (Podman, Node, Claude Code)
  Vagrantfile            # libvirt-based smoke tests (Ubuntu + Debian)

At runtime, /opt/homelab/services/ contains .svc files for the service registry.

Architecture

All infrastructure containers run as rootless Podman under the coder user, managed by systemd via Podman Quadlet files (~coder/.config/containers/systemd/).

┌─────────────────────────────────────────────────┐
│  coder user (rootless Podman)                   │
│                                                 │
│  homelab pod (shared network namespace)         │
│  ├─ traefik   :80, :443                        │
│  └─ pihole    :53, :8080 (web UI)              │
│                                                 │
│  ollama (standalone)  :11434                    │
└─────────────────────────────────────────────────┘
  • Podman pod for Traefik + Pi-hole — shared network namespace, ports published at the pod level
  • Ollama runs standalone (GPU lifecycle decoupled from infra)
  • Traefik file provider only — no Docker/Podman socket mounting. Dashboard, Pi-hole, and Ollama routes are static YAML files; host-local routes managed by the CLI
  • sysctl net.ipv4.ip_unprivileged_port_start=0 — allows rootless Podman to bind ports 53, 80, 443
  • Podman secrets for CF_DNS_API_TOKEN, PIHOLE_PASSWORD, TRAEFIK_DASHBOARD_USERS

Deployment Order

Run the scripts in this order on a fresh host:

# 1. Infrastructure: Traefik, Pi-hole, homelab CLI (requires Podman + crun)
sudo bash setup-homelab.sh

# 2. Coder user: isolation, toolchain, MCP tools, agent CLIs, Ollama
sudo bash setup-coder.sh

setup-homelab.sh handles all infrastructure — creates a minimal coder user if needed, writes Podman Quadlet files, starts the homelab pod. setup-coder.sh then installs the full development toolchain (Rust, Python, Node.js) plus MCP tools (cratedex, coder-mcp, qt-mcp) and Ollama.

What the coder user gets

After both scripts run, the coder user can:

  • Run Claude Code / Codex / Gemini with three MCP servers (cargo-mcp, coder-mcp, qt-mcp)
  • Use homelab dev up myapp 3000 --ttl 4h to expose dev servers on the LAN
  • Use homelab svc ls to discover Ollama, cratedex, and other registered services
  • Use Podman for containers (no Docker access)
  • Access Ollama at http://localhost:11434 or https://llm.DOMAIN

LAN-only security model

Routes created via homelab dev up or homelab add are only accessible on the LAN:

  1. DNS: Pi-hole resolves *.DOMAIN to the LAN IP (e.g. 192.168.1.100). Only devices using Pi-hole as their DNS server see these subdomains.
  2. NAT: The LAN IP is not externally routable. Your router's NAT does not forward ports 80/443 to the homelab machine (unless you explicitly configure it).
  3. TLS: Traefik obtains a Let's Encrypt wildcard certificate via Cloudflare DNS-01 challenge. Browsers trust it without warnings on the LAN.

This means an AI agent running homelab dev up myapp 3000 creates a route that is accessible to all LAN devices but not from the public internet.

What setup-homelab.sh changes

On the target machine it will:

  • Create /opt/homelab with Traefik + Pi-hole configuration
  • Create a coder user (minimal) if it doesn't exist
  • Write Podman Quadlet files to ~coder/.config/containers/systemd/
  • Create Podman secrets for CF token, Pi-hole password, dashboard auth
  • Configure Let's Encrypt wildcard TLS via Cloudflare DNS-01 challenge
  • Disable the systemd-resolved stub listener so Pi-hole can bind :53
  • Stop and disable nginx
  • Set sysctl net.ipv4.ip_unprivileged_port_start=0 for rootless port binding
  • Start the homelab pod (Traefik + Pi-hole)
  • Install the homelab CLI from cli/homelab.sh
  • Install a systemd timer for automatic TTL garbage collection
  • Optionally switch the host's DNS to Pi-hole (only if DNS verification succeeds)

Rollback is supported via --rollback.

Prerequisites

  • Debian/Ubuntu host with systemd
  • Podman >= 4.4 with crun, slirp4netns, uidmap
  • Cloudflare API Token with Zone/Read + DNS/Edit permissions (export CF_DNS_API_TOKEN=...)
  • ACME email address for Let's Encrypt notifications (export ACME_EMAIL=...)

Usage

Run as root:

sudo CF_DNS_API_TOKEN=your-token ACME_EMAIL=you@example.com bash setup-homelab.sh

Non-interactive (useful for automation):

sudo CF_DNS_API_TOKEN=your-token ACME_EMAIL=you@example.com bash setup-homelab.sh --yes

Rollback:

sudo bash setup-homelab.sh --rollback

Configuration overrides

You can override key settings via environment variables:

sudo LAN_IP=auto DOMAIN=example.lan TZ=America/Sao_Paulo bash setup-homelab.sh --yes

Notes:

  • LAN_IP=auto will auto-detect the primary IPv4 on the host.
  • TLS is handled by Let's Encrypt (wildcard cert via Cloudflare DNS-01). Browsers trust it automatically.

homelab CLI

The script installs /usr/local/bin/homelab on the target host.

Route management

homelab add <name> <port>                    # basic route
homelab add <name> <port> --owner fcc --ttl 2h  # with metadata
homelab rm <name>                            # remove route
homelab ls                                   # list routes
homelab ls --json                            # JSON output
homelab ls --owner fcc                       # filter by owner
homelab inspect <name>                       # full route metadata

Dev server lifecycle (agent-friendly)

homelab dev up <name> <port> --ttl 4h   # auto-fills owner, created, TTL
homelab dev down <name>                  # remove route
homelab dev ps                           # routes + TTL remaining + port liveness
homelab dev gc                           # remove expired TTL routes

TTL supports 30m, 2h, 1d, 4h30m formats. A systemd timer runs homelab dev gc every 15 minutes.

System info

homelab info                  # domain, LAN IP, container status, route count
homelab info --json           # JSON output
homelab status                # pod and container status
homelab logs [service]        # follow logs
homelab restart [service]     # restart stack or service
homelab password              # show Pi-hole admin password

User management

sudo homelab user add <username>   # create user in homelab group
homelab user ls                    # list homelab group members

Service registry

Discover and manage registered infrastructure services (Ollama, MCP servers, etc.):

homelab svc ls                     # list services + live status
homelab svc ls --json              # JSON output
homelab svc info <name>            # full service metadata
homelab svc start <name>           # start (systemctl start)
homelab svc stop <name>            # stop (systemctl stop)
homelab svc log <name>             # follow logs

Services are registered by setup scripts as .svc files in /opt/homelab/services/. There is no svc add/rm — the CLI reads whatever .svc files are present.

.svc file format

name=ollama
type=systemd
unit=ollama.service
user=coder
port=11434
route=llm
url=http://localhost:11434
description=LLM inference (Ollama, ROCm GPU)

Supported type values:

  • systemd — managed via systemctl (uses --user -M user@ when user= is set)
  • stdio — on-demand MCP servers (no start/stop)

Route file format

Routes are stored as .route files in /opt/homelab/traefik/dynamic/. New format:

port=8090
owner=fcc
created=2026-02-09T14:50:24Z
ttl=2h
health=

Old format (bare port number) is still readable for backward compatibility.

Global --json flag

All output commands support --json for machine-readable output. The flag can appear anywhere in the argument list.

Coder User Setup

setup-coder.sh provisions an isolated coder Linux user for AI coding agents:

sudo bash setup-coder.sh

What it sets up:

Component Details
User coder with homelab group, NOT in docker/sudo
Isolation /home/fcc is chmod 700
Containers Podman (rootless), no Docker socket
Rust rustup + stable toolchain, cratedex (systemd service)
Node.js via nvm (LTS)
Python System python3 + uv + ruff + qt-mcp
GitHub gh CLI with fine-grained PAT
Agent CLIs Claude Code (native), OpenAI Codex, Google Gemini
MCP cargo-mcp/cratedex (HTTP), coder-mcp (stdio), qt-mcp (stdio/uvx)
Ollama Podman Quadlet with ROCm GPU (standalone, not in homelab pod)
Instructions ~/.claude/CLAUDE.md with agent guidelines

Security boundaries:

                    fcc                     coder
Home dir:           /home/fcc (700)         /home/coder (750)
Docker:             YES (group)             NO
Podman:             optional                YES (rootless)
sudo:               YES                     NO
homelab group:      YES                     YES
GitHub:             full-scope token        fine-grained PAT

Vagrant Smoke Tests (libvirt)

This repo includes a basic Vagrant-based smoke test pipeline that provisions:

  • Ubuntu 24.04
  • Debian 13 ("trixie")

Requirements on the host running Vagrant:

  • vagrant
  • vagrant-libvirt
  • libvirtd running and your user allowed to use it
  • Hardware virtualization (/dev/kvm) is strongly recommended. If it's not available, the Vagrantfile falls back to QEMU emulation (much slower).

Run:

vagrant up --provider=libvirt

This will:

  • Install Podman + crun in the guest
  • Generate a local wildcard cert at /etc/ssl/cloudflare/origin.{pem,key} inside the guest
  • Run setup-homelab.sh --yes
  • Execute basic checks (podman pod ps, curl to Traefik, dig against Pi-hole)
  • Run extended CLI smoke tests (add/rm, metadata, JSON, dev up/down/ps/gc, info, inspect)

If you need different boxes, override:

UBUNTU_BOX=cloud-image/ubuntu-24.04 DEBIAN_BOX=debian/trixie64 vagrant up --provider=libvirt

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages