Reorganize repos into kitestacks-homelab + plain-English doc rewrite
- Rewrote RUNBOOK.md and DEBUG-DOCUMENTATION.md in simple 5th-grade language with real-world analogies for every technical concept - Updated README.md with current service inventory and folder map - Added cloud-migration/ subdirectory (from kitestacks-cloud-migration repo) - Added autosync/ subdirectory (from kitestacks-homelab-autosync-test repo) - Added osticket/ subdirectory (from OSTicketSystem repo) - Added cloud/ placeholder for future cloud configs - Excluded binary DB/postgres files from autosync subdirectory Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f79478158d
commit
fb822d5142
75 changed files with 11711 additions and 338 deletions
359
RUNBOOK.md
359
RUNBOOK.md
|
|
@ -1,221 +1,174 @@
|
|||
# KiteStacks Homelab — Complete Setup Runbook
|
||||
# KiteStacks Homelab — How Everything Works
|
||||
|
||||
**Last Updated:** 2026-06-18
|
||||
**Status:** Production (monk primary, kscloud1 Hetzner cloud replica)
|
||||
**Maintainer:** kenpat
|
||||
**Last Updated:** 2026-06-18
|
||||
**Status:** Up and running
|
||||
**Owner:** kenpat
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
## The Big Picture
|
||||
|
||||
KiteStacks is a personal homelab — a small set of programs (called "services") that run
|
||||
on two computers. One computer sits at home (called **monk**), and one rents space in
|
||||
a data center in Germany (called **kscloud1**).
|
||||
|
||||
People on the internet can reach every website without knowing where the computers are,
|
||||
because all traffic goes through **Cloudflare** — a free service that acts like a secret
|
||||
post-office. Cloudflare knows the address; the rest of the world doesn't.
|
||||
|
||||
```
|
||||
Internet
|
||||
│
|
||||
└── Cloudflare (DNS + Tunnel)
|
||||
│ Active-Active across 2 connectors
|
||||
├── cloudflared on monk (primary home machine, Docker container)
|
||||
└── cloudflared on kscloud1 (Hetzner VPS, <KSCLOUD1_PUBLIC_IP>)
|
||||
|
||||
Tailscale overlay network (VPN mesh):
|
||||
monk <MONK_TAILSCALE_IP>
|
||||
kscloud1 <KSCLOUD1_TAILSCALE_IP> ← hosts shared Authentik Postgres + Redis
|
||||
You (browser)
|
||||
│
|
||||
└─► Cloudflare (the post office)
|
||||
│
|
||||
├─► monk (home machine, runs most services)
|
||||
└─► kscloud1 (cloud backup machine in Germany)
|
||||
```
|
||||
|
||||
**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 | — |
|
||||
If monk goes offline, kscloud1 keeps serving the sites — Cloudflare automatically
|
||||
switches traffic over. This is called **active-active** (both doors are always open).
|
||||
|
||||
---
|
||||
|
||||
## Service Inventory
|
||||
## What Runs Where
|
||||
|
||||
### 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
|
||||
### Services on monk
|
||||
| What it is | What it does | Website |
|
||||
|------------|--------------|---------|
|
||||
| Authentik | Login manager — handles all usernames and passwords | auth.kitestacks.com |
|
||||
| Portainer | Dashboard to manage all the Docker containers | portainer.kitestacks.com |
|
||||
| Forgejo | Git — stores all the code and scripts | gitforge.kitestacks.com |
|
||||
| BookStack | Wiki — where all the notes and guides live | wiki.kitestacks.com |
|
||||
| Grafana | Charts showing how healthy the servers are | grafana.kitestacks.com |
|
||||
| Karakeep | Saves and organizes bookmarks | links.kitestacks.com |
|
||||
| Kavita | Reads ebooks and manga | kavita.kitestacks.com |
|
||||
| OSTicket | Help-desk ticket system | tasks.kitestacks.com |
|
||||
| Open WebUI | Chat with AI models (GPT, Claude, local models) | ai.kitestacks.com |
|
||||
| Uptime Kuma | Watches every service and alerts if one goes down | status.kitestacks.com |
|
||||
| KiteStacks Portal | The main homepage with links to everything | www.kitestacks.com |
|
||||
|
||||
### 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)
|
||||
### Services on kscloud1 (cloud backup)
|
||||
- A copy of BookStack
|
||||
- A copy of the main Portal
|
||||
- The login database (PostgreSQL) and session memory (Redis) that Authentik uses
|
||||
- The Cloudflare connector (so the site keeps running if monk is off)
|
||||
|
||||
---
|
||||
|
||||
## Cloudflare Tunnel
|
||||
## Cloudflare Tunnel (the secret post office)
|
||||
|
||||
### 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`
|
||||
### Why it exists
|
||||
Normal websites need a router setting called "port forwarding" and a fixed home IP address.
|
||||
Cloudflare Tunnel removes both requirements — monk connects **out** to Cloudflare, and
|
||||
Cloudflare forwards visitor traffic back in. Your home address is never exposed.
|
||||
|
||||
### 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.
|
||||
### How to check it's healthy
|
||||
Go to Cloudflare Zero Trust → Networks → Tunnels. You should see **2 healthy connectors**
|
||||
(one from monk, one from kscloud1).
|
||||
|
||||
```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
|
||||
### Adding a new website
|
||||
In Cloudflare Zero Trust → Networks → Tunnels → your tunnel → Edit → Public Hostname:
|
||||
- Subdomain: `newservice`
|
||||
- Domain: `kitestacks.com`
|
||||
- Service: `http://container-name:port`
|
||||
- Service URL: `http://container-name:port`
|
||||
|
||||
Both monk and kscloud1 must have the container running on the same port.
|
||||
Both monk and kscloud1 need to be running that container on the same port.
|
||||
|
||||
---
|
||||
### Fix: If you see 3 connectors instead of 2
|
||||
The old cloudflared system service on monk is probably running alongside the Docker one.
|
||||
Run this on monk to fix it:
|
||||
|
||||
## 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 `<KSCLOUD1_TAILSCALE_IP>:5432`
|
||||
- Redis: kscloud1 at `<KSCLOUD1_TAILSCALE_IP>: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/<slug>/.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="<REDACTED>" \
|
||||
postgres:16 psql -h <KSCLOUD1_TAILSCALE_IP> -U authentik authentik -c \
|
||||
"UPDATE authentik_providers_oauth2_oauth2provider SET issuer_mode='per_provider' WHERE provider_ptr_id=<ID>;"
|
||||
sudo systemctl disable --now cloudflared
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Portainer
|
||||
## Authentik (the login manager)
|
||||
|
||||
### OAuth setup (Authentik)
|
||||
Portainer CE uses AuthenticationMethod=3 (OAuth). Configured via the BoltDB.
|
||||
### What it does
|
||||
Every website on KiteStacks uses Authentik for login. Instead of each website having its
|
||||
own username and password, Authentik is the one source of truth. You log in once and
|
||||
all the websites trust that login. This system is called **SSO** (Single Sign-On).
|
||||
|
||||
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`
|
||||
### Where the database lives
|
||||
Authentik's user database lives on **kscloud1** (not on monk). Both machines share it
|
||||
through a private encrypted network called **Tailscale**.
|
||||
|
||||
### 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:
|
||||
### Adding a new app to SSO
|
||||
|
||||
1. Go to `https://auth.kitestacks.com/if/admin/`
|
||||
2. **Providers** → Create → OAuth2/OpenID Provider
|
||||
3. Name it after the app (e.g., `myapp`)
|
||||
4. Note the Client ID and Client Secret
|
||||
5. **Application** → Create → link it to the provider
|
||||
6. Set up the app with:
|
||||
- Login URL (your app's OIDC issuer URL)
|
||||
- Client ID and Client Secret
|
||||
- Callback URL: `https://yourapp.kitestacks.com/auth/callback`
|
||||
|
||||
---
|
||||
|
||||
## Portainer (the container dashboard)
|
||||
|
||||
### What it does
|
||||
Portainer is a web dashboard that shows all running Docker containers. Think of Docker
|
||||
containers like small self-contained boxes — each one runs one program. Portainer lets
|
||||
you start, stop, restart, and view logs for all the boxes without typing commands.
|
||||
|
||||
### If you get locked out
|
||||
```bash
|
||||
# Stop Portainer
|
||||
docker stop portainer
|
||||
|
||||
# Reset the password (the command will print a new temporary password)
|
||||
docker run --rm -v portainer_data:/data portainer/helper-reset-password
|
||||
|
||||
# Start it again
|
||||
docker start portainer
|
||||
```
|
||||
|
||||
### First-time OAuth login issue
|
||||
When someone logs into Portainer through Authentik for the first time, they get created
|
||||
as a regular user (not admin). They won't be able to see any servers. To fix this,
|
||||
create their account as admin **before** their first login:
|
||||
|
||||
```bash
|
||||
# Get auth token
|
||||
# Step 1: Get a login token
|
||||
TOKEN=$(curl -sk -X POST https://portainer.kitestacks.com/api/auth \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"<REDACTED>"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['jwt'])")
|
||||
-d '{"username":"admin","password":"YOUR_PASSWORD"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['jwt'])")
|
||||
|
||||
# Create user as admin (Role:1), no password needed for OAuth users
|
||||
# Step 2: Create the user as admin (role 1 = admin)
|
||||
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
|
||||
## BookStack (the wiki)
|
||||
|
||||
### Setup (both monk and kscloud1)
|
||||
Location:
|
||||
- monk: `~/kitestacks-live/docker/bookstack/docker-compose.yml`
|
||||
- kscloud1: `/opt/kitestacks/docker/bookstack/docker-compose.yml`
|
||||
### What it does
|
||||
BookStack is a self-hosted wiki — like a private Wikipedia just for this homelab.
|
||||
All notes, runbooks, and guides live here.
|
||||
|
||||
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=<REDACTED>
|
||||
- OIDC_USER_ATTRIBUTE=email
|
||||
- APP_KEY=<REDACTED>
|
||||
```
|
||||
### Important settings
|
||||
BookStack uses Authentik for login. Two settings must be correct:
|
||||
|
||||
### Generate APP_KEY
|
||||
```bash
|
||||
docker run --rm --entrypoint /bin/bash lscr.io/linuxserver/bookstack:latest appkey
|
||||
```
|
||||
- `OIDC_ISSUER_DISCOVER=true` — tells BookStack to automatically find all login endpoints
|
||||
- `OIDC_ISSUER` — must point to the per-app Authentik URL, like:
|
||||
`https://auth.kitestacks.com/application/o/bookstack/`
|
||||
|
||||
### 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).
|
||||
### Fix: If cache breaks after running a PHP command
|
||||
Sometimes running admin commands inside the container breaks file permissions:
|
||||
|
||||
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
|
||||
### Clear BookStack's config cache
|
||||
```bash
|
||||
docker exec bookstack php /app/www/artisan config:clear
|
||||
docker exec bookstack php /app/www/artisan cache:clear
|
||||
|
|
@ -223,27 +176,27 @@ docker exec bookstack php /app/www/artisan cache:clear
|
|||
|
||||
---
|
||||
|
||||
## kscloud1 Access
|
||||
## kscloud1 (the cloud backup machine)
|
||||
|
||||
### SSH
|
||||
### SSH access
|
||||
```bash
|
||||
ssh -i ~/.ssh/id_ed25519_kscloud1 root@<KSCLOUD1_TAILSCALE_IP>
|
||||
ssh -i ~/.ssh/id_ed25519_kscloud1 root@KSCLOUD1_TAILSCALE_IP
|
||||
```
|
||||
|
||||
### 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:
|
||||
### If you can't SSH in (key was lost)
|
||||
|
||||
1. Open Hetzner Cloud console → your server → **Console** tab (this is like a TV remote for the server)
|
||||
2. Log in as `root` using the Linux root password
|
||||
3. On monk, share your public SSH key temporarily:
|
||||
```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:
|
||||
4. In the Hetzner console, type:
|
||||
```bash
|
||||
curl http://<MONK_TAILSCALE_IP>:7777/key.txt > /root/.ssh/authorized_keys
|
||||
curl http://MONK_TAILSCALE_IP:7777/key.txt > /root/.ssh/authorized_keys
|
||||
```
|
||||
5. Enable root SSH (if needed):
|
||||
5. If root SSH is disabled:
|
||||
```bash
|
||||
sed -i 's/^#*PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
|
||||
systemctl restart ssh
|
||||
|
|
@ -251,43 +204,67 @@ ssh -i ~/.ssh/id_ed25519_kscloud1 root@<KSCLOUD1_TAILSCALE_IP>
|
|||
|
||||
---
|
||||
|
||||
## OSTicket SMTP
|
||||
## OSTicket (help desk)
|
||||
|
||||
**Config:** smtp.gmail.com:587, STARTTLS
|
||||
**From:** `kitestacks.helpdesk@gmail.com` (app password stored in DB)
|
||||
OSTicket is the ticket/task system at `tasks.kitestacks.com`.
|
||||
Emails sent to `kitestacks.helpdesk@gmail.com` become tickets automatically.
|
||||
|
||||
To test email delivery: Admin Panel → Diagnostics → Send Test Email
|
||||
To test that email is working: Admin Panel → Diagnostics → Send Test Email
|
||||
|
||||
---
|
||||
|
||||
## Forgejo
|
||||
## Forgejo (code storage)
|
||||
|
||||
Runs on monk at `localhost:3006` (port 2222 for SSH git).
|
||||
Forgejo is the Git server — all scripts, configs, and docs live here.
|
||||
|
||||
### Generate API token for automation
|
||||
### Create an API token for automation
|
||||
```bash
|
||||
docker exec -u git forgejo forgejo admin user generate-access-token \
|
||||
--username kenpat --token-name "my-token" --raw \
|
||||
--username kenpat \
|
||||
--token-name "my-token" \
|
||||
--raw \
|
||||
--scopes "read:user,write:user,read:repository,write:repository"
|
||||
```
|
||||
|
||||
Note: SSH to gitforge.kitestacks.com only works from inside the local network,
|
||||
not through Cloudflare (Cloudflare blocks non-HTTPS ports).
|
||||
For git operations from monk, use `ssh://git@localhost:2222/kenpat/repo.git`.
|
||||
|
||||
---
|
||||
|
||||
## Common Docker Operations
|
||||
## Everyday Docker Commands
|
||||
|
||||
```bash
|
||||
# View logs for a service
|
||||
docker logs <container> --tail 50 -f
|
||||
# See all running containers and their status
|
||||
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||||
|
||||
# View recent logs for a service
|
||||
docker logs CONTAINER_NAME --tail 50 -f
|
||||
|
||||
# Restart a service
|
||||
cd ~/kitestacks-live/docker/<service> && docker compose restart
|
||||
cd ~/kitestacks-live/docker/SERVICE_NAME
|
||||
docker compose restart
|
||||
|
||||
# Full stack restart
|
||||
# Stop and restart a service (harder reset)
|
||||
docker compose down && docker compose up -d
|
||||
|
||||
# Update a container image
|
||||
# Pull latest image and restart
|
||||
docker compose pull && docker compose up -d
|
||||
|
||||
# Check all running containers
|
||||
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tailscale (the private tunnel between machines)
|
||||
|
||||
Tailscale creates an encrypted private network between monk and kscloud1.
|
||||
Nothing on this network is visible to the public internet.
|
||||
|
||||
Used for:
|
||||
- monk connecting to kscloud1's PostgreSQL and Redis (for Authentik)
|
||||
- SSH from monk to kscloud1
|
||||
- Prometheus on monk scraping metrics from kscloud1
|
||||
|
||||
To check connection status:
|
||||
```bash
|
||||
tailscale status
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue