homelab/README.md

9.4 KiB

homelab

Infrastructure-as-Code for a 3-machine homelab running K3s.

Architecture

Machine IP Role
Minisforum UM780 XTX 192.168.7.77 K3s control-plane
nik-debian (HP ProDesk) 192.168.7.183 K3s storage agent
Mac Mini M2 192.168.7.96 Standalone Docker (outside cluster)

Roadmap

Phase Description Status
P0 CA web installer (ca.home.arpa) Done
P1 Prometheus + Grafana + Loki Done
P2 Authentik SSO Done
P3 Photoview Done
P4 Home Assistant 🔜 Planned
P5 Portfolio site (nik4nao.com) Done
P6 WireGuard split tunnel 🔜 Planned

Live Services

Service URL TLS Notes
Traefik dashboard https://traefik.home.arpa Internal CA Protected by Authentik
Authentik https://auth.home.arpa / https://auth.nik4nao.com Internal CA / Let's Encrypt SSO for all services
Gitea https://gitea.nik4nao.com Let's Encrypt Git + Docker registry
Pi-hole (primary) https://pihole.home.arpa Internal CA DNS, runs on Minisforum
Pi-hole (secondary) externalIPs on Debian (192.168.7.183)
Grafana https://grafana.nik4nao.com Let's Encrypt Protected by Authentik OIDC
Prometheus internal kube-prometheus-stack
Loki + Promtail internal Log aggregation
Jellyfin https://jellyfin.home.arpa Internal CA Media server, NFS storage
qBittorrent https://qbittorrent.home.arpa Internal CA /mnt/storage/torrents
JDownloader https://jdownloader.home.arpa Internal CA /mnt/storage/dl
Dashy https://dashy.home.arpa Internal CA Config via ConfigMap
Glances https://glances.home.arpa Internal CA DaemonSet + Debian Docker
Photoview https://photoview.home.arpa Internal CA NFS photo gallery
Watch Party https://watch-party.nik4nao.com Let's Encrypt Mac Mini, CI/CD deployed
Portfolio https://nik4nao.com Let's Encrypt Hugo + terminal theme
CA installer http://ca.home.arpa Internal CA cert download page

Auth

  • SSO: Authentik at auth.home.arpa (internal) / auth.nik4nao.com (public)
  • Protected services: Traefik dashboard, Grafana (OIDC), Gitea (OIDC)
  • MFA: TOTP enforced, 8hr sessions
  • Users: nik (admin), akadmin (break-glass)
  • Gitea: local login disabled, OIDC only

TLS

  • *.home.arpa — internal self-signed CA (cert-manager). Install CA cert via http://ca.home.arpa
  • *.nik4nao.com — Let's Encrypt via HTTP-01 (Traefik)

Repo Structure

ansible/
  inventory.yaml
  playbooks/
    bootstrap-minisforum.yaml   # OS hardening, packages, UFW, /data dirs
    deploy-watch-party.yaml     # deploy watch-party app on Mac Mini
    join-debian-agent.yaml      # join Debian as K3s agent
    setup-gitea-runner.yaml     # Gitea Actions runner (act_runner systemd)
    setup-glances-debian.yaml   # Glances on Debian host
    setup-k3s.yaml              # K3s server install, Helm, kubeconfig
    setup-monitoring.yaml       # Prometheus + Grafana + Loki stack
    setup-nfs-debian.yaml       # NFS server on Debian
  roles/
    common/                     # user, SSH hardening, UFW, base packages
    gitea-runner/               # act_runner v0.2.11 systemd service
    glances/                    # Glances system monitor
    k3s-agent/                  # K3s agent node join
    k3s-server/                 # K3s server install + Helm
    monitoring/                 # Prometheus/Grafana stack
    nfs-server/                 # NFS server configuration
    watch-party/                # Watch Party Docker Compose on Mac Mini
config/
  dashy/conf.yaml               # Dashy dashboard config (applied via ConfigMap)
manifests/
  authentik/                    # Ingress, middleware, proxy outpost, secrets
  cert-manager/                 # ClusterIssuers, porkbun-secret.sh
  core/                         # Dashy, Glances, CA installer, CoreDNS config
  gitea/                        # PV, runner, backup CronJob, public ingress
  media/                        # Jellyfin, qBittorrent, JDownloader, Photoview
  monitoring/                   # Grafana/Loki datasource ConfigMap, PVs, grafana-secret.sh
  network/                      # DDNS CronJob, Traefik dashboard, pihole-debian-patch.sh
  portfolio/                    # Portfolio deployment, registry pull secret
values/
  authentik.yaml
  cert-manager.yaml
  gitea.yaml
  kube-prometheus-stack.yaml
  loki-stack.yaml
  pihole.yaml
  pihole-debian.yaml
  traefik.yaml

Prerequisites

  • Ansible on workstation: pip install ansible
  • Ansible collections: ansible-galaxy collection install community.general ansible.posix
  • SSH key: ~/.ssh/id_ed25519-nik-macbookair
  • kubectl + helm installed

Connecting

# SSH
ssh minisforum        # port 430, via ~/.ssh/config
ssh nik-debian        # port 22

# Kubectl
export KUBECONFIG=/tmp/k3s-minisforum.yaml
kubectl get nodes
kubectl get pods -A

Deploying / Re-deploying

Ansible (host-level)

# Bootstrap Minisforum
ansible-playbook -i ansible/inventory.yaml ansible/playbooks/bootstrap-minisforum.yaml

# K3s server
ansible-playbook -i ansible/inventory.yaml ansible/playbooks/setup-k3s.yaml

# Join Debian as agent
ansible-playbook -i ansible/inventory.yaml ansible/playbooks/join-debian-agent.yaml

# NFS on Debian
ansible-playbook -i ansible/inventory.yaml ansible/playbooks/setup-nfs-debian.yaml

# Gitea Actions runner
ansible-playbook -i ansible/inventory.yaml ansible/playbooks/setup-gitea-runner.yaml

# Glances on Debian
ansible-playbook -i ansible/inventory.yaml ansible/playbooks/setup-glances-debian.yaml

# Watch Party on Mac Mini
ansible-playbook -i ansible/inventory.yaml ansible/playbooks/deploy-watch-party.yaml

Helm (cluster services)

# Traefik
helm repo add traefik https://helm.traefik.io/traefik && helm repo update
helm upgrade --install traefik traefik/traefik \
  --namespace traefik --create-namespace -f values/traefik.yaml

# cert-manager
helm repo add jetstack https://charts.jetstack.io && helm repo update
helm upgrade --install cert-manager jetstack/cert-manager \
  --namespace cert-manager --create-namespace -f values/cert-manager.yaml

# Gitea
helm repo add gitea-charts https://dl.gitea.com/charts/ && helm repo update
helm upgrade --install gitea gitea-charts/gitea \
  --namespace gitea --create-namespace -f values/gitea.yaml

# Pi-hole (Minisforum)
helm repo add mojo2600 https://mojo2600.github.io/pihole-kubernetes/ && helm repo update
helm upgrade --install pihole mojo2600/pihole \
  --namespace pihole --create-namespace -f values/pihole.yaml
# Note: re-run manifests/network/pihole-debian-patch.sh after every Pi-hole upgrade
#       (externalIPs for Debian secondary are lost on upgrade)

# Authentik
helm repo add authentik https://charts.goauthentik.io && helm repo update
helm upgrade --install authentik authentik/authentik \
  --namespace authentik --create-namespace -f values/authentik.yaml

# kube-prometheus-stack
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts && helm repo update
helm upgrade --install kube-prometheus-stack prometheus-community/kube-prometheus-stack \
  --namespace monitoring --create-namespace -f values/kube-prometheus-stack.yaml

# Loki
helm repo add grafana https://grafana.github.io/helm-charts && helm repo update
helm upgrade --install loki grafana/loki-stack \
  --namespace monitoring --create-namespace -f values/loki-stack.yaml

Secrets (create before applying manifests)

# Porkbun API (cert-manager DNS-01)
bash manifests/cert-manager/porkbun-secret.sh

# Grafana admin password
bash manifests/monitoring/grafana-secret.sh

# DDNS credentials
bash manifests/network/ddns-secret.sh

# Gitea Actions runner token
bash manifests/gitea/runner-secret.sh

Known Gotchas

  • Gitea ROOT_URL: changing ROOT_URL in values/gitea.yaml is not enough — must also delete the gitea-inline-config secret and re-run helm upgrade. Disable the built-in ingress (ingress.enabled=false) and use the manual IngressRoute in manifests/gitea/.
  • Pihole secondary externalIPs: lost on every Helm upgrade — re-run manifests/network/pihole-debian-patch.sh after each upgrade.
  • Prometheus hostPath: /data/prometheus requires chmod -R 777 (owned by UID 65534).
  • Grafana PVC: use local-path dynamic provisioning — do not use a static hostPath PV, K3s overrides storageClassName: "".
  • Loki datasource: provisioned via labeled ConfigMap (grafana_datasource), not the Grafana UI — the bundled plugin in loki-stack v2.9.3 is incompatible with Grafana 12.
  • Authentik forwardAuth: Cookie header must be in authRequestHeaders in the Traefik middleware or you get an infinite redirect loop after login.
  • Traefik v3 api@internal: requires both traefik and websecure entrypoints in the IngressRoute, otherwise 404.
  • CoreDNS custom config: use .server suffix for zone blocks. .override suffix cannot contain zone {} syntax — crashes CoreDNS.
  • Photoview video: PHOTOVIEW_DISABLE_VIDEO=true only takes effect on a fresh scan — delete the SQLite DB and restart before rescanning.
  • CI/CD Buildkit CA: internal CA must be injected into the buildx_buildkit_multiarch0 container on every CI run (does not persist across restarts).
  • Pihole DNS: no wildcard support — every new home.arpa subdomain needs an explicit entry in values/pihole.yaml.