# KiteStacks Homelab — Complete Setup Runbook **Last Updated:** 2026-06-18 **Status:** Production (monk primary, kscloud1 Hetzner cloud replica) **Maintainer:** kenpat --- ## Architecture Overview ``` Internet │ └── Cloudflare (DNS + Tunnel) │ Active-Active across 2 connectors ├── cloudflared on monk (primary home machine, Docker container) └── cloudflared on kscloud1 (Hetzner VPS, ) Tailscale overlay network (VPN mesh): monk kscloud1 ← hosts shared Authentik Postgres + Redis ``` **Public subdomains** route through the same Cloudflare Tunnel token. Both monk and kscloud1 are connectors so the site stays up if either goes offline. | Subdomain | Service | Port | |-----------|---------|------| | auth.kitestacks.com | Authentik | 9000 | | portainer.kitestacks.com | Portainer | 9443 | | wiki.kitestacks.com | BookStack | 6875 (monk) / 6877 (kscloud1) | | grafana.kitestacks.com | Grafana | 3000 | | gitforge.kitestacks.com | Forgejo | 3006 | | links.kitestacks.com | Karakeep | 3100 | | status.kitestacks.com | Uptime Kuma | 3001 | | tasks.kitestacks.com | OSTicket | 8080 | | flux.kitestacks.com | FluxCD | — | --- ## Service Inventory ### Running on monk authentik, authentik-worker, authentik-ldap, authentik-ldap-proxy, bookstack, bookstack-db, cloudflared, flux, forgejo, grafana, karakeep, karakeep-chrome, karakeep-meilisearch, kavita, kite-litellm, kite-openwebui, kitestacks-metrics-api, kitestacks-portal, node-exporter, ntfy, osticket, osticket-app, osticket-db, portainer, prometheus, uptime-kuma, blackbox-exporter ### Running on kscloud1 (extras) bookstack, bookstack-db-ks, kite-monitor, osticket-app-118, osticket-db-118, www-backup, homepage-backup, cloudflared, authentik-postgresql, authentik-redis ### Shared infrastructure on kscloud1 - PostgreSQL `:5432` — Authentik DB used by both hosts (Tailscale only) - Redis `:6379` — Authentik session cache (Tailscale only) --- ## Cloudflare Tunnel ### How it works Both monk and kscloud1 run `cloudflared` as Docker containers using the **same tunnel token**. Cloudflare load-balances across both connectors (active-active). The tunnel token is stored in: - monk: `~/kitestacks-live/docker/cloudflared/.env` → `TUNNEL_TOKEN` - kscloud1: `/opt/kitestacks/docker/cloudflared/.env` → `TUNNEL_TOKEN` ### Fix: Phantom 3rd Replica If `cloudflared tunnel info` shows 3 connectors instead of 2, the native cloudflared systemd service on monk is running alongside the Docker container. ```bash # Check systemd cloudflared on monk systemctl status cloudflared # Disable it — Docker container is the correct one sudo systemctl disable --now cloudflared ``` ### Adding a new hostname route In Cloudflare Zero Trust → Networks → Tunnels → your tunnel → Edit → Public Hostname: - Subdomain: `newservice` - Domain: `kitestacks.com` - Service: `http://container-name:port` Both monk and kscloud1 must have the container running on the same port. --- ## Authentik SSO ### Architecture Authentik uses a **shared database** hosted on kscloud1. monk's Authentik containers connect via Tailscale. - monk containers: `authentik`, `authentik-worker`, `authentik-ldap`, `authentik-ldap-proxy` - DB: PostgreSQL on kscloud1 at `:5432` - Redis: kscloud1 at `:6379` ### Adding OIDC SSO for a new app 1. In Authentik admin (`https://auth.kitestacks.com/if/admin/`): - **Providers** → Create → OAuth2/OpenID Provider - Name the provider after the app (e.g. `bookstack`) - Set `issuer_mode` based on the app's requirements (see Debug doc) - Note the Client ID and Client Secret 2. **Application** → Create → link to the provider 3. **Policy Binding** → bind the `default-authentication-flow` to the application 4. Configure the app with: - `OIDC_ISSUER` = discovery base URL - `OIDC_CLIENT_ID` / `OIDC_CLIENT_SECRET` - Callback URL = `https://yourapp.kitestacks.com/auth/callback` ### Checking OIDC discovery URL ```bash # Per-provider (issuer_mode=per_provider) curl -s https://auth.kitestacks.com/application/o//.well-known/openid-configuration | python3 -m json.tool # Global (issuer_mode=global) # Note: global issuer URL does NOT serve a JSON discovery doc at /.well-known/ # Use per-provider mode for apps that auto-discover endpoints (BookStack, etc.) ``` ### Changing provider issuer_mode via SQL ```bash docker run --rm --network host \ -e PGPASSWORD="" \ postgres:16 psql -h -U authentik authentik -c \ "UPDATE authentik_providers_oauth2_oauth2provider SET issuer_mode='per_provider' WHERE provider_ptr_id=;" ``` --- ## Portainer ### OAuth setup (Authentik) Portainer CE uses AuthenticationMethod=3 (OAuth). Configured via the BoltDB. Key settings: - `OAuthLoginURI`: `https://auth.kitestacks.com/application/o/authorize/` - `OAuthTokenURI`: `https://auth.kitestacks.com/application/o/token/` - `OAuthUserURI`: `https://auth.kitestacks.com/application/o/userinfo/` - `OAuthClientID`: `portainer` - `OAuthRedirectURI`: `https://portainer.kitestacks.com` - `OAuthAutoCreateUsers`: `true` - `OAuthDefaultTeamID`: `0` ### Pre-creating an admin user before first OAuth login OAuth auto-created users default to Role:2 (regular user) and can't see environments. Pre-create them as Role:1 (admin) via the API before they log in: ```bash # Get auth token TOKEN=$(curl -sk -X POST https://portainer.kitestacks.com/api/auth \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":""}' | python3 -c "import sys,json; print(json.load(sys.stdin)['jwt'])") # Create user as admin (Role:1), no password needed for OAuth users curl -sk -X POST "https://portainer.kitestacks.com/api/users" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"username":"user@example.com","role":1}' ``` ### Reset admin password (if locked out) ```bash # Stop Portainer docker stop portainer # Reset password (shows new temp password) docker run --rm -v portainer_data:/data portainer/helper-reset-password # Restart docker start portainer ``` --- ## BookStack ### Setup (both monk and kscloud1) Location: - monk: `~/kitestacks-live/docker/bookstack/docker-compose.yml` - kscloud1: `/opt/kitestacks/docker/bookstack/docker-compose.yml` Key environment variables: ```yaml - APP_URL=https://wiki.kitestacks.com - DB_HOST=bookstack-db - AUTH_METHOD=oidc - OIDC_ISSUER=https://auth.kitestacks.com/application/o/bookstack/ - OIDC_ISSUER_DISCOVER=true - OIDC_CLIENT_ID=bookstack - OIDC_CLIENT_SECRET= - OIDC_USER_ATTRIBUTE=email - APP_KEY= ``` ### Generate APP_KEY ```bash docker run --rm --entrypoint /bin/bash lscr.io/linuxserver/bookstack:latest appkey ``` ### OIDC Configuration BookStack uses `OIDC_ISSUER_DISCOVER=true` to auto-discover all endpoints from Authentik. The `OIDC_ISSUER` must match the per-app discovery URL base (not the global Authentik URL). The Authentik bookstack provider must have `issuer_mode='per_provider'` so its discovery document returns the correct per-app issuer URL. See Debug doc for full troubleshooting. ### Fix cache permissions after artisan runs Running `php artisan` as root creates root-owned cache dirs that block the app: ```bash docker exec bookstack chown -R abc:users /config/www/framework/cache/ ``` ### Clear Laravel config/cache ```bash docker exec bookstack php /app/www/artisan config:clear docker exec bookstack php /app/www/artisan cache:clear ``` --- ## kscloud1 Access ### SSH ```bash ssh -i ~/.ssh/id_ed25519_kscloud1 root@ ``` ### If SSH key is lost / not working 1. Open Hetzner Cloud console: `console.hetzner.cloud` → your server → Console tab 2. Log in as `root` (Linux user password) 3. Serve the key from monk over Tailscale: ```bash # On monk — start temporary HTTP server cat ~/.ssh/id_ed25519_kscloud1.pub > ~/key.txt python3 -m http.server 7777 --directory ~/ ``` 4. In Hetzner console, type: ```bash curl http://:7777/key.txt > /root/.ssh/authorized_keys ``` 5. Enable root SSH (if needed): ```bash sed -i 's/^#*PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config systemctl restart ssh ``` --- ## OSTicket SMTP **Config:** smtp.gmail.com:587, STARTTLS **From:** `kitestacks.helpdesk@gmail.com` (app password stored in DB) To test email delivery: Admin Panel → Diagnostics → Send Test Email --- ## Forgejo Runs on monk at `localhost:3006` (port 2222 for SSH git). ### Generate API token for automation ```bash docker exec -u git forgejo forgejo admin user generate-access-token \ --username kenpat --token-name "my-token" --raw \ --scopes "read:user,write:user,read:repository,write:repository" ``` --- ## Common Docker Operations ```bash # View logs for a service docker logs --tail 50 -f # Restart a service cd ~/kitestacks-live/docker/ && docker compose restart # Full stack restart docker compose down && docker compose up -d # Update a container image docker compose pull && docker compose up -d # Check all running containers docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" ```