docs: migrate homelab docs from *.lthn.lan to *.lthn.sh

- Rewrite handover doc for lthn.sh with real TLS cert, proper DNS
- Add UniFi gateway DNS record reference (16 A records, no wildcards)
- Update OpenBrain usage guide to lthn.sh URLs
- Split services: *.lthn.sh (lab) vs *.infra.lthn.sh (admin tools)
- GoGetSSL wildcard cert covers lthn.sh + *.lthn.sh + *.infra.lthn.sh

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-03 13:43:39 +00:00
parent 7fe48a6268
commit e000e31567
3 changed files with 286 additions and 79 deletions

View file

@ -1,4 +1,4 @@
# lthn.lan Homelab Setup — Handover to Charon
# lthn.sh Homelab Setup — Handover to Charon
> **For:** Charon (Linux homelab agent, 10.69.69.165)
> **From:** Virgil (macOS)
@ -6,42 +6,121 @@
## Goal
Stand up the Host UK Laravel app on the Linux homelab as `lthn.lan` — a private dev/ops hub away from production. This joins the existing `.lan` service mesh (ollama.lthn.lan, qdrant.lthn.lan, eaas.lthn.lan).
Stand up the Host UK Laravel app on the Linux homelab as `hub.lthn.sh` — a private dev/ops hub away from production. This joins the existing homelab service mesh (ollama, qdrant, eaas) which is migrating from `*.lthn.lan` to `*.lthn.sh`.
## What lthn.lan Is
## Domain Strategy
The personal admin hub — issues, dashboards, agent coordination. NOT production-facing. Runs the same codebase as lthn.ai but configured for homelab use with access to local AI services.
Clean separation between production and homelab:
| Zone | Purpose | Visibility |
|------|---------|------------|
| `*.lthn.sh` | Homelab — ML, agents, lab services | Internal only |
| `*.infra.lthn.sh` | Homelab — admin/infra tools | Internal only |
| `lthn.ai` | Production — portal, forge, API | Public |
| `lthn.io` | Production — landing, service mesh | Public |
| `leth.in` | Internal prod DNS (CoreDNS on noc) | Internal |
| `lthn.host` | Shared/collab — demos, community | Public when needed |
## Architecture
```
Mac (snider) ──hosts file──▶ lthn.lan (10.69.69.165)
├── Traefik (TLS termination, self-signed)
├── Laravel app (FrankenPHP/Octane, port 80)
├── MariaDB 11 (port 3306)
└── Redis/Dragonfly (port 6379)
UniFi Gateway ──DNS──▶ 10.69.69.165 (Linux homelab)
├── Traefik (TLS via real cert, *.lthn.sh + *.infra.lthn.sh)
├── Laravel hub (FrankenPHP/Octane, port 80)
├── MariaDB 11 (port 3306)
└── Redis/Dragonfly (port 6379)
Already running on 10.69.69.165:
ollama.lthn.lan → Ollama (embeddings, LEM inference)
qdrant.lthn.lan → Qdrant (vector search)
eaas.lthn.lan → EaaS scoring API v0.2.0
ollama → Ollama (embeddings, LEM inference)
qdrant → Qdrant (vector search)
eaas → EaaS scoring API v0.2.0
```
## Prerequisites
**Hardware**: Ryzen 9, 128GB RAM, RX 7800 XT (AMD ROCm GPU)
These should already exist on the machine:
## DNS Records — UniFi Gateway
- Docker (or Podman) with Traefik v3.6+ running
- External Docker network `proxy` for Traefik
- SSH key for forge.lthn.ai (port 2223) — needed for `composer install`
No wildcard support on UniFi — each service needs an individual A record.
All point to `10.69.69.165`.
If Traefik isn't set up yet, see the existing `.lan` services for the pattern.
### Lab services (`*.lthn.sh`)
| Hostname | Service |
|----------|---------|
| `hub.lthn.sh` | Laravel admin hub |
| `lab.lthn.sh` | LEM Lab |
| `ollama.lthn.sh` | Ollama inference + embeddings |
| `qdrant.lthn.sh` | Qdrant vector search |
| `eaas.lthn.sh` | EaaS scoring API |
### Infrastructure (`*.infra.lthn.sh`)
| Hostname | Service |
|----------|---------|
| `traefik.infra.lthn.sh` | Traefik dashboard |
| `grafana.infra.lthn.sh` | Grafana |
| `prometheus.infra.lthn.sh` | Prometheus |
| `influx.infra.lthn.sh` | InfluxDB |
| `auth.infra.lthn.sh` | Authentik SSO |
| `portainer.infra.lthn.sh` | Portainer |
| `phpmyadmin.infra.lthn.sh` | phpMyAdmin |
| `maria.infra.lthn.sh` | MariaDB admin |
| `postgres.infra.lthn.sh` | PostgreSQL admin |
| `redis.infra.lthn.sh` | Redis admin |
## TLS Certificate
A real GoGetSSL cert covers all homelab domains. No self-signed certs, no TLS skip logic.
**SANs**: `lthn.sh`, `*.lthn.sh`, `*.infra.lthn.sh`
**Validity**: 3 Mar 2026 → 1 Jun 2026
### Deploy the cert to Traefik
Copy the cert + key to the homelab (from snider's Mac):
```bash
# From Mac
scp -P 4819 ~/Downloads/fullchain_lthn.sh.crt root@10.69.69.165:/opt/traefik/certs/lthn.sh.crt
scp -P 4819 ~/Downloads/lthn.sh.key root@10.69.69.165:/opt/traefik/certs/lthn.sh.key
```
Create a Traefik dynamic config file at `/opt/traefik/dynamic/lthn-sh-cert.yml`:
```yaml
tls:
certificates:
- certFile: /certs/lthn.sh.crt
keyFile: /certs/lthn.sh.key
stores:
default:
defaultCertificate:
certFile: /certs/lthn.sh.crt
keyFile: /certs/lthn.sh.key
```
Ensure Traefik's compose mounts the certs directory:
```yaml
volumes:
- /opt/traefik/certs:/certs:ro
- /opt/traefik/dynamic:/etc/traefik/dynamic:ro
```
And the static config watches for file provider changes:
```yaml
providers:
file:
directory: /etc/traefik/dynamic
watch: true
```
## Step 1: Clone the Repo
```bash
mkdir -p /opt/services/lthn-lan
cd /opt/services/lthn-lan
mkdir -p /opt/services/lthn-sh
cd /opt/services/lthn-sh
# Clone via forge SSH
git clone ssh://git@forge.lthn.ai:2223/lthn/hostuk.git app
@ -59,7 +138,7 @@ Host forge.lthn.ai
## Step 2: Create docker-compose.yml
Create `/opt/services/lthn-lan/docker-compose.yml`:
Create `/opt/services/lthn-sh/docker-compose.yml`:
```yaml
services:
@ -67,31 +146,38 @@ services:
build:
context: ./app
dockerfile: Dockerfile
container_name: lthn-lan
container_name: lthn-sh-hub
restart: unless-stopped
env_file: .env
volumes:
- app_storage:/app/storage/logs
networks:
- proxy
- lthn-lan
- lthn-sh
depends_on:
mariadb:
condition: service_healthy
labels:
traefik.enable: "true"
traefik.http.routers.lthn-sh-hub.rule: "Host(`hub.lthn.sh`)"
traefik.http.routers.lthn-sh-hub.entrypoints: websecure
traefik.http.routers.lthn-sh-hub.tls: "true"
traefik.http.services.lthn-sh-hub.loadbalancer.server.port: "80"
traefik.docker.network: proxy
mariadb:
image: mariadb:11
container_name: lthn-lan-db
container_name: lthn-sh-db
restart: unless-stopped
environment:
MARIADB_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"
MARIADB_DATABASE: lthn_lan
MARIADB_USER: lthn_lan
MARIADB_DATABASE: lthn_sh
MARIADB_USER: lthn_sh
MARIADB_PASSWORD: "${DB_PASSWORD}"
volumes:
- mariadb_data:/var/lib/mysql
networks:
- lthn-lan
- lthn-sh
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
@ -105,34 +191,34 @@ volumes:
networks:
proxy:
external: true
lthn-lan:
lthn-sh:
driver: bridge
```
## Step 3: Create .env
Create `/opt/services/lthn-lan/.env`:
Create `/opt/services/lthn-sh/.env`:
```bash
APP_NAME="LTHN Hub"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=https://lthn.lan
APP_DOMAIN=lthn.lan
APP_URL=https://hub.lthn.sh
APP_DOMAIN=lthn.sh
TRUSTED_PROXIES=*
DB_CONNECTION=mariadb
DB_HOST=lthn-lan-db
DB_HOST=lthn-sh-db
DB_PORT=3306
DB_DATABASE=lthn_lan
DB_USERNAME=lthn_lan
DB_DATABASE=lthn_sh
DB_USERNAME=lthn_sh
DB_PASSWORD=changeme-generate-a-real-one
DB_ROOT_PASSWORD=changeme-generate-a-real-one
SESSION_DRIVER=redis
SESSION_DOMAIN=.lthn.lan
SESSION_DOMAIN=.lthn.sh
SESSION_SECURE_COOKIE=true
REDIS_CLIENT=predis
@ -146,14 +232,14 @@ BROADCAST_CONNECTION=log
OCTANE_SERVER=frankenphp
# OpenBrain — connects to existing .lan services
BRAIN_OLLAMA_URL=https://ollama.lthn.lan
BRAIN_QDRANT_URL=https://qdrant.lthn.lan
# OpenBrain — connects to homelab services
BRAIN_OLLAMA_URL=https://ollama.lthn.sh
BRAIN_QDRANT_URL=https://qdrant.lthn.sh
BRAIN_COLLECTION=openbrain
BRAIN_EMBEDDING_MODEL=embeddinggemma
# EaaS scorer
EAAS_URL=https://eaas.lthn.lan
EAAS_URL=https://eaas.lthn.sh
```
Then generate the app key:
@ -161,26 +247,45 @@ Then generate the app key:
docker compose run --rm app php artisan key:generate
```
## Step 4: Traefik Labels
## Step 4: Update Existing Services
Add these labels to the `app` service in docker-compose.yml (or use a Traefik dynamic config file):
Existing services (ollama, qdrant, eaas) need their Traefik router rules updated from `*.lthn.lan` to `*.lthn.sh`. For each service, update the Host rule:
**Docker labels** (if using label-based routing):
```yaml
labels:
traefik.enable: "true"
traefik.http.routers.lthn-lan.rule: "Host(`lthn.lan`)"
traefik.http.routers.lthn-lan.entrypoints: websecure
traefik.http.routers.lthn-lan.tls: "true"
traefik.http.services.lthn-lan.loadbalancer.server.port: "80"
traefik.docker.network: proxy
# Example: ollama
traefik.http.routers.ollama.rule: "Host(`ollama.lthn.sh`)"
# Example: qdrant
traefik.http.routers.qdrant.rule: "Host(`qdrant.lthn.sh`)"
# Example: eaas
traefik.http.routers.eaas.rule: "Host(`eaas.lthn.sh`)"
```
Note: For `.lan` domains, Traefik uses self-signed certs (no Let's Encrypt — not a real TLD). The same pattern as ollama.lthn.lan/qdrant.lthn.lan/eaas.lthn.lan.
**Or** Traefik dynamic config files (if using file provider):
```yaml
# /opt/traefik/dynamic/ollama.yml
http:
routers:
ollama:
rule: "Host(`ollama.lthn.sh`)"
entryPoints: [websecure]
tls: {}
service: ollama
services:
ollama:
loadBalancer:
servers:
- url: "http://127.0.0.1:11434"
```
Infrastructure tools follow the same pattern with `*.infra.lthn.sh` hostnames.
## Step 5: Build and Start
```bash
cd /opt/services/lthn-lan
cd /opt/services/lthn-sh
# Build the image (amd64)
docker compose build
@ -209,27 +314,24 @@ docker compose ps
# Check migrations ran
docker compose logs app | grep -i migration
# Test HTTP (from the machine itself)
curl -sk https://lthn.lan/ | head -20
# Test HTTP (from any machine with DNS configured)
curl -s https://hub.lthn.sh/ | head -20
# Check Horizon (queue workers)
curl -sk https://lthn.lan/horizon/api/stats
curl -s https://hub.lthn.sh/horizon/api/stats
# Verify TLS is the real cert (not self-signed)
echo | openssl s_client -connect 10.69.69.165:443 -servername hub.lthn.sh 2>/dev/null | openssl x509 -noout -subject -dates
```
## Step 7: /etc/hosts on Mac
Already done by snider:
```
10.69.69.165 lthn.lan
```
No `-k` flag needed — the cert is real and trusted.
## Embedding Model on GPU
The `embeddinggemma` model on ollama.lthn.lan appears to be running on CPU. It's only ~256MB — should fit easily alongside whatever else is on the RX 7800 XT. Check with:
The `embeddinggemma` model on ollama may be running on CPU. It's only ~256MB — should fit easily alongside whatever else is on the RX 7800 XT. Check with:
```bash
# On the Linux machine
curl -sk https://ollama.lthn.lan/api/ps
curl -s https://ollama.lthn.sh/api/ps
```
If it shows CPU, try pulling it fresh or restarting Ollama — it should auto-detect the GPU.
@ -245,7 +347,7 @@ The app container runs 4 supervised processes:
| Scheduler | Cron loop (`schedule:run`) | — |
| Redis | In-container cache/session/queue | 6379 |
Reverb (WebSocket) is optional for lthn.lan — skip it unless needed.
Reverb (WebSocket) is optional — skip it unless needed.
## Key Artisan Commands
@ -276,15 +378,28 @@ SSH key must be available inside the container for forge access. The Dockerfile
## Known Issues
- **Self-signed TLS**: All `.lan` domains use self-signed certs. The app's `BrainService` auto-detects `.lan` URLs and skips verification. Browsers will warn — just accept.
- **Embedding 500s on large sections**: Some very large plan sections (30KB+) cause Ollama to return 500. Not critical — only 4 out of 4,671 sections affected.
- **PHP 8.5**: The Dockerfile uses PHP 8.5. Make sure the base image supports it (`dunglas/frankenphp:1-php8.5-trixie`).
## Migration Checklist — lthn.lan → lthn.sh
When switching existing services from the old `*.lthn.lan` naming:
- [ ] Deploy cert + key to `/opt/traefik/certs/`
- [ ] Create `lthn-sh-cert.yml` dynamic config
- [ ] Update Traefik router rules (Host matchers) for all services
- [ ] Configure UniFi gateway DNS (all A records → 10.69.69.165)
- [ ] Test from Mac: `curl https://hub.lthn.sh/`
- [ ] Remove old `/etc/hosts` entries for `*.lthn.lan`
- [ ] Update `php-agentic/config.php` defaults to `*.lthn.sh`
- [ ] Update `Boot.php``verifySsl` can be `true` always (real certs)
## Future: Satellite Services
Once lthn.lan is stable, the plan is to add Website/Service satellites:
- `eaas.lthn.ai` → Ethics scorer frontage
- `models.lthn.ai` → Model data + HuggingFace info
- Each feature (LEM.Lab etc) → its own subdomain via Website/Service pattern
Once hub.lthn.sh is stable, the plan is to add Website/Service satellites:
- `eaas.lthn.ai` → Ethics scorer frontage (public)
- `models.lthn.ai` → Model data + HuggingFace info (public)
- `lab.lthn.sh` → LEM Lab (internal)
- Each feature gets its own subdomain via Website/Service pattern
These are just additional `Boot.php` modules with domain patterns — the multi-tenant modular monolith handles everything from one codebase.

View file

@ -25,12 +25,14 @@ Agent ──recall()────▶ BrainService
| Service | URL | What |
|---------|-----|------|
| Ollama | `https://ollama.lthn.lan` | Embedding model (`embeddinggemma`, 768 dimensions) |
| Qdrant | `https://qdrant.lthn.lan` | Vector storage + cosine similarity search |
| MariaDB | `lthn-lan-db:3306` | `brain_memories` table (workspace-scoped) |
| Laravel | `https://lthn.lan` | BrainService, artisan commands, MCP tools |
| Ollama | `https://ollama.lthn.sh` | Embedding model (`embeddinggemma`, 768 dimensions) |
| Qdrant | `https://qdrant.lthn.sh` | Vector storage + cosine similarity search |
| MariaDB | `lthn-sh-db:3306` | `brain_memories` table (workspace-scoped) |
| Laravel | `https://hub.lthn.sh` | BrainService, artisan commands, MCP tools |
All `.lan` services use self-signed TLS behind Traefik. The app auto-skips TLS verification for `.lan` URLs.
All `*.lthn.sh` services use real TLS certs (GoGetSSL wildcard). Internal-only DNS via UniFi gateway.
> **Migration note**: Services are transitioning from `*.lthn.lan` (self-signed, /etc/hosts) to `*.lthn.sh` (real certs, proper DNS). During transition, either URL scheme works.
## Seeding Knowledge
@ -80,8 +82,8 @@ If the Laravel app isn't available, use the Go brain-seed tool:
```bash
cd ~/Code/go-ai
go run cmd/brain-seed/main.go \
--ollama=https://ollama.lthn.lan \
--qdrant=https://qdrant.lthn.lan \
--ollama=https://ollama.lthn.sh \
--qdrant=https://qdrant.lthn.sh \
--collection=openbrain \
--model=embeddinggemma
```
@ -134,19 +136,21 @@ For debugging or bulk operations:
```bash
# Collection stats
curl -sk https://qdrant.lthn.lan/collections/openbrain | python3 -m json.tool
curl -s https://qdrant.lthn.sh/collections/openbrain | python3 -m json.tool
# Raw vector search (embed query first via Ollama)
VECTOR=$(curl -sk https://ollama.lthn.lan/api/embeddings \
VECTOR=$(curl -s https://ollama.lthn.sh/api/embeddings \
-d '{"model":"embeddinggemma","prompt":"Traefik setup"}' \
| python3 -c "import sys,json; print(json.dumps(json.load(sys.stdin)['embedding']))")
curl -sk https://qdrant.lthn.lan/collections/openbrain/points/search \
curl -s https://qdrant.lthn.sh/collections/openbrain/points/search \
-H 'Content-Type: application/json' \
-d "{\"vector\":$VECTOR,\"limit\":5,\"with_payload\":true}" \
| python3 -m json.tool
```
No `-sk` flag needed — real TLS certs.
## Storing New Memories
### Via BrainService (PHP)
@ -215,7 +219,7 @@ php artisan brain:ingest --workspace=1 --fresh --source=memory
### Check Collection Health
```bash
curl -sk https://qdrant.lthn.lan/collections/openbrain | \
curl -s https://qdrant.lthn.sh/collections/openbrain | \
python3 -c "import sys,json; r=json.load(sys.stdin)['result']; print(f'Points: {r[\"points_count\"]}, Status: {r[\"status\"]}')"
```
@ -240,3 +244,4 @@ This deletes the Qdrant collection and re-creates it with fresh data.
- **Embedding model**: `embeddinggemma` (768d, ~135ms per embedding on GPU)
- **Collection status**: green
- **4 failed sections**: Oversized plan sections that exceeded Ollama's context window — not critical
- **Domain migration**: `*.lthn.lan``*.lthn.sh` (in progress)

View file

@ -0,0 +1,87 @@
# UniFi Gateway DNS Records — lthn.sh
> **For:** snider (UniFi gateway config)
> **Date:** 3 Mar 2026
## Overview
UniFi gateway doesn't support wildcard DNS. Each service needs an individual A record pointing to the homelab box (10.69.69.165).
All covered by a single GoGetSSL cert with SANs: `lthn.sh`, `*.lthn.sh`, `*.infra.lthn.sh`.
## Lab Services — `*.lthn.sh`
| Hostname | IP | Service |
|----------|-----|---------|
| `hub.lthn.sh` | 10.69.69.165 | Laravel admin hub |
| `lab.lthn.sh` | 10.69.69.165 | LEM Lab |
| `ollama.lthn.sh` | 10.69.69.165 | Ollama inference + embeddings |
| `qdrant.lthn.sh` | 10.69.69.165 | Qdrant vector search |
| `eaas.lthn.sh` | 10.69.69.165 | EaaS scoring API |
## Infrastructure — `*.infra.lthn.sh`
| Hostname | IP | Service |
|----------|-----|---------|
| `traefik.infra.lthn.sh` | 10.69.69.165 | Traefik dashboard |
| `grafana.infra.lthn.sh` | 10.69.69.165 | Grafana |
| `prometheus.infra.lthn.sh` | 10.69.69.165 | Prometheus |
| `influx.infra.lthn.sh` | 10.69.69.165 | InfluxDB |
| `auth.infra.lthn.sh` | 10.69.69.165 | Authentik SSO |
| `portainer.infra.lthn.sh` | 10.69.69.165 | Portainer |
| `phpmyadmin.infra.lthn.sh` | 10.69.69.165 | phpMyAdmin |
| `maria.infra.lthn.sh` | 10.69.69.165 | MariaDB admin |
| `postgres.infra.lthn.sh` | 10.69.69.165 | PostgreSQL admin |
| `redis.infra.lthn.sh` | 10.69.69.165 | Redis admin |
## Bare domain
| Hostname | IP | Service |
|----------|-----|---------|
| `lthn.sh` | 10.69.69.165 | Redirects to `hub.lthn.sh` (or landing page) |
## Total: 16 A records
All pointing to the same IP. Add more as new services come online.
## After UniFi Config
Once DNS is live, remove the old `/etc/hosts` entries on Mac:
```
# REMOVE these lines from /etc/hosts:
10.69.69.165 ollama.lthn.lan
10.69.69.165 qdrant.lthn.lan
10.69.69.165 eaas.lthn.lan
10.69.69.165 lthn.lan
10.69.69.165 traefik.lthn.lan
10.69.69.165 blesta.lthn.lan
10.69.69.165 auth.lthn.lan
10.69.69.165 phpmyadmin.lthn.lan
10.69.69.165 portainer.lthn.lan
10.69.69.165 grafana.lthn.lan
10.69.69.165 lab.lthn.lan
10.69.69.165 prometheus.lthn.lan
10.69.69.165 influx.lthn.lan
10.69.69.165 maria.lthn.lan
10.69.69.165 postgres.lthn.lan
10.69.69.165 redis.lthn.lan
```
Test resolution:
```bash
# Should resolve to 10.69.69.165 via UniFi DNS
dig hub.lthn.sh @<unifi-gateway-ip> +short
# Test each service
for h in hub lab ollama qdrant eaas; do
echo -n "$h.lthn.sh → "; dig $h.lthn.sh +short
done
```
## Notes
- UniFi gateway DNS serves these records to all LAN clients automatically
- No public DNS records exist for `lthn.sh` — the zone in CloudNS is empty (used only for ACME DNS-01 cert validation)
- The Mac, the Linux homelab, and any other LAN device will all resolve via UniFi
- Charon's CoreDNS on the Linux box can coexist — it handles `leth.in` (prod internal), UniFi handles `lthn.sh` (homelab)