Opinionated "single host" homelab bootstrap that:
- Replaces nginx with Traefik (ports 80/443)
- Runs Pi-hole as LAN DNS (port 53) with
*.example.lanwildcard pointing at this host - Installs a
homelabCLI to manage host-local Traefik routes with metadata, TTLs, and JSON output - Provisions an isolated
coderuser for running AI coding agents safely - Runs everything as rootless Podman containers — no Docker required
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.
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
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.shsetup-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.
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 4hto expose dev servers on the LAN - Use
homelab svc lsto discover Ollama, cratedex, and other registered services - Use Podman for containers (no Docker access)
- Access Ollama at
http://localhost:11434orhttps://llm.DOMAIN
Routes created via homelab dev up or homelab add are only accessible on the LAN:
- DNS: Pi-hole resolves
*.DOMAINto the LAN IP (e.g.192.168.1.100). Only devices using Pi-hole as their DNS server see these subdomains. - 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).
- 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.
On the target machine it will:
- Create
/opt/homelabwith Traefik + Pi-hole configuration - Create a
coderuser (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=0for rootless port binding - Start the homelab pod (Traefik + Pi-hole)
- Install the
homelabCLI fromcli/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.
- 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=...)
Run as root:
sudo CF_DNS_API_TOKEN=your-token ACME_EMAIL=you@example.com bash setup-homelab.shNon-interactive (useful for automation):
sudo CF_DNS_API_TOKEN=your-token ACME_EMAIL=you@example.com bash setup-homelab.sh --yesRollback:
sudo bash setup-homelab.sh --rollbackYou can override key settings via environment variables:
sudo LAN_IP=auto DOMAIN=example.lan TZ=America/Sao_Paulo bash setup-homelab.sh --yesNotes:
LAN_IP=autowill 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.
The script installs /usr/local/bin/homelab on the target host.
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 metadatahomelab 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 routesTTL supports 30m, 2h, 1d, 4h30m formats. A systemd timer runs homelab dev gc every 15 minutes.
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 passwordsudo homelab user add <username> # create user in homelab group
homelab user ls # list homelab group membersDiscover 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 logsServices 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.
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 viasystemctl(uses--user -M user@whenuser=is set)stdio— on-demand MCP servers (no start/stop)
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.
All output commands support --json for machine-readable output. The flag can appear anywhere in the argument list.
setup-coder.sh provisions an isolated coder Linux user for AI coding agents:
sudo bash setup-coder.shWhat 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
This repo includes a basic Vagrant-based smoke test pipeline that provisions:
- Ubuntu 24.04
- Debian 13 ("trixie")
Requirements on the host running Vagrant:
vagrantvagrant-libvirtlibvirtdrunning 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=libvirtThis 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,curlto Traefik,digagainst 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