Commit 29e97f

2026-06-15 09:53:04 Damien: Add homelab wiki — Infrastructure, Docker Swarm, Traefik, Network/Media/Documents/AI/Downloads stacks, Game Servers
/dev/null .. AI Stack.md
@@ 0,0 1,182 @@
+ # AI Stack
+
+ ← [[Home]]
+
+ Services running on PCT 109 (ai, 192.168.2.83). Privileged LXC, NVIDIA GPU passthrough. **Not a Docker Swarm node** — all services are standalone Docker Compose.
+
+ ---
+
+ ## Service URLs
+
+ | Service | URL | Port |
+ |---------|-----|------|
+ | LiteLLM | `litellm.carr-family.org` | 4000 (lan-only) |
+ | n8n | `n8n.carr-family.org` | 5678 (lan-only) |
+ | OpenClaw | `openclaw.carr-family.org` | 18789 (lan-only) |
+ | Odysseus | `odysseus.carr-family.org` | 7000 (lan-only) |
+ | Firecrawl API | — | `192.168.2.83:3002` |
+ | Whisper | — | `192.168.2.83:8001` |
+ | Proton Bridge SMTP | — | `192.168.2.83:1025` |
+ | Proton Bridge IMAP | — | `192.168.2.83:1143` |
+
+ ---
+
+ ## Key Paths
+
+ | Path | Contents |
+ |------|----------|
+ | `/usr/local/bin/hermes` | Hermes agent binary |
+ | `/root/.hermes/config.yaml` | Hermes config (model, MCP servers) |
+ | `/opt/litellm/config.yaml` | LiteLLM model list + master key |
+ | `/opt/litellm/docker-compose.yml` | LiteLLM + Postgres compose |
+ | `/opt/ai-tools/docker-compose.yml` | n8n + Qdrant compose |
+ | `/opt/n8n/data` | n8n workflows, credentials |
+ | `/opt/qdrant/data` | Qdrant vector store |
+ | `/opt/firecrawl/` | Firecrawl compose project |
+ | `/root/.openclaw/openclaw.json` | OpenClaw config |
+ | `/opt/odysseus/` | Odysseus compose project |
+ | `/opt/whisper/` | Whisper compose project |
+ | `/opt/proton-bridge/` | Proton Bridge compose project |
+ | `/mnt/obsidian/` | Obsidian vault (read-write bind mount) |
+
+ ---
+
+ ## LiteLLM
+
+ OpenAI-compatible proxy at `litellm.carr-family.org/ui`. Compose: `/opt/litellm/`.
+
+ - **Master key:** `sk-homelab-litellm-admin`
+ - **UI login:** admin / `sk-homelab-litellm-admin`
+ - **DB:** postgres:16-alpine (host port 5433)
+ - **Restart:** `pct exec 109 -- bash -c "cd /opt/litellm && docker compose up -d"`
+
+ **Models:**
+
+ | Model | Backend |
+ |-------|---------|
+ | `gpt-4o`, `gpt-4o-mini`, `gpt-4.1`, `gpt-4.1-mini` | OpenAI (direct) |
+ | `llama3.1:8b`, `llama3.2:3b` | Ollama @ 192.168.2.40:11434 (CPU) |
+ | `qwen3.6:27b`, `qwen3.5`, `ministral-3`, `llama3.2`, `llama3.1:8b-gpu` | Ollama @ 192.168.2.11:11434 (RTX 3060) |
+ | `llava` | Ollama @ 192.168.2.11 — vision/OCR (paperless-gpt uses `llava:7b`) |
+ | `nomic-embed-text` | Ollama @ 192.168.2.11 — embeddings for Qdrant |
+
+ ---
+
+ ## n8n
+
+ Workflow automation at `n8n.carr-family.org`. Compose: `/opt/ai-tools/` (n8n + Qdrant).
+
+ - **Data:** `/opt/n8n/data`
+ - **Restart:** `pct exec 109 -- bash -c "cd /opt/ai-tools && docker compose up -d"`
+ - **Vault mount:** `/mnt/obsidian:/mnt/obsidian:ro` inside container
+ - **Code node:** `NODE_FUNCTION_ALLOW_BUILTIN=fs,path` — required to read vault files
+
+ **Credentials in n8n:**
+
+ | Name | Type | Value |
+ |------|------|-------|
+ | Qdrant Local | Qdrant API | `http://qdrant:6333` (no API key) |
+ | Ollama .11 | Ollama | `http://192.168.2.11:11434` |
+ | LiteLLM OpenAI | OpenAI | Key: `sk-homelab-litellm-admin`, Base: `http://192.168.2.83:4000` |
+
+ **Claude Code MCP:** `n8n-mcp` server in `~/.claude/mcp.json` on Proxmox host. URL: `https://n8n.carr-family.org/mcp-server/http`. Reload by restarting Claude Code.
+
+ **Key Workflows:**
+ - *Obsidian Vault — Ingest to Qdrant* — reads all `.md` from vault, chunks + embeds into Qdrant. Run manually to re-index.
+ - *Obsidian Vault — Chat* — chat UI backed by Qdrant RAG + gpt-4o via LiteLLM.
+
+ ---
+
+ ## Qdrant
+
+ Vector store. Part of `/opt/ai-tools/docker-compose.yml`.
+
+ - **Ports:** 6333 (HTTP/REST), 6334 (gRPC)
+ - **Data:** `/opt/qdrant/data`
+ - **Collection:** `obsidian_vault` (vector size: 192, distance: Cosine, model: `nomic-embed-text`)
+ - **Check:** `curl http://192.168.2.83:6333/collections/obsidian_vault`
+
+ ---
+
+ ## Hermes Agent
+
+ AI agent with MCP access to the Obsidian vault.
+
+ - **Binary:** `/usr/local/bin/hermes`
+ - **Default model:** `gpt-4o` via LiteLLM
+ - **MCP server:** `obsidian-vault` — `mcp-server-filesystem /mnt/obsidian` (read/write/search)
+ - **Manage MCP:** `hermes mcp ls/add/remove`
+ - **Config:** `/root/.hermes/config.yaml`, `/root/.hermes/.env`
+
+ ---
+
+ ## OpenClaw
+
+ Claude AI desktop client. Version `2026.5.27`.
+
+ - **UI:** `openclaw.carr-family.org` (lan-only)
+ - **Gateway port:** 18789, systemd service: `openclaw-gateway.service`
+ - **Restart:** `openclaw daemon restart`
+ - **Config:** `/root/.openclaw/openclaw.json`
+
+ **Model Providers:**
+
+ | Provider | Backend | Models |
+ |----------|---------|--------|
+ | `openai` | OpenAI API (direct) | gpt-4o, gpt-4o-mini, gpt-4.1, gpt-4.1-mini |
+ | `ollama-11` | Ollama @ 192.168.2.11 (RTX 3060) | qwen3.6:27b, qwen3.5, ministral-3, llama3.2, llama3.1:8b, llava:7b |
+ | `ollama-40` | Ollama @ 192.168.2.40 (CPU) | llama3.1:8b, llama3.2:3b |
+
+ **Cron Jobs:**
+
+ | Job | Schedule | Model | Discord Channel |
+ |-----|----------|-------|-----------------|
+ | `homelab-self-heal` | every 15 min | `openai/gpt-4o-mini` | `1509897448645201981` |
+ | `archibus-daily-digest` | 08:00 Mon–Fri | `ollama-11/qwen3.6:27b` | `1491754165335101460` |
+
+ ---
+
+ ## Odysseus
+
+ Self-hosted AI workspace at `odysseus.carr-family.org`. Port 7000.
+
+ - **Compose:** `/opt/odysseus/docker-compose.yml`
+ - **Data:** `/opt/odysseus/data/` (SQLite, uploads, ChromaDB)
+ - **Bundled:** ChromaDB (127.0.0.1:8100), SearXNG (127.0.0.1:8080), ntfy (127.0.0.1:8091)
+ - **LLM:** via LiteLLM proxy (`http://host.docker.internal:4000/v1`)
+ - **Restart:** `pct exec 109 -- bash -c "cd /opt/odysseus && docker compose restart"`
+ - **Update:** `pct exec 109 -- bash -c "cd /opt/odysseus && git pull && docker compose up -d --build"`
+
+ ---
+
+ ## Firecrawl
+
+ Web scraping / crawling API.
+
+ - **API:** `http://192.168.2.83:3002`
+ - **API key:** `fc-1d4542a7babd39e33b04ce858af45295`
+ - **Compose:** `/opt/firecrawl/`
+ - **Restart:** `pct exec 109 -- bash -c "cd /opt/firecrawl && docker compose up -d"`
+
+ ---
+
+ ## Whisper Transcription
+
+ `faster-whisper-server` — OpenAI-compatible `/v1/audio/transcriptions`. Used by the Audio Recorder.
+
+ - **Port:** 8001 (host-mode)
+ - **Model:** `base` (cached at `/tank/appdata/whisper/models/`)
+ - **Compose:** `/opt/whisper/docker-compose.yml`
+ - **Restart:** `pct exec 109 -- bash -c "cd /opt/whisper && docker compose restart"`
+
+ ---
+
+ ## Proton Bridge (SMTP/IMAP Relay)
+
+ Headless Proton Bridge for outbound email from gcjobs-filler, Calibre-Web, etc.
+
+ - **SMTP:** `192.168.2.83:1025` (STARTTLS)
+ - **IMAP:** `192.168.2.83:1143` (STARTTLS)
+ - **Credentials:** `[email protected]` / `8A2SC9qao04GsSqBrfjtFg`
+ - **Compose:** `/opt/proton-bridge/docker-compose.yml`
+ - **Restart:** `pct exec 109 -- bash -c "rm -f /opt/proton-bridge/data/.cache/protonmail/bridge-v3/bridge-v3.lock && cd /opt/proton-bridge && docker compose restart"`
/dev/null .. Docker Swarm.md
@@ 0,0 1,109 @@
+ # Docker Swarm
+
+ ← [[Home]]
+
+ Four-node Swarm cluster managed from PCT 108 (network, 192.168.2.82). PCT 109 is **not** a Swarm node.
+
+ | Node | Hostname | IP | Role |
+ |------|----------|----|------|
+ | PCT 101 | downloads | 192.168.2.190 | Worker |
+ | PCT 102 | media-core | 192.168.2.191 | Worker |
+ | PCT 104 | documents | 192.168.2.105 | Worker |
+ | PCT 107 | debian | 192.168.2.81 | Worker |
+ | PCT 108 | network | 192.168.2.82 | **Manager** |
+
+ ---
+
+ ## Key Commands
+
+ All Swarm commands run from the Proxmox host via `pct exec 108 --`.
+
+ ```bash
+ # Status
+ pct exec 108 -- docker node ls
+ pct exec 108 -- docker stack ls
+ pct exec 108 -- docker service ls
+
+ # Inspect a service
+ pct exec 108 -- docker service ps <service> # failed replica history
+ pct exec 108 -- docker service logs <service> --tail 50
+
+ # Deploy / update
+ pct exec 108 -- docker stack deploy -c <compose> <stack>
+ pct exec 108 -- docker service update --force <service> # restart + fresh overlay IP
+
+ # Check containers on a specific node
+ pct exec 102 -- docker ps -a --filter name=<name>
+ ```
+
+ ---
+
+ ## Compose Files
+
+ Stored in Portainer on PCT 108:
+ ```
+ /mnt/tank/appdata/portainer/compose/<id>/docker-compose.yml
+ ```
+
+ **Important:** Portainer stores stack env vars in its internal DB (`portainer.db`) — there are no `.env` files on disk. When redeploying via CLI, export vars manually:
+ ```bash
+ pct exec 108 -- bash -c 'export VAR=val && docker stack deploy -c <compose> <stack>'
+ ```
+
+ ### Portainer Stack Directory → Stack Mapping
+
+ | Dir ID | Stack |
+ |--------|-------|
+ | 2 | documents-linkwarden |
+ | 4 | documents-paperless |
+ | 6 | network-guacamole |
+ | 7 | documents-nextcloud |
+ | 10 | documents-immich |
+ | 22 | documents-homarr |
+ | 23 | authentik |
+ | 25 | dozzel |
+ | 26 | media-core |
+ | 28 | documents-actual-budget |
+ | 31 | network-cloudbeaver |
+ | 32 | romm |
+
+ ---
+
+ ## Swarm Labels vs Container Labels
+
+ In Docker Swarm, **only `deploy.labels` are read by Traefik** — container-level `labels:` are ignored.
+
+ ```yaml
+ services:
+ myservice:
+ deploy:
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.myservice.rule=Host(`myservice.carr-family.org`)"
+ ```
+
+ ---
+
+ ## Overlay Network Health Check
+
+ Services on PCT 102 can develop stale VXLAN attachments after redeployments, causing 504s even though the service shows `1/1`.
+
+ ```bash
+ TRAEFIK=$(pct exec 108 -- docker ps -q --filter name=traefik)
+ IP=$(pct exec 102 -- docker inspect <container-name> --format '{{(index .NetworkSettings.Networks "traefik-public").IPAddress}}')
+ pct exec 108 -- docker exec $TRAEFIK ping -c 2 $IP
+ # Fix:
+ pct exec 108 -- docker service update --force <service-name>
+ ```
+
+ ---
+
+ ## Watchtower
+
+ Automatic image updates daily at 04:00 AM. See [[Network Stack]] for full config.
+
+ **Excluding a container** (required for `network_mode: "service:..."` deps):
+ ```yaml
+ labels:
+ - "com.centurylinklabs.watchtower.enable=false"
+ ```
/dev/null .. Documents Stack.md
@@ 0,0 1,113 @@
+ # Documents Stack
+
+ ← [[Home]]
+
+ Services running on PCT 104 (documents, 192.168.2.105). Docker storage on ZFS (`/tank/docker`).
+
+ ---
+
+ ## Service URLs
+
+ | Service | URL | Port | Stack |
+ |---------|-----|------|-------|
+ | Immich | `photos.carr-family.org` | 2283 | `documents-immich` (compose 10) |
+ | Nextcloud | `cloud.carr-family.org` | 8085 | `documents-nextcloud` (compose 7) |
+ | Paperless-ngx | `paperless.carr-family.org` | 8000 | `documents-paperless` (compose 4) |
+ | paperless-gpt | `paperless-gpt.carr-family.org` | 8080 | `documents-paperless` (compose 4) |
+ | Linkwarden | `links.carr-family.org` | 3000 | `documents-linkwarden` (compose 2) |
+ | Actual Budget | `actual.carr-family.org` | 5006 | `documents-actual-budget` (compose 28) |
+ | OtterWiki | `otterwiki.carr-family.org` | 8081 | standalone (not Swarm) |
+
+ ---
+
+ ## Immich (compose 10)
+
+ Photo management at `photos.carr-family.org`. Services: immich-server, machine-learning, postgres/pgvecto.rs (port 5435), valkey.
+
+ GPU: uses `-cuda` image (`ghcr.io/immich-app/immich-machine-learning:release-cuda`) with `DEVICE=cuda` + `NVIDIA_VISIBLE_DEVICES=all`.
+
+ **Redeploy** (env vars not in .env — export in shell):
+ ```bash
+ pct exec 108 -- bash -c 'export DB_PASSWORD=T5dBbAvgHWceH7jhsxjism4b2Cre9NA DB_USERNAME=immich DB_DATABASE_NAME=immich && docker stack deploy -c /mnt/tank/appdata/portainer/compose/10/docker-compose.yml documents-immich'
+ ```
+
+ ---
+
+ ## Nextcloud (compose 7)
+
+ File cloud at `cloud.carr-family.org`. Services: app (port 8085), cron, postgres:16 (port 5432).
+
+ - Nextcloud data migrated to ZFS 2026-05-25 — stored at `/mnt/tank/appdata/nextcloud/` inside PCT 104
+ - `PUID/PGID: 33:33`
+
+ ---
+
+ ## Paperless-ngx (compose 4)
+
+ Document management at `paperless.carr-family.org`. Services: webserver (port 8000), paperless-gpt (port 8080), redis, postgres (port 5433).
+
+ **Redeploy** (env vars not in .env):
+ ```bash
+ pct exec 108 -- bash -c 'export POSTGRES_DB=paperless POSTGRES_USER=paperless POSTGRES_PASSWORD=eWJarhDasRNBu0LfBwuI6VPOXwnRCUy PAPERLESS_SECRET_KEY=J60Om5l2dsL1pUSWz3AQhqizRFhlii8dRzsynYrAfzbBsV316S PAPERLESS_TIME_ZONE=America/Chicago PAPERLESS_URL=https://paperless.carr-family.org "PAPERLESS_CSRF_TRUSTED_ORIGINS=https://paperless.carr-family.org" && docker stack deploy -c /mnt/tank/appdata/portainer/compose/4/docker-compose.yml documents-paperless'
+ ```
+
+ **Postgres password gotcha** — `POSTGRES_PASSWORD` only applies on first-init. If the DB is recreated with existing data, a password mismatch causes `password authentication failed` crash-loop. Fix via local socket (trust auth):
+ ```bash
+ DB=$(pct exec 104 -- docker ps --format '{{.Names}}' | grep 'paperless_db\.' | head -1)
+ pct exec 104 -- docker exec $DB psql -U paperless -d paperless -c "ALTER USER paperless PASSWORD 'newpassword'"
+ pct exec 108 -- docker service update --force documents-paperless_webserver
+ ```
+
+ ### paperless-gpt
+
+ LLM-powered title/tag suggestions + OCR at `paperless-gpt.carr-family.org` (port 8080).
+
+ - General LLM: `gpt-4o-mini` via LiteLLM (`http://192.168.2.83:4000`)
+ - OCR LLM: `llava:7b` via LiteLLM → Ollama on `192.168.2.11`
+ - Tag a document `paperless-gpt-ocr` to trigger OCR processing
+ - API token: `71c8ba36985118d419cf9a1f5f8497290d816e10`
+
+ ---
+
+ ## Linkwarden (compose 2)
+
+ Bookmark manager at `links.carr-family.org`. Services: linkwarden (port 3000), meilisearch, postgres (port 5434).
+
+ **API token:**
+ ```
+ eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..jE4UUgAXi4UpbmBi.1HbSWJSBdDAHur9EAI28WT2Cbt4rJqzakS_RrjQq9jT34iRJBb7nEW97Ml218sECdaShktMrdxAXrl6zQFXBkM4G2jpWtSEOUlIKlOH0v1KAtXRfpb9b.tNDkqQ1JdOz6gALxNDKhVA
+ ```
+
+ **PUT /api/v1/links/:id gotcha** — payload must include `collection: {id, name, ownerId}` and tags as `[{id, name}]`. Omitting either returns `400`.
+
+ **Auto-sort script:** runs on PCT 109 every 2 minutes — `/opt/linkwarden-sort/sort.py`. Moves links out of Unorganized (id 1) based on AI tags, URL patterns, title keywords. Log: `/var/log/linkwarden-sort.log`.
+
+ **Collection IDs:**
+
+ | ID | Name |
+ |----|------|
+ | 1 | Unorganized |
+ | 4 | Minecraft |
+ | 7 | 3D-Printing |
+ | 8 | Homelab |
+ | 9 | Self-Host |
+ | 10 | Tabletop/Board Games |
+ | 11 | Video Games |
+ | 14 | Art |
+ | 17 | Satisfactory |
+ | 18 | Dungeons and Dragons |
+ | 19 | Pokemon |
+
+ ---
+
+ ## OtterWiki (standalone — not Swarm)
+
+ This wiki. Docker Compose on PCT 104 at `/mnt/tank/appdata/otterwiki/docker-compose.yml`.
+
+ - **Image:** `redimp/otterwiki:2`
+ - **Port:** 8081 (host-mode)
+ - **Static route:** `routes.yml` → `http://192.168.2.105:8081`
+ - **Appdata:** `/mnt/tank/appdata/otterwiki/` — contains `settings.cfg` and `repository/` (git repo, all pages as `.md` files)
+ - **PUID/PGID:** `33:33`
+
+ Restart: `pct exec 104 -- docker compose -f /mnt/tank/appdata/otterwiki/docker-compose.yml restart`
/dev/null .. Downloads.md
@@ 0,0 1,80 @@
+ # Downloads
+
+ ← [[Home]]
+
+ Services running on PCT 101 (downloads, 192.168.2.190). All standalone containers — not Swarm-managed.
+
+ ---
+
+ ## Services
+
+ | Container | URL | Port | Purpose |
+ |-----------|-----|------|---------|
+ | `qbittorrent-mam` | `qbittorrent.carr-family.org` | 8080 | MyAnonamouse torrent client |
+ | `qbittorrent-vpn` | `qbittorrent-vpn.carr-family.org` | 8081 | VPN-routed general torrents |
+ | `gluetun-proton` | — | — | ProtonVPN gateway for qbittorrent-vpn |
+ | `qui` | `qui.carr-family.org` | 7476 | Multi-instance qBittorrent web UI |
+
+ Compose files on PCT 101: `/root/stacks/qbittorrent-mam/docker-compose.yml`, `/root/stacks/bittorrent-vpn/docker-compose.yml`
+
+ ---
+
+ ## qbittorrent-mam
+
+ MAM (MyAnonamouse) torrent client. Image: `linuxserver/qbittorrent:4.6.7`.
+
+ - **WebUI:** `192.168.2.190:8080`, creds: `admin / 32Ab0321!!`
+ - **Volume:** `/mnt/tank/media:/data` (only — bookingest accessible as `/data/bookingest` via this mount; separate mount breaks hardlinks)
+ - **Config:** `/mnt/tank/appdata/qbittorrent/`
+
+ **Tuned settings (4-core Xeon E5-1620 v2, 8 GB RAM, 1 Gbps):**
+ - `max_active_downloads`: 70
+ - `disk_cache`: 512 MB
+ - `max_connec`: 1000, `max_connec_per_torrent`: 200
+
+ ### Categories
+
+ | Category | Save Path | Used By |
+ |----------|-----------|---------|
+ | `MAM` | `/data/content/books-seeds` | Direct MAM ebook downloads |
+ | `audiobooks` | `/data/content/audiobooks` | Audiobookshelf library |
+ | `books-shelfmark` | `/data/bookingest` (default) | Shelfmark ebooks |
+
+ ### On-completion Hook
+
+ Script: `/mnt/tank/appdata/qbittorrent/scripts/hardlink-to-ingest.sh` — fires only for `MAM` category. Hardlinks by extension, preserving directory structure:
+
+ - `.epub/.mobi/.azw3/.pdf` etc. → `/data/bookingest` (CWA ingest)
+ - `.m4b/.mp3/.m4a/.aax` etc. → `/data/content/audiobooks` (Audiobookshelf)
+ - `.cbz/.cbr/.cb7/.cbt` etc. → `/data/content/comics` (Komga)
+
+ Hardlinks work because `bookingest` and `books-seeds` are both on the `tank/media` ZFS dataset.
+
+ ### Fixing Incorrect Save Paths
+
+ The API `setLocation` returns 200 but silently fails. Reliable method:
+ 1. `docker kill --signal SIGKILL qbittorrent-mam`
+ 2. Patch `qBt-savePath` AND `save_path` in `/mnt/tank/appdata/qbittorrent/qBittorrent/BT_backup/<hash>.fastresume`
+ 3. Delete all `*.fastresume.*` stale backup files
+ 4. `docker start qbittorrent-mam`
+ 5. Force-recheck the affected torrents
+
+ ---
+
+ ## qbittorrent-vpn + gluetun-proton
+
+ VPN-routed torrents. `qbittorrent-vpn` uses `network_mode: "service:gluetun"` — both containers excluded from Watchtower (`com.centurylinklabs.watchtower.enable=false`).
+
+ - **Compose:** `/root/stacks/bittorrent-vpn/docker-compose.yml` on PCT 101
+ - **WebUI:** `192.168.2.190:8081` (internal), `qbittorrent-vpn.carr-family.org` (external)
+ - **VPN:** ProtonVPN via gluetun
+
+ ---
+
+ ## qui
+
+ Multi-instance qBittorrent web UI at `qui.carr-family.org`. Not in Swarm — static route in `routes.yml` → `192.168.2.190:7476`.
+
+ **Instances:**
+ - `qbittorrent-mam` → `http://192.168.2.190:8080` (admin / 32Ab0321!!)
+ - `qbittorrent-vpn` → `http://192.168.2.190:8081`
/dev/null .. Game Servers.md
@@ 0,0 1,133 @@
+ # Game Servers
+
+ ← [[Home]]
+
+ Game servers managed via Pterodactyl. Panel on PCT 300, Wings daemon on PCT 301.
+
+ ---
+
+ ## Access
+
+ | Service | URL | IP |
+ |---------|-----|----|
+ | Pterodactyl Panel | `pterodactyl.carr-family.org` | `192.168.2.136` (lan-only) |
+ | Panel admin | admin / `[email protected]` | — |
+ | Wings API | — | `192.168.2.134:8080` |
+
+ ---
+
+ ## PCT 300 — Pterodactyl Panel
+
+ Apache2 + MariaDB + Redis. Not a Swarm node.
+
+ - **DB:** `[email protected]` / `XYq5KbPPIipdQ` — database `panel`
+ - **Redis:** `redis-server`
+ - **Queue worker:** `pteroq.service`
+ - **Application API key:** `ptcl1234567890abSetupSecretKey345678901234567890`
+
+ **Important:** always use `pct exec 300 -- env -i HOME=/root PATH=... TMPDIR=/tmp LANG=en_US.UTF-8 bash -c` to avoid inheriting `TMPDIR=/tmp/claude-0` from the Proxmox host (breaks MariaDB).
+
+ ---
+
+ ## PCT 301 — Pterodactyl Wings
+
+ Wings daemon v1.12 + Docker.
+
+ - **Config:** `/etc/pterodactyl/config.yml` — `remote: http://192.168.2.136`, token_id `08ifx4nsAOfSdrlK`
+ - **Game server volumes:** `/var/lib/pterodactyl/volumes/<uuid>/` owned `999:988`
+ - **Local registry:** `192.168.2.134:5000` (auto-restart container `registry`)
+
+ **Wings API power commands:**
+ ```bash
+ WINGS_TOKEN="hy92RMjddu3CIrGPCm3F7tBDSfl1BREKpAw4hkXuSC0F3vVBKxd6SFPW1UrGG4Zz"
+ curl -s -X POST http://192.168.2.134:8080/api/servers/<uuid>/power \
+ -H "Authorization: Bearer $WINGS_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{"action": "start|stop|kill|restart"}'
+ ```
+
+ **Manage Wings:**
+ ```bash
+ pct exec 301 -- env -i HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin TMPDIR=/tmp bash -c "systemctl restart wings"
+ pct exec 301 -- env -i HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin TMPDIR=/tmp bash -c "journalctl -u wings --no-pager -n 50"
+ ```
+
+ ### Custom Docker Image
+
+ `localhost:5000/pterodactyl-steamcmd:latest` — based on `cm2network/steamcmd:latest`, adds pterodactyl user (999:988). CMD: `["/bin/bash", "/home/container/start.sh"]`.
+
+ **Image rebuild workflow:**
+ ```bash
+ # On PCT 301 — build and push to local registry, then kill+start the server
+ docker build -t localhost:5000/pterodactyl-steamcmd:latest /tmp/ptero-steamcmd/
+ docker push localhost:5000/pterodactyl-steamcmd:latest
+ ```
+
+ ### SteamCMD Update Pattern
+
+ Wings can't pull `localhost:5000/` images during install. Always update manually:
+ ```bash
+ WINGS_TOKEN="hy92RMjddu3CIrGPCm3F7tBDSfl1BREKpAw4hkXuSC0F3vVBKxd6SFPW1UrGG4Zz"
+ curl -s -X POST http://192.168.2.134:8080/api/servers/<uuid>/power \
+ -H "Authorization: Bearer $WINGS_TOKEN" -H "Content-Type: application/json" -d '{"action":"kill"}'
+
+ pct exec 301 -- env -i HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin TMPDIR=/tmp bash -c "
+ docker run --rm -u 999:988 \
+ -v /var/lib/pterodactyl/volumes/<uuid>:/home/container \
+ localhost:5000/pterodactyl-steamcmd:latest \
+ bash -c 'cd /home/container && ./steamcmd/steamcmd.sh \
+ +@sSteamCmdForcePlatformType linux +force_install_dir /home/container \
+ +login anonymous +app_update <appid> validate +quit'"
+
+ curl -s -X POST http://192.168.2.134:8080/api/servers/<uuid>/power \
+ -H "Authorization: Bearer $WINGS_TOKEN" -H "Content-Type: application/json" -d '{"action":"start"}'
+ ```
+
+ ---
+
+ ## Port Allocations (PCT 301)
+
+ | Port(s) | Game |
+ |---------|------|
+ | 7777 TCP+UDP, 8888 TCP | Satisfactory |
+ | 25565–25566 | Minecraft Java |
+ | 27015–27020 | Source/Valve (CS2, TF2, L4D2) |
+ | 2456–2458 | Valheim |
+ | 19132 | Minecraft Bedrock |
+ | 30000–30010 | Custom |
+
+ ---
+
+ ## Satisfactory
+
+ Server ID 1, UUID `949251ef-944e-44f4-8b20-60be34b0120a`. Steam App ID: 1690800.
+
+ - **Image:** `localhost:5000/pterodactyl-steamcmd:latest`
+ - **Ports:** 7777 TCP+UDP (game traffic), 8888 TCP (ReliableMessaging — **required for external clients**)
+ - **RAM:** 6 GB | **Disk:** 20 GB
+ - **Volume:** `/var/lib/pterodactyl/volumes/949251ef-944e-44f4-8b20-60be34b0120a/`
+ - **Connect:** `192.168.2.134:7777`
+ - **Save files:** `<volume>/.config/Epic/FactoryGame/Saved/SaveGames/server/`
+
+ **DNS:** `satisfactory.carr-family.org` → public IP `174.95.181.77` (grey cloud, proxy off). Router port forwards TCP+UDP 7777 and TCP 8888 to `192.168.2.134`.
+
+ **Known gotchas:**
+ - **TCP 8888 is required** — without it, external clients join but are kicked exactly 20 seconds later (`LogReliableMessaging: Handshake timed out`)
+ - **Ports 15000/15777 are obsolete** — removed since Patch 1.0
+ - **Version must match client exactly** — mismatches show as `ConnectionTimeout`, not an explicit error
+ - **Auto-pause causes join timeouts during startup** — disable auto-pause after claiming the server
+ - **First-time setup** — wait 35+ seconds after start before connecting to an unclaimed server
+
+ **Update:**
+ ```bash
+ WINGS_TOKEN="hy92RMjddu3CIrGPCm3F7tBDSfl1BREKpAw4hkXuSC0F3vVBKxd6SFPW1UrGG4Zz"
+ curl -s -X POST http://192.168.2.134:8080/api/servers/949251ef-944e-44f4-8b20-60be34b0120a/power \
+ -H "Authorization: Bearer $WINGS_TOKEN" -H "Content-Type: application/json" -d '{"action":"kill"}'
+ pct exec 301 -- env -i HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin TMPDIR=/tmp bash -c "
+ docker run --rm -u 999:988 \
+ -v /var/lib/pterodactyl/volumes/949251ef-944e-44f4-8b20-60be34b0120a:/home/container \
+ localhost:5000/pterodactyl-steamcmd:latest \
+ bash -c 'cd /home/container && ./steamcmd/steamcmd.sh +@sSteamCmdForcePlatformType linux +force_install_dir /home/container +login anonymous +app_update 1690800 validate +quit'"
+ curl -s -X POST http://192.168.2.134:8080/api/servers/949251ef-944e-44f4-8b20-60be34b0120a/power \
+ -H "Authorization: Bearer $WINGS_TOKEN" -H "Content-Type: application/json" -d '{"action":"start"}'
+ ```
/dev/null .. Infrastructure.md
@@ 0,0 1,103 @@
+ # Infrastructure
+
+ ← [[Home]]
+
+ Proxmox host running Debian 12 Bookworm with six LXC containers and two ZFS pools.
+
+ ---
+
+ ## Storage Pools
+
+ | Name | Type | Total | Used | Notes |
+ |------|------|-------|------|-------|
+ | `tank` | ZFS | 3.77 TB | ~23% | Primary data — host path `/tank/`, mounted as `/mnt/tank/` inside containers |
+ | `local-ssd` | dir | 239 GB | ~64% | SSD (PCT 102 rootfs) |
+ | `local-lvm` | lvmthin | 354 GB | ~17% | LVM thin (PCT 108 rootfs) |
+ | `local` | dir | 98 GB | ~34% | Proxmox local storage |
+
+ ZFS ACLs: `tank/appdata` has `acltype=posixacl`. Use `setfacl` when a non-owner user needs access.
+
+ ---
+
+ ## LXC Mount Points
+
+ ### PCT 101 (downloads — 192.168.2.190)
+
+ | Mount | Host | Container |
+ |-------|------|-----------|
+ | mp2 | `/tank/appdata` | `/mnt/tank/appdata` |
+
+ ### PCT 102 (media-core — 192.168.2.191)
+
+ Privileged container. Full ZFS tank pool mounted.
+
+ | Mount | Host | Container |
+ |-------|------|-----------|
+ | mp0 | `/tank/` | `/mnt/tank/` |
+
+ **Media library root:** `/mnt/tank/media/content/` — movies, tv, music, books, audiobooks, podcasts, comics, roms
+
+ ### PCT 104 (documents — 192.168.2.105)
+
+ Privileged container. Only `/tank/media` and `/tank/appdata/nextcloud` are mounted — not the full tank.
+
+ | Mount | Host | Container | Notes |
+ |-------|------|-----------|-------|
+ | mp0 | `/tank/media` | `/mnt/tank/media` | Media library |
+ | mp1 | `/mnt/media-storage` | `/mnt/media-storage` | Separate media storage |
+ | mp2 | `/tank/appdata/nextcloud` | `/mnt/tank/appdata/nextcloud` | Nextcloud data (migrated 2026-05-25) |
+ | lxc.mount.entry | `/tank/docker` | `tank/docker` | Docker storage on ZFS, not rootfs |
+
+ ### PCT 107 (debian — 192.168.2.81)
+
+ | Mount | Host | Container |
+ |-------|------|-----------|
+ | mp1 | `/tank/appdata/nextcloud/data/data/nextcloud/files/obsidian` | `/mnt/obsidian` |
+
+ ### PCT 108 (network — 192.168.2.82)
+
+ Swarm manager. Appdata on rootfs (local-lvm), NOT ZFS. Portainer compose files are on ZFS.
+
+ | Mount | Host | Container |
+ |-------|------|-----------|
+ | mp0 | `/tank/` | `/mnt/tank/` |
+
+ ### PCT 109 (ai — 192.168.2.83)
+
+ Privileged container. `lxc.apparmor.profile: unconfined` (required for `docker build`). Not a Swarm node.
+
+ | Mount | Host | Container | Notes |
+ |-------|------|-----------|-------|
+ | mp0 | `/tank/appdata/nextcloud/data/data/nextcloud/files/obsidian` | `/mnt/obsidian` | Obsidian vault (read-write) |
+
+ ---
+
+ ## GPU Passthrough
+
+ NVIDIA GPU passthrough is configured for PCT 102, 104, and 109 via `lxc.cgroup2.devices.allow` and `lxc.mount.entry` in `/etc/pve/lxc/<vmid>.conf`.
+
+ **PCT 102:** GTX card, full media stack (Jellyfin transcoding, Immich ML)
+ **PCT 104:** GTX 1050 — Immich machine-learning (CUDA)
+ **PCT 109:** NVIDIA GPU — Whisper transcription
+
+ `nvidia-container-toolkit` is installed in each. Docker default runtime set to `nvidia` in `/etc/docker/daemon.json`.
+
+ To enable GPU in a container, set:
+ ```yaml
+ environment:
+ NVIDIA_VISIBLE_DEVICES: all
+ NVIDIA_DRIVER_CAPABILITIES: all
+ ```
+
+ **After a Proxmox host driver upgrade:** update versioned lib filenames in `/etc/pve/lxc/<vmid>.conf` and reboot the affected container.
+
+ ---
+
+ ## Management Scripts (Proxmox host `/root/`)
+
+ | Script | Purpose |
+ |--------|---------|
+ | `create-app-folders.sh` | Create `/tank/appdata/<app>` dirs with `1000:1000` |
+ | `setup-lxc-bind-mounts.sh` | Add ZFS bind mounts to LXC containers (`DRY_RUN=false` to apply) |
+ | `create_lxc.sh <CTID> <HOST> <IP>` | Provision new Ubuntu 22.04 LXC |
+ | `migrate-appdata-to-zfs.sh` | Migrate appdata from rootfs to ZFS dataset |
/dev/null .. Media Stack.md
@@ 0,0 1,130 @@
+ # Media Stack
+
+ ← [[Home]]
+
+ Services running on PCT 102 (media-core, 192.168.2.191). Stack: `media-core` (compose 26).
+
+ ---
+
+ ## Service URLs
+
+ | Service | URL | Port |
+ |---------|-----|------|
+ | Jellyfin | `jellyfin.carr-family.org` | 8096 (standalone, static route) |
+ | Sonarr | `sonarr.carr-family.org` | 8989 |
+ | Radarr | `radarr.carr-family.org` | 7878 |
+ | Prowlarr | `prowlarr.carr-family.org` | 9696 |
+ | Jellyseerr | `seerr.carr-family.org` | 5055 |
+ | Calibre-Web Automated | `book.carr-family.org` | 8083 |
+ | Audiobookshelf | `audiobook.carr-family.org` | 13378 |
+ | Audiobookrequest | `abr.carr-family.org` | 8000 |
+ | Shelfmark | `shelfmark.carr-family.org` | 8084 |
+ | OpenBooks | `openbooks.carr-family.org` | 8875 |
+ | Komga | `komga.carr-family.org` | 25600 |
+ | RomM | `romm.carr-family.org` | 8984 |
+
+ ---
+
+ ## Storage Layout
+
+ | Path | Contents |
+ |------|----------|
+ | `/mnt/tank/media/content/movies` | Movies |
+ | `/mnt/tank/media/content/tv` | TV shows |
+ | `/mnt/tank/media/content/books` | Calibre library (managed by CWA — don't move files manually) |
+ | `/mnt/tank/media/content/books-seeds` | MAM torrent seeding copies (never touch) |
+ | `/mnt/tank/media/content/audiobooks` | Audiobookshelf + Shelfmark audiobook downloads |
+ | `/mnt/tank/media/content/comics` | Komga library |
+ | `/mnt/tank/media/content/roms` | RomM ROM library (organize by platform subfolder) |
+ | `/mnt/tank/media/bookingest` | CWA ingest folder + Shelfmark ebook downloads |
+
+ ---
+
+ ## Calibre-Web Automated (CWA)
+
+ Volumes: `/mnt/tank/media/bookingest:/cwa-book-ingest`, `/mnt/tank/media/content/books:/calibre-library`
+
+ Config: `NETWORK_SHARE_MODE=true`, `CWA_WATCH_MODE=poll`
+
+ **Ingest mount gotcha** — verify mount with `docker inspect <container> | grep Mounts`. If CWA sees an empty ingest folder, the compose may have been updated without a redeploy.
+
+ **root:root ownership breaks deletes** — CWA web server runs as `abc` (uid 1000) but ingest processor runs as root with `NETWORK_SHARE_MODE=true` which skips the post-import chown. Files land as `root:root` and deletes fail with `[Errno 13]`. Fix: `chown -R 1000:1000 /tank/media/content/books`.
+
+ **Bulk-delete always shows success (CWA bug)** — the `/ajax/deleteselectedbooks` route always returns `{"success": true}`. Check container logs for `Deleting book X failed` if books reappear.
+
+ **Don't bulk-delete duplicates while ingest is running** — wait for all imports to complete first.
+
+ ### Calibre Email Setup
+
+ - SMTP hostname: `192.168.2.83` (Proton Bridge)
+ - Port: `1025`, Encryption: None
+ - Login: `[email protected]`, Password: `8A2SC9qao04GsSqBrfjtFg`
+
+ ### calibredb Commands
+
+ ```bash
+ # Get CWA container name
+ pct exec 102 -- docker ps --format '{{.Names}}' | grep calibre
+
+ # List all books as JSON
+ pct exec 102 -- docker exec <cwa-container> calibredb list \
+ --fields=id,title,authors,series,series_index,tags --sort-by=title \
+ -s '' --library-path=/calibre-library --for-machine
+
+ # Remove by ID
+ pct exec 102 -- docker exec <cwa-container> calibredb remove \
+ --library-path=/calibre-library <id1>,<id2>
+ ```
+
+ ---
+
+ ## Shelfmark
+
+ Book search & request tool. Config: `/mnt/tank/appdata/shelfmark/plugins/`.
+
+ After any config edit: `pct exec 108 -- docker service update --force media-core_shelfmark`
+
+ - **qBittorrent connection:** `192.168.2.190:8080`, creds `admin / 32Ab0321!!`
+ - **Ebook category:** `books-shelfmark`, **Audiobook category:** `audiobooks`
+ - **Remote path mappings** (`advanced.json`) — must use camelCase keys and `host: "qbittorrent"`:
+
+ ```json
+ [
+ {"host": "qbittorrent", "remotePath": "/data/bookingest", "localPath": "/books"},
+ {"host": "qbittorrent", "remotePath": "/data/content/audiobooks", "localPath": "/audiobooks"}
+ ]
+ ```
+
+ ---
+
+ ## Komga
+
+ Comics & manga at `komga.carr-family.org`. Library: `/mnt/tank/media/content/comics`.
+
+ `CONVERT_TO_CBZ=false`, `REPAIR_EXTENSIONS=false` — do not re-enable (was renaming .cbr → .cbz, breaking seeding).
+
+ ---
+
+ ## RomM (compose 32)
+
+ ROM manager with in-browser EmulatorJS at `romm.carr-family.org`. Port 8984, host-mode on 192.168.2.191. Auth: RomM built-in (no Authentik — would break EmulatorJS API calls).
+
+ - **ROMs:** `/mnt/tank/media/content/roms:/romm/library` — organize by platform subfolder
+ - **Config:** `/mnt/tank/appdata/romm/config/config.yml` — must exist before first start
+ - **Metadata:** IGDB (`u5audru2zn9na5a0x3wq26yslmk1s5`) + RetroAchievements configured in compose env
+
+ ```bash
+ pct exec 108 -- docker stack deploy -c /mnt/tank/appdata/portainer/compose/32/docker-compose.yml romm
+ ```
+
+ ---
+
+ ## Jellyfin (PCT 101 — standalone, not Swarm)
+
+ Media server at `jellyfin.carr-family.org`. Static route in `routes.yml` → `192.168.2.191:8096`.
+
+ ---
+
+ ## FlareSolverr
+
+ Internal only (`192.168.2.191:8191`). Cloudflare bypass for Prowlarr indexers.
/dev/null .. Network Stack.md
@@ 0,0 1,105 @@
+ # Network Stack
+
+ ← [[Home]]
+
+ Services running on PCT 108 (network, 192.168.2.82) — the Docker Swarm manager node.
+
+ ---
+
+ ## Service URLs
+
+ | Service | URL | Internal |
+ |---------|-----|----------|
+ | Traefik dashboard | `traefik.carr-family.org` | `192.168.2.82:443` |
+ | Portainer | `portainer.carr-family.org` | `192.168.2.82:9443` (HTTPS) |
+ | Homarr | `homepage.carr-family.org` | `192.168.2.82:7575` |
+ | Authentik | `auth.carr-family.org` | — |
+ | Guacamole | `guac.carr-family.org` | `192.168.2.82:8080` |
+ | CloudBeaver | `db.carr-family.org` | `192.168.2.82:8978` |
+ | Dozzle | `dozzle.carr-family.org` | — (global, all nodes) |
+
+ ---
+
+ ## Authentik (stack: `authentik`, compose 23)
+
+ SSO at `auth.carr-family.org`. Image: `ghcr.io/goauthentik/server:2026.2`.
+
+ Services: server, worker, proxy outpost, postgres:16-alpine (host port 5433), redis:7-alpine.
+
+ **Proxy outpost** (`ghcr.io/goauthentik/proxy:2026.2`) — handles Traefik ForwardAuth. Connects to `authentik` and `traefik-public` networks. forwardAuth address: `http://authentik_authentik-proxy:9000/outpost.goauthentik.io/auth/traefik`.
+
+ **Env file:** `/root/authentik.env` on PCT 108 (chmod 600). Required: `AUTHENTIK_SECRET_KEY`, `PG_USER`, `PG_PASS`, `PG_DB`.
+
+ **Redeploy:**
+ ```bash
+ pct exec 108 -- bash -c "set -a && source /root/authentik.env && set +a && docker stack deploy -c /mnt/tank/appdata/portainer/compose/23/docker-compose.yml authentik"
+ ```
+
+ **Trusted IP bypass:** LAN `192.168.2.0/24`, Tailscale `100.64.0.0/10`, `205.194.16.9/32` (friend's house) — Authentik Admin → Policy Engine → Policies → `Trusted IP Bypass`.
+
+ > Note (2026-06-12): `authentik` middleware removed from all routes.yml routers. Routes are now unprotected via Traefik. Services with authentik applied via compose labels still have it but it's effectively unused without the middleware definition.
+
+ ---
+
+ ## Homarr (stack: `documents-homarr`, compose 22)
+
+ Dashboard at `homepage.carr-family.org`. Pinned to manager node. Image: `ghcr.io/homarr-labs/homarr:latest`.
+
+ - **Port:** 3001 (published) → 7575 (internal); Traefik label targets 7575
+ - **DB:** `/mnt/tank/appdata/homarr/db/db.sqlite` (SQLite)
+ - **Populate script:** `/root/populate-homarr.js` on Proxmox host
+
+ **After re-populate, restore board permissions:**
+ ```bash
+ pct exec 108 -- sqlite3 /mnt/tank/appdata/homarr/db/db.sqlite "
+ INSERT OR IGNORE INTO boardUserPermission VALUES ('a2tzcbvgfkkt16aamvuwa6rs','bskmlbq5oayy4845ekbomk9y','full');
+ INSERT OR IGNORE INTO boardGroupPermission VALUES ('a2tzcbvgfkkt16aamvuwa6rs','nw94xpf6j307ceruir2b82x9','full');"
+ ```
+
+ **Integrations:** Sonarr, Radarr, Prowlarr, Jellyseerr (apiKey). Jellyfin + qBittorrent need manual setup (no apiKey support). `immich` and `paperlessNgx` kinds crash the integrations page — do not add.
+
+ ---
+
+ ## Guacamole (stack: `network-guacamole`, compose 6)
+
+ Remote desktop gateway at `guac.carr-family.org`. v1.6.0 + guacd + postgresql:15 (host port 5434).
+
+ ---
+
+ ## CloudBeaver (stack: `network-cloudbeaver`, compose 31)
+
+ DB viewer at `db.carr-family.org` (lan-only). Workspace: `/mnt/tank/appdata/cloudbeaver/workspace`.
+
+ **DB Connections:**
+
+ | Name | Host | Port | User | DB |
+ |------|------|------|------|----|
+ | authentik | 192.168.2.82 | 5433 | authentik | authentik |
+ | guacamole | 192.168.2.82 | 5434 | guacamole | guacamole |
+ | nextcloud | 192.168.2.105 | 5432 | nextcloud | nextcloud |
+ | paperless | 192.168.2.105 | 5433 | paperless | paperless |
+ | linkwarden | 192.168.2.105 | 5434 | postgres | postgres |
+ | immich | 192.168.2.105 | 5435 | immich | immich |
+ | litellm | 192.168.2.83 | 5433 | litellm | litellm |
+
+ Postgres ports are exposed host-mode on each node's LAN IP.
+
+ ---
+
+ ## Watchtower (stack: `watchtower`)
+
+ Automatic image updates daily at 04:00 AM (`0 0 4 * * *` — cron6 format).
+
+ - **Mode:** Global (one instance per node — covers Swarm services and standalone containers)
+ - **Config:** `WATCHTOWER_CLEANUP=true`, `WATCHTOWER_ROLLING_RESTART=true`, `DOCKER_API_VERSION=1.40`
+ - **Compose:** `/mnt/tank/appdata/watchtower/docker-compose.yml`
+ - **Notifications:** Disabled (was spamming emails — removed 2026-06-12)
+
+ **Excluded containers** (incompatible with rolling restart — `network_mode: "service:..."` dependency):
+ - `gluetun-proton` and `qbittorrent-vpn` — label `com.centurylinklabs.watchtower.enable=false`
+
+ ---
+
+ ## Dozzle (stack: `dozzel`, compose 25)
+
+ Live container log viewer at `dozzle.carr-family.org`. Global mode — runs on all 4 worker nodes (not PCT 108).
/dev/null .. Traefik.md
@@ 0,0 1,78 @@
+ # Traefik
+
+ ← [[Home]]
+
+ Reverse proxy running in the `network` Swarm stack on PCT 108. Handles all `*.carr-family.org` traffic.
+
+ - **Config:** `/mnt/tank/appdata/traefik/traefik.yml`
+ - **Dynamic routes:** `/mnt/tank/appdata/traefik/routes.yml`
+ - **Certs:** `/mnt/tank/appdata/traefik/certs/acme.json`
+ - **TLS:** Cloudflare DNS challenge (`CF_DNS_API_TOKEN_FILE` from Docker secret)
+ - **Entrypoints:** `web` (80 → 443 redirect), `websecure` (443)
+ - **Trusted IPs:** `192.168.2.0/24` (LAN), `100.64.0.0/10` (Tailscale), Cloudflare IP ranges
+
+ Two providers: **Swarm** (local Docker socket on PCT 108) + **Docker** (`tcp://192.168.2.191:2375` for standalone containers on PCT 102).
+
+ ---
+
+ ## Static Routes (`routes.yml`)
+
+ Used for services not in Docker Swarm (standalone containers, VMs, or other LXC nodes).
+
+ | Host | Backend |
+ |------|---------|
+ | `homeassist.carr-family.org` | `192.168.2.129:8123` |
+ | `qbittorrent-vpn.carr-family.org` | `192.168.2.190:8081` |
+ | `qbittorrent.carr-family.org` | `192.168.2.190:8080` (lan-only) |
+ | `ai.carr-family.org` | `192.168.2.81:3000` |
+ | `gcjobs.carr-family.org` | `192.168.2.81:8501` |
+ | `jellyfin.carr-family.org` | `192.168.2.191:8096` |
+ | `n8n.carr-family.org` | `192.168.2.83:5678` (lan-only) |
+ | `litellm.carr-family.org` | `192.168.2.83:4000` (lan-only) |
+ | `openclaw.carr-family.org` | `192.168.2.83:18789` (lan-only) |
+ | `odysseus.carr-family.org` | `192.168.2.83:7000` (lan-only) |
+ | `otterwiki.carr-family.org` | `192.168.2.105:8081` |
+ | `pterodactyl.carr-family.org` | `192.168.2.136:80` (lan-only) |
+ | `qui.carr-family.org` | `192.168.2.190:7476` |
+ | `gcjobs-filler.carr-family.org` | `192.168.2.81:8000` |
+
+ ---
+
+ ## Middlewares
+
+ | Name | Purpose |
+ |------|---------|
+ | `lan-only` | IP allowlist — LAN + Tailscale |
+ | `auth` | Basic auth via `traefik_auth` secret |
+ | `secure-headers` | HSTS |
+ | `authentik` | ForwardAuth → Authentik outpost (removed from routes.yml as of 2026-06-12 — compose labels may still reference it) |
+
+ **Cross-provider reference:** Middlewares defined in `routes.yml` must be referenced as `authentik@file` / `lan-only@file` in Swarm service labels — plain names default to `@swarm` and 404.
+
+ ---
+
+ ## Docker Secrets
+
+ | Secret | Purpose |
+ |--------|---------|
+ | `cf_dns_token` | Cloudflare DNS challenge for TLS |
+ | `cf_api_email` | Cloudflare account email |
+ | `traefik_auth` | Dashboard basic auth |
+
+ ---
+
+ ## routes.yml Edit Gotcha
+
+ `sed -i` replaces the file with a new inode; Traefik's bind-mount stays pinned to the old inode and misses changes. Always write in-place **and** force-restart after any edit:
+
+ ```bash
+ pct exec 108 -- docker service update --force network_traefik
+ ```
+
+ ---
+
+ ## Cloudflare DNS-only (UDP game traffic — bypasses Traefik)
+
+ | Host | IP | Notes |
+ |------|----|-------|
+ | `satisfactory.carr-family.org` | `174.95.181.77` (public IP) | Grey cloud (proxy off); router port forwards → `192.168.2.134`. TCP+UDP 7777, TCP 8888 (ReliableMessaging). |
home.md ..
@@ 1,28 1,36 @@
- ## Welcome to your wiki!
-
- Your Otter Wiki is up and running.
-
- This is your [[Home]] Page, the first page you see when you access your
- wiki.
-
- The first steps you might want to do:
-
- 1. [Register an account](/-/register). The very first account is an
- admin account which is able to configure the wiki.
- 2. Check the [configuration](/-/admin#application_preferences) of your wiki.
- You can change its name, configure the permissions necessary to
- view and edit pages or upload attachments.
- 3. If you require users to confirm their email address (recommended),
- make sure that you will configure and test your [Email Preferences](/-/admin#mail_preferences).
- 4. [Edit your Home](/Home/edit)! Do not like the change? Visit the
- page [history](/Home/history) and revert any change ever made.
- 5. You can [attach](/Home/attachments) images and other files to any page
- and then display them and link to them inside the page.
- 6. [Create new pages](/-/create)! If you need help with the Markdown syntax,
- check out the [Markdown guide](/-/help/syntax).
- 7. Read the [user guide](/-/help) and learn about An Otter Wikis features.
-
- We hope that An Otter Wiki is just what you are looking for.
- If you have any suggestions, feature requests or run into any
- issues, please reach out and report them
- via [github](https://github.com/redimp/otterwiki/issues).
+ # Homelab Wiki
+
+ Proxmox-based homelab running a Docker Swarm cluster across six LXC containers.
+
+ - **Host:** Proxmox (Debian 12) — `homelab`
+ - **Domain:** `carr-family.org` — Cloudflare DNS + wildcard TLS
+ - **External access:** Cloudflare Tunnel → Traefik on PCT 108 (`192.168.2.82:443`)
+
+ ---
+
+ ## Containers
+
+ | VMID | Name | IP | Role |
+ |------|------|----|------|
+ | 101 | downloads | 192.168.2.190 | Docker Swarm worker — downloads |
+ | 102 | media-core | 192.168.2.191 | Docker Swarm worker, NVIDIA GPU, media stack |
+ | 104 | documents | 192.168.2.105 | Docker Swarm worker, NVIDIA GPU, docs/photos |
+ | 107 | debian | 192.168.2.81 | Docker Swarm worker — general purpose |
+ | 108 | network | 192.168.2.82 | **Docker Swarm manager**, Portainer, Traefik |
+ | 109 | ai | 192.168.2.83 | AI services — not a Swarm node |
+ | 300 | pterodactyl-panel | 192.168.2.136 | Pterodactyl Panel |
+ | 301 | pterodactyl-wings | 192.168.2.134 | Pterodactyl Wings + game servers |
+
+ ---
+
+ ## Sections
+
+ - [[Infrastructure]] — storage pools, LXC mounts, GPU passthrough
+ - [[Docker Swarm]] — Swarm management commands and patterns
+ - [[Traefik]] — reverse proxy, static routes, middlewares
+ - [[Network Stack]] — Portainer, Authentik, Guacamole, CloudBeaver, Homarr, Watchtower
+ - [[Media Stack]] — Sonarr, Radarr, Jellyfin, Calibre-Web, Audiobookshelf, Komga, RomM
+ - [[Documents Stack]] — Immich, Nextcloud, Paperless, Linkwarden, OtterWiki
+ - [[AI Stack]] — LiteLLM, n8n, Qdrant, Hermes, OpenClaw, Odysseus, Whisper, Proton Bridge
+ - [[Downloads]] — qBittorrent, gluetun/ProtonVPN
+ - [[Game Servers]] — Pterodactyl, Satisfactory
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9