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 |
