Centerpoint Home Lab
Description Specifically for Centerpoint set up
- Server Introduction
- Infrastructure & Networking
- Chapter Introduction
- Traefik v3
- Step-CA (Internal Certificate Authority)
- Portainer EE
- Authentik (SSO / Identity Provider)
- AdGuard Home
- CrowdSec
- Dozzle
- Uptime-Kuma
- Homepage.Dev
- AI & Automation
- Chapter Introduction
- Ollama
- Open-WebUI
- Faster Whisper
- Kokoro
- Riffado (OpenPlaud)
- Paperless-AI
- n8n
- Open Notebook
- 09-mcp-github.md
- h04-media-entertainment
- 00-chapter-intro.md
- 01-plex.md
- 02-jellyfin.md
- 03-emby.md
- 04-tautulli.md
- 05-immich.md
- 06-audiobookshelf.md
- 07-seerr.md
- 09-threadfin.md
- h05-media-management
- 00-chapter-intro.md
- 01-sonarr.md
- 02-radarr.md
- 03-prowlarr.md
- 04-bazarr.md
- 05-nzbget.md
- 06-whisparr.md
- 07-lazylibrarian.md
- 08-mylar3.md
- 09-audiobookrequest.md
- 10-profilarr.md
- 11-dispatcharr.md
- 12-pulsarr.md
- 13-flaresolverr.md
- h06-documents-organization
- 00-chapter-intro.md
- 01-paperless-ngx.md
- 02-bookstack.md
- 03-karakeep.md
- 04-memos.md
- 05-booklore.md
- 06-mealie.md
- 07-homebox.md
- 08-medikeep.md
- 09-lubelogger.md
- 10-hortusfox.md
- 11-linkstack.md
- 12-reitti.md
- Swarm-Reitti Bridge
- AdventureLog - Travel Helper
- h07-finance
- h08-utilities
Server Introduction
Hardware specifications, storage layout, network topology, and the base Docker environment that underpins the entire Centerpoint stack.
Overview & Hardware
Overview & Hardware tags: [hardware, centerpoint, specs]
Overview
Centerpoint is the primary homelab server — a Mini PC form factor running a full self-hosted Docker stack. It acts as the central compute node for all containerised services, reverse proxying, media, AI inference, and automation in the homelab.
| Property | Value |
|---|---|
| Hostname | centerpoint |
| Role | Primary Docker host |
| Form Factor | Mini PC (NUC-style) |
| IP Address | 192.168.1.85 |
| Tailscale IP | 100.117.158.101 |
| OS | Ubuntu 24.04.4 LTS (Noble Numbat) |
| Kernel | 6.17.0-35-generic |
CPU
| Property | Value |
|---|---|
| Model | Intel Core Ultra 9 285H |
| Core Architecture | P-cores only (no hyperthreading) |
| Physical Cores | 16 |
| Sockets | 1 |
| Threads | 16 (1 per core) |
The 285H is an Intel Meteor Lake H-series mobile processor with a dedicated Neural Processing Unit (NPU). The absence of E-cores or hyperthreading means all 16 logical CPUs are full performance cores, which benefits parallel container workloads.
Memory
| Property | Value |
|---|---|
| Total RAM | 96 GB |
At time of writing, approximately 33 GB is actively in use with ~9 GB free and ~49 GB used as page cache — normal for a long-running Linux system running a large Docker stack.
Notes
- Uptime is typically measured in weeks; the system runs continuously.
- The NPU in the 285H is not currently leveraged by any container workload — it is available for future OpenVINO or AI inference optimisation.
Last Updated: 2026-06-16
Storage Layout
Overview
Centerpoint uses a three-tier storage strategy:
- Local NVMe — fast system and application data storage
- Ceph OSD block devices — two NVMe drives contributing to a distributed Ceph cluster for object/block storage across the homelab
- NFS mounts from UnRAID — bulk media and data storage from the NAS server at
192.168.1.119
Local NVMe Drives
System Drive — nvme1n1 (1.8 TB)
The primary system disk, GPT-partitioned with LVM.
| Partition | Size | Mount Point | Purpose |
|---|---|---|---|
nvme1n1p1 |
1 GB | /boot/efi |
EFI System Partition |
nvme1n1p2 |
2 GB | /boot |
Boot partition |
nvme1n1p3 (LVM PV) |
1.8 TB | — | LVM physical volume |
ubuntu-vg/ubuntu-lv |
1.8 TB | / |
Root filesystem |
Current usage: 520 GB used / 1.3 TB free
This drive holds the OS, all Docker image layers (/var/lib/docker), container
volumes, and the compose project files under /home/jeeves/docker/.
Ceph OSD — nvme0n1 (931.5 GB)
Configured as a Ceph OSD block device under LVM management.
Mounted at /media/jeeves/1TB_Vol2.
The volume appears nearly empty at the filesystem level because Ceph manages the block device directly — actual utilised capacity is tracked by the Ceph cluster, not the OS mount point.
Ceph OSD — nvme2n1 (931.5 GB)
Second Ceph OSD block device, LVM-managed.
Mounted at /media/jeeves/1TB_Vol1. Shows ~115 GB used at the OS level; the
remainder is managed by Ceph.
Both Ceph OSD drives contribute to a distributed storage pool shared across the homelab. Ceph provides replication and data protection at the cluster level rather than at the individual host level.
NFS Mounts — UnRAID (192.168.1.119)
UnRAID at 192.168.1.119 hosts a 38 TB storage pool (~32 TB in use) and exports
three NFS shares, auto-mounted on Centerpoint at boot.
| Mount Point | NFS Source | Consumer Services |
|---|---|---|
/mnt/Photos |
192.168.1.119:/mnt/user/Photos |
Immich |
/mnt/data |
192.168.1.119:/mnt/user/Data |
General / miscellaneous |
/mnt/Multimedia |
192.168.1.119:/mnt/user/Multimedia |
Plex, Jellyfin, Stash, Audiobookshelf |
These paths are bind-mounted into media containers — the media files themselves are never stored locally on Centerpoint.
Notes / Gotchas
- If UnRAID is offline or rebooting, NFS mounts will stall and any container with
a bind mount into
/mnt/*will hang or fail to start until the mount recovers. - Docker named volumes (databases, config state, etc.) all reside on the system
NVMe under
/var/lib/docker/volumes/— these are not automatically backed up to UnRAID. - The Ceph cluster should be monitored independently; degraded OSD status does not surface through Docker or the OS mounts in any obvious way.
Last Updated: 2026-06-16
Networking
Overview
Centerpoint is reachable via four distinct paths depending on the use case.
| Path | Address / Domain | Use Case |
|---|---|---|
| LAN (physical) | 192.168.1.85 |
Direct IP access, management |
| Tailscale VPN | 100.117.158.101 |
Secure remote access |
| Traefik (internal) | *.home.local |
Named HTTPS on LAN |
| Traefik (external) | *.jeeves5454.ddns.net / *.jeevesconsults.ca |
Internet-facing HTTPS |
Physical Network
| Property | Value |
|---|---|
| Interface | enp47s0 |
| IP Address | 192.168.1.85/24 |
| Default Gateway | 192.168.1.1 (Unifi router) |
| Subnet | 192.168.1.0/24 |
The IP is assigned via DHCP with a static lease on the Unifi gateway, making it functionally static.
Tailscale Mesh VPN
Centerpoint runs as a Tailscale node and exit node, allowing remote devices to route all traffic through the home network.
| Peer | Tailscale IP | Platform | Notes |
|---|---|---|---|
| centerpoint | 100.117.158.101 |
Linux | This host — exit node |
| corsec | 100.101.27.107 |
Linux | HA OS server — also exit node |
| halcyon | 100.81.166.2 |
Windows | Active peer (direct connection) |
| Mobile | varies | iOS | Occasional peers |
Docker Network Architecture
Docker maintains approximately 25 bridge networks on the host. Each application stack follows a consistent isolation pattern:
| Network | Purpose |
|---|---|
traefik-net |
Shared bridge — all Traefik-fronted containers attach here |
*-internal |
Per-stack isolated networks for app-to-database communication |
media-network |
Shared bridge for media stack containers |
bridge |
Docker default (not used for production workloads) |
Typical stack pattern:
- Application container → attached to both
traefik-netand<stack>-internal - Database / cache sidecar → attached to
<stack>-internalonly (never on Traefik)
DNS
Internal (*.home.local): Resolved by AdGuard Home on the LAN. A wildcard DNS
record points *.home.local → 192.168.1.85 so Traefik receives all requests and
routes by hostname.
External (*.jeeves5454.ddns.net): DDNS via No-IP, keeps the external hostname
updated with the home WAN IP.
External (*.jeevesconsults.ca): Managed via DNS provider with appropriate
A / CNAME records pointing to the home WAN.
Notes / Gotchas
- If AdGuard Home is down, all
*.home.localDNS resolution fails. Services remain accessible via direct IP but HTTPS by hostname will not work. - Tailscale must be running (
tailscaled) for the100.xaddress to be reachable. - The large number of
br-*interfaces visible onip addris normal — Docker creates one per network. - Traefik handles all TLS termination; individual containers do not need to manage certificates.
Last Updated: 2026-06-16
Docker Environment
Overview
All services on Centerpoint run as Docker containers, managed through Docker Compose project files and monitored via Portainer EE. Traefik v3 serves as the reverse proxy and TLS termination point for every service.
Docker Engine
| Property | Value |
|---|---|
| Docker Version | 29.5.3 |
| Docker Compose | v5.1.4 |
| Storage Driver | overlayfs |
| Docker Root | /var/lib/docker |
| Total Containers | 112 |
| Running | 97 |
| Stopped | 15 |
| Images | 108 |
Project Structure
All compose projects live under /home/jeeves/docker/, with one subdirectory per
logical stack:
/home/jeeves/docker/
├── adguard/
├── ai-stack/ ← Ollama, Open Web UI, Faster-Whisper, Kokoro
├── arr/ ← Sonarr, Radarr, Prowlarr, Bazarr, NZBGet, etc.
├── authentik/
├── bookstack/
├── crowdsec/
├── homepage/
├── immich/
├── paperless/
├── traefik/
└── ... (one directory per stack)
Each directory contains a docker-compose.yml and any local config files or
bind-mount targets specific to that stack.
Container Management — Portainer EE
Portainer Enterprise Edition provides the web UI for container lifecycle management, log viewing, stack deployment, and environment monitoring.
Portainer connects to the Docker daemon via a dockerproxy sidecar container
(Tecnativa Docker Socket Proxy) rather than mounting the Docker socket directly.
This limits the API surface exposed to Portainer and reduces the blast radius of
any container compromise.
Traefik v3 — Reverse Proxy
Traefik is the single ingress point for all named HTTP/HTTPS traffic. It runs
permanently on traefik-net and discovers routes automatically from Docker container
labels — no manual reload required when stacks are added or removed.
| Property | Value |
|---|---|
| HTTP port | 80 — auto-redirects all traffic to HTTPS |
| HTTPS port | 443 |
| Dashboard port | 8080 — internal only (traefik.home.local) |
| Static config | /home/jeeves/docker/traefik/traefik.yml |
| Dynamic config | /home/jeeves/docker/traefik/dynamic.yml (file-watched) |
| Access logs | /var/log/traefik/access.log (JSON, buffered) |
Certificate Resolvers
| Resolver | Scope | Method | Certificate Authority |
|---|---|---|---|
letsencrypt |
External domains | HTTP challenge | Let's Encrypt |
step-ca |
*.home.local |
ACME | Internal Step-CA (ca.home.local) |
Internal certificates have a 720-hour (30-day) duration and auto-renew via Traefik's built-in ACME client against the Step-CA instance.
Active Plugins
| Plugin | Version | Purpose |
|---|---|---|
PascalMinder/geoblock |
v0.3.6 | Country-level block on external-facing routes |
maxlerebourg/crowdsec-bouncer-traefik-plugin |
v1.3.0 | Blocks IPs flagged by the local CrowdSec LAPI |
Standard Routing Pattern
Each service defines Traefik labels in its own docker-compose.yml. The typical
pattern for a dual-route service (internal + external) is:
labels:
- "traefik.enable=true"
# External route — Let's Encrypt TLS + security middleware
- "traefik.http.routers.<name>-ext.rule=Host(`<svc>.jeeves5454.ddns.net`)"
- "traefik.http.routers.<name>-ext.entrypoints=websecure"
- "traefik.http.routers.<name>-ext.tls.certresolver=letsencrypt"
- "traefik.http.routers.<name>-ext.middlewares=authentik-auth@docker,plex-geoblock@file,crowdsec-bouncer@file"
# Internal route — Step-CA TLS, no extra middleware
- "traefik.http.routers.<name>-int.rule=Host(`<svc>.home.local`)"
- "traefik.http.routers.<name>-int.entrypoints=websecure"
- "traefik.http.routers.<name>-int.tls.certresolver=step-ca"
# Backend service port
- "traefik.http.services.<name>-svc.loadbalancer.server.port=<port>"
Services that are internal-only omit the -ext router entirely. Services that
require OAuth2 authentication on external routes add authentik@file middleware.
Notes / Gotchas
traefik-netis an externally created network and must exist before any Traefik-fronted stack is started:docker network create traefik-net- Never restart
dockerproxywhile Portainer is actively being used — it will lose its Docker connection until the proxy is back up. - Compose files use the stack subdirectory as their working directory — relative bind mount paths resolve from there.
overlayfscan accumulate orphaned image layers over time. Prune periodically:docker image prune docker volume prune # caution — only remove truly unused volumes
Last Updated: 2026-06-16
Infrastructure & Networking
Chapter Introduction
Overview
This chapter documents the foundational infrastructure layer that all other services depend on. These nine services form the backbone of the Centerpoint stack — handling reverse proxying, TLS, identity, DNS, threat detection, logging, and observability.
Services in This Chapter
| Service | Container(s) | Purpose |
|---|---|---|
| Traefik v3 | traefik |
Reverse proxy and TLS termination |
| Step-CA | step-ca |
Internal ACME certificate authority |
| Portainer EE | portainer |
Docker container management UI |
| Authentik | authentik-server, authentik-worker, authentik-postgresql, authentik-geoip |
SSO / identity provider |
| AdGuard Home | adguardhome |
LAN DNS resolver and ad blocking |
| CrowdSec | crowdsec, crowdsec-bouncer-traefik |
Collaborative threat detection and blocking |
| Dozzle | dozzle |
Real-time container log viewer |
| Uptime-Kuma | uptime-kuma |
Service uptime and endpoint monitoring |
| Homepage | homepage |
Homelab dashboard |
Dependency Order
When starting from scratch, services must come up in this order:
- AdGuard Home —
*.home.localDNS must resolve before anything can find its neighbours - Step-CA — required for Traefik to issue internal certificates on first boot
- Traefik — all named HTTPS routes depend on it
- Authentik (PostgreSQL → Server → Worker) — required before any externally-accessible service that enforces SSO
- CrowdSec → CrowdSec Bouncer — bouncer cannot connect to LAPI until CrowdSec is healthy
- All other infrastructure services (Portainer, Dozzle, Uptime-Kuma, Homepage) can start in any order
Last Updated: 2026-06-16
Traefik v3
Overview
Traefik is the single ingress point for all named HTTP/HTTPS traffic on Centerpoint.
It runs on the traefik-net Docker bridge network and discovers routes automatically
from container labels — no manual reload is needed when stacks are added or removed.
All TLS termination happens at Traefik; individual application containers serve plain
HTTP internally.
Access
| Type | URL / Endpoint | Notes |
|---|---|---|
| Dashboard | https://traefik.home.local |
Internal only — LAN access required |
The dashboard is exposed only on the internal Step-CA route. There is no external (internet-facing) route for the Traefik dashboard.
Configuration
Image: traefik:v3.6.13
Compose project: traefik (same project as step-ca)
Ports
| Port | Protocol | Purpose |
|---|---|---|
80 |
TCP | HTTP — auto-redirects all traffic to HTTPS |
443 |
TCP | HTTPS — primary entrypoint |
8080 |
TCP | Traefik API / Dashboard (internal) |
Static Configuration — traefik.yml
Location: /home/jeeves/docker/traefik/traefik.yml
Key settings:
api:
dashboard: true
insecure: false
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
providers:
docker:
network: traefik-net
exposedByDefault: false
file:
directory: /config
watch: true
certificatesResolvers:
letsencrypt:
acme:
email: <redacted>
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web
step-ca:
acme:
email: <redacted>
storage: /letsencrypt/step-ca.json
caServer: https://ca.home.local:9000/acme/acme/directory
certificatesDuration: 720
experimental:
plugins:
geoblock:
moduleName: github.com/PascalMinder/geoblock
version: v0.3.6
crowdsec-bouncer-traefik-plugin:
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
version: v1.3.0
Certificate Resolvers
| Resolver | Scope | Method | CA | Duration |
|---|---|---|---|---|
letsencrypt |
External (*.jeeves5454.ddns.net) |
HTTP challenge | Let's Encrypt | 90 days |
step-ca |
Internal (*.home.local) |
ACME | Internal Step-CA at ca.home.local:9000 |
30 days |
Plugins
| Plugin | Version | Purpose |
|---|---|---|
PascalMinder/geoblock |
v0.3.6 | Blocks requests from countries not in allowlist |
maxlerebourg/crowdsec-bouncer-traefik-plugin |
v1.3.0 | Forwards requests to CrowdSec bouncer for IP checks |
Geoblock is configured to allow CA, US, and IN only. The middleware is
named plex-geoblock@file and is defined in
/home/jeeves/docker/traefik/config/crowdsec.yml.
Dynamic Configuration
Location: /home/jeeves/docker/traefik/config/ (file-watched)
| File | Contents |
|---|---|
crowdsec.yml |
plex-geoblock and crowdsec-bouncer middleware defs |
dynamic.yml |
Static file-provider routes for Plex (no Docker labels) |
middlewares.yml |
Additional shared middleware definitions |
Standard Dual-Route Label Pattern
Each service that requires both internal and external access uses this label pattern
in its docker-compose.yml:
labels:
- "traefik.enable=true"
# External route — Let's Encrypt TLS + security middleware
- "traefik.http.routers.<name>-ext.rule=Host(`<svc>.jeeves5454.ddns.net`)"
- "traefik.http.routers.<name>-ext.entrypoints=websecure"
- "traefik.http.routers.<name>-ext.tls.certresolver=letsencrypt"
- "traefik.http.routers.<name>-ext.middlewares=authentik-auth@docker,plex-geoblock@file,crowdsec-bouncer@file"
# Internal route — Step-CA TLS, no SSO middleware
- "traefik.http.routers.<name>-int.rule=Host(`<svc>.home.local`)"
- "traefik.http.routers.<name>-int.entrypoints=websecure"
- "traefik.http.routers.<name>-int.tls.certresolver=step-ca"
# Backend
- "traefik.http.services.<name>-svc.loadbalancer.server.port=<port>"
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/traefik/traefik.yml |
/traefik.yml |
Static config |
/home/jeeves/docker/traefik/config |
/config |
Dynamic config (file-watched) |
/home/jeeves/docker/traefik/letsencrypt |
/letsencrypt |
ACME certificate storage |
/var/log/traefik |
/var/log/traefik |
Access logs (JSON) |
/var/run/docker.sock |
/var/run/docker.sock |
Docker label discovery |
Dependencies
traefik-netDocker network (must be created manually before first start)step-cacontainer (must be healthy for internal certs to renew)- CrowdSec LAPI at
crowdsec:8080(bouncer requires it; Traefik starts without it but will error on blocked requests)
Notes / Gotchas
traefik-netis externally managed — always create it first:docker network create traefik-net- Traefik discovers routes from container labels only on containers attached to
traefik-net. If a container is not on this network it will be invisible to Traefik even withtraefik.enable=true. exposedByDefault: falsemeans every service must explicitly opt in withtraefik.enable=true.- The
step-cacertificate resolver stores its ACME account and certs in/letsencrypt/step-ca.json(separate from the Let's Encrypt storage). Do not delete this file or certificates will be re-issued from scratch. - Access logs are written to
/var/log/traefik/access.login JSON format and are also bind-mounted into the CrowdSec container for log ingestion.
Last Updated: 2026-06-16
Step-CA (Internal Certificate Authority)
Overview
Step-CA is an internal ACME-compatible certificate authority that issues TLS
certificates for all *.home.local domains. It allows Traefik to provision and
auto-renew trusted certificates for internal services without relying on Let's
Encrypt or exposing any ports to the internet.
Clients and browsers on the LAN trust *.home.local certificates because the
Step-CA root certificate has been manually installed as a trusted CA on each device.
Access
| Type | URL / Endpoint | Notes |
|---|---|---|
| ACME API | https://ca.home.local:9000/acme/acme/directory |
Used by Traefik only |
| Step-CA UI | N/A | No web UI — CLI managed only |
Step-CA does not have a browser-accessible dashboard. Management is via the
step CLI tool.
Configuration
Image: smallstep/step-ca:latest
Compose project: traefik (same project as traefik container)
Ports
| Port | Protocol | Purpose |
|---|---|---|
9000 |
TCP | ACME API (host-bound: 0.0.0.0:9000) |
Port 9000 is bound directly to the host so that Traefik (and any other ACME client
on the LAN) can reach it at ca.home.local:9000.
CA Configuration — ca.json
Location: /home/jeeves/docker/step-ca/config/config/ca.json
Key settings (secrets redacted):
{
"root": "/home/step/certs/root_ca.crt",
"crt": "/home/step/certs/intermediate_ca.crt",
"key": "/home/step/secrets/intermediate_ca_key",
"address": "0.0.0.0:9000",
"dnsNames": ["ca.home.local"],
"db": {
"type": "badger",
"dataSource": "/home/step/db"
},
"authority": {
"provisioners": [
{
"type": "ACME",
"name": "acme"
}
]
}
}
Certificate duration for ACME-issued certs is set to 720 hours (30 days) in
Traefik's step-ca resolver config. Traefik renews automatically before expiry.
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/step-ca/config |
/home/step |
CA configuration, certificates, database |
Directory Layout Inside the Volume
/home/jeeves/docker/step-ca/config/
├── config/
│ ├── ca.json ← Main CA configuration
│ └── defaults.json ← Step CLI defaults
├── certs/
│ ├── root_ca.crt ← Root CA certificate (install this on client devices)
│ └── intermediate_ca.crt
├── secrets/ ← Private keys — never expose these
│ ├── root_ca_key
│ ├── intermediate_ca_key
│ └── password ← Key encryption password (REDACTED)
└── db/ ← BadgerDB certificate issuance database
Dependencies
- AdGuard Home —
ca.home.localmust resolve on the LAN for Traefik to reach the ACME endpoint - No other service dependencies
Trusting the Root CA on Client Devices
Every device that accesses *.home.local URLs in a browser must trust the Step-CA
root certificate. The root cert is at:
/home/jeeves/docker/step-ca/config/certs/root_ca.crt
Also available at /home/jeeves/docker/traefik/step-ca-root.crt (Traefik keeps a
copy for its own ACME client trust store).
macOS
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain root_ca.crt
Windows
Import-Certificate -FilePath root_ca.crt -CertStoreLocation Cert:\LocalMachine\Root
Ubuntu / Debian
sudo cp root_ca.crt /usr/local/share/ca-certificates/step-ca.crt
sudo update-ca-certificates
Notes / Gotchas
- The Step-CA password file (
/home/step/secrets/password) is read on startup. If the volume is lost, the CA must be re-initialized and all client devices must re-import the new root certificate. - Do not delete the
db/directory — it contains the certificate issuance history. Loss means Step-CA cannot check for revoked certificates. - Step-CA is in the same Portainer compose project as Traefik. Restarting the Traefik stack will also restart Step-CA briefly. Schedule this during off-hours.
- Traefik caches issued certs in its ACME storage file. If Step-CA is temporarily down, Traefik will continue serving existing certs until they near expiry.
Last Updated: 2026-06-16
Portainer EE
Overview
Portainer Enterprise Edition is the primary container management interface for Centerpoint. It provides a web UI for deploying stacks, viewing container logs, managing volumes and networks, and monitoring resource usage across the Docker environment.
All compose stacks are deployed and managed through Portainer rather than by
running docker compose directly on the host. Portainer stores stack definitions
internally (under /data/compose/) and rebuilds containers from those definitions
when updated.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://portainer.home.local |
LAN access via Step-CA TLS |
| HTTPS UI | https://192.168.1.85:9443 |
Direct IP fallback |
| Tunnel | Port 8001 (agent) |
For Edge Agent / remote access |
Portainer does not have an external (internet-facing) Traefik route. Access is LAN-only or via Tailscale.
Configuration
Image: portainer/portainer-ee:latest
Compose project: portainer
Ports
| Port | Protocol | Purpose |
|---|---|---|
8001 |
TCP | Edge Agent tunnel port |
9443 |
TCP | HTTPS management UI (host-bound) |
Traefik Labels
traefik.enable: "true"
traefik.http.routers.portainer.rule: Host(`portainer.home.local`)
traefik.http.routers.portainer.entrypoints: websecure
traefik.http.routers.portainer.tls.certresolver: step-ca
traefik.http.services.portainer.loadbalancer.server.port: 9443
Internal-only route — no external Traefik router.
Volumes / Bind Mounts
| Host Path / Volume | Container Path | Purpose |
|---|---|---|
portainer_portainer_data |
/data |
Portainer state and stack data (named volume, external) |
/var/run/docker.sock |
/var/run/docker.sock |
Direct Docker socket access |
The
portainer_portainer_datavolume is declaredexternal: true— it must exist before the stack is started.
Note: Portainer mounts the Docker socket directly. This is intentional for Portainer EE; it is the only service with direct socket access.
Dependencies
- Docker socket (
/var/run/docker.sock) — no other service dependencies - Traefik on
traefik-netfor theportainer.home.localroute - Step-CA for the internal TLS certificate
Notes / Gotchas
- Portainer stores all deployed stack definitions under
/data/compose/<id>/v<version>/. These are the authoritative copies of each stack'sdocker-compose.ymlwhile managed through Portainer. Files under/home/jeeves/docker/may be out of date if a stack was edited directly in the Portainer UI. - Portainer EE licence key is stored in the Portainer UI and must be re-entered if
the
portainer_portainer_datavolume is lost. - The
portainer-ee:latesttag follows the latest stable EE release. Pin to a specific version (e.g.portainer-ee:2.22.0) before any planned maintenance to avoid unintended upgrades. - Portainer agent is not separately deployed on Centerpoint — Portainer connects to its local Docker daemon directly via the socket.
- Portainer does not work over Traefik - I have not been troubleshooting this, and is a future action or Task to manage
FUTURE WORK
- Identify issue with Portainer and Traefik interaction
- Migrate Portainer cert with Step-CA cert for consistent cert management
Last Updated: 2026-06-16
Authentik (SSO / Identity Provider)
Overview
Authentik is the identity provider and single sign-on (SSO) gateway for all
externally-accessible services on Centerpoint. It implements ForwardAuth middleware
for Traefik, meaning that any request arriving at an external route tagged with
authentik-auth@docker is intercepted and validated by Authentik before being
forwarded to the backend service.
Authentik also serves as an OAuth2/OIDC provider — services like BookStack use OIDC for native user login rather than ForwardAuth.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://auth.home.local |
LAN access via Step-CA TLS |
| External | https://auth.jeevesconsults.ca |
Internet-facing — OIDC redirect target |
The external URL (auth.jeevesconsults.ca) is the one registered as the OIDC issuer
with downstream services. It must be reachable by browsers completing OAuth2 flows.
Containers in This Stack
| Container | Image | Role |
|---|---|---|
authentik-server |
ghcr.io/goauthentik/server:2026.5.x |
HTTP server + ForwardAuth endpoint |
authentik-worker |
ghcr.io/goauthentik/server:2026.5.x |
Background task worker (Rust entry) |
authentik-postgresql |
postgres:16-alpine |
Primary database |
authentik-geoip |
ghcr.io/maxmind/geoipupdate:v7.x |
GeoIP database updater (MaxMind) |
Startup Order
authentik-postgresql must reach a healthy state before authentik-server and
authentik-worker start. The authentik-geoip sidecar runs independently.
Configuration
Compose project: authentik
Key Environment Variables
| Variable | Value / Notes |
|---|---|
AUTHENTIK_SECRET_KEY |
REDACTED — long random string, must stay constant |
AUTHENTIK_POSTGRESQL__HOST |
authentik-postgresql |
AUTHENTIK_POSTGRESQL__NAME |
authentik |
AUTHENTIK_POSTGRESQL__USER |
authentik |
AUTHENTIK_POSTGRESQL__PASSWORD |
REDACTED |
AUTHENTIK_REDIS__HOST |
redis (internal sidecar or external Redis) |
AUTHENTIK_LISTEN__HTTP |
0.0.0.0:9000 — explicit bind required for Docker bridge |
AUTHENTIK_ERROR_REPORTING__ENABLED |
false |
Important:
AUTHENTIK_LISTEN__HTTP: "0.0.0.0:9000"is required in Authentik 2026.5+. Newer versions default to[::](IPv6 wildcard) which Docker bridge networks cannot reach via IPv4. Without this override, ForwardAuth requests from Traefik fail silently.
ForwardAuth Middleware
The Authentik ForwardAuth middleware is defined on the authentik-server container
labels and is available to all services on traefik-net as:
authentik-auth@docker
Example usage on a protected external route:
labels:
- "traefik.http.routers.<name>-ext.middlewares=authentik-auth@docker,plex-geoblock@file,crowdsec-bouncer@file"
OIDC Configuration (for native OIDC apps)
Applications using OIDC (e.g. BookStack) configure Authentik as their provider with the following settings:
| Setting | Value |
|---|---|
| Issuer URL | https://auth.jeevesconsults.ca/application/o/<app-slug>/ |
| JWKS URL | <issuer>/.well-known/jwks.json |
| Client ID | Per-application (set in Authentik admin UI) |
| Client Secret | REDACTED — set per-application |
email_verified |
Requires custom Scope Mapping returning true (2026.5+ change) |
Volumes / Bind Mounts
| Host Path / Volume | Container Path | Purpose |
|---|---|---|
authentik_media |
/media |
Uploaded assets (logos, avatars) |
authentik_custom-templates |
/templates |
Custom email / flow templates |
authentik_postgresql_data |
/var/lib/postgresql/data |
PostgreSQL data directory |
authentik_geoip_data |
/usr/share/GeoIP |
MaxMind GeoIP databases |
All volumes are named Docker volumes managed by Portainer.
Sub-section: PostgreSQL
The authentik-postgresql container is a dedicated Postgres 16 sidecar that stores
all Authentik state: users, groups, policies, flows, tokens, and audit logs. It is not
shared with any other service.
Database credentials are passed to the server via AUTHENTIK_POSTGRESQL__* environment
variables. The container is on the authentik-internal network only — it is never
exposed to traefik-net or the host.
Sub-section: GeoIP Updater
authentik-geoip runs the MaxMind GeoIPUpdate daemon, which downloads and refreshes
the GeoLite2-City and GeoLite2-ASN databases on a schedule. The databases are
shared into authentik-server via the authentik_geoip_data volume.
A MaxMind account and licence key (REDACTED) are required for the GeoIP databases.
Sub-section: Worker
authentik-worker runs as the same container image as authentik-server but with
the worker entrypoint. It handles background tasks: email delivery, outpost health
checks, blueprint application, and event cleanup. Since Authentik 2025.10+, the
worker uses a Rust-based entrypoint for improved performance.
Dependencies
authentik-postgresql(must be healthy before server/worker start)traefik-net(server must be on this network for ForwardAuth to reach it)- MaxMind account for GeoIP updates (not strictly required, but GeoIP features will be unavailable without it)
Notes / Gotchas
- If the
AUTHENTIK_SECRET_KEYchanges, all tokens, sessions, and cookies are immediately invalidated — all users will be logged out across all services. - Authentik's admin interface is at
/if/admin/. Initial admin credentials are set via theAUTHENTIK_BOOTSTRAP_PASSWORDenv var on first start. - For OIDC apps:
email_verifieddefaults tofalsesince Authentik 2025.10. Create a custom Scope Mapping that hard-codes"email_verified": Trueand attach it to the OAuth2 provider. - Existing local users who also log in via OIDC must have their External Auth ID
set in Admin → Users → Edit → External Auth ID to match the
subclaim from the OIDC token, otherwise two separate accounts will be created. DOZZLE_ENABLE_SHELL=trueon Dozzle requires that this admin account is separate from the Authentik service account.
Last Updated: 2026-06-16
AdGuard Home
Overview
AdGuard Home serves as the primary LAN-wide DNS resolver for the entire homelab
network. All devices on the 192.168.1.0/24 subnet use it as their primary DNS
server (192.168.1.85:53). It provides:
- DNS-based ad and tracker blocking
- Wildcard DNS entry
*.home.local → 192.168.1.85enabling all Traefik internal routes - DNS rewrites for custom local hostnames
- Query logging and per-client statistics
- Upstream DNS forwarding (via DoH to external resolvers)
A secondary AdGuard Home instance runs on the CorSec server (Home Assistant
machine at 192.168.1.64). DNS rewrites are also maintained there so that name
resolution continues during Centerpoint maintenance or restarts. Client devices
should have both IPs configured as DNS resolvers.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://adguard.home.local |
LAN access via Step-CA TLS |
| Direct | http://192.168.1.85:80 |
Plain HTTP UI (AdGuard internal port) |
DNS service itself runs on port 53 (TCP + UDP), bound to the host IP.
Configuration
Image: adguard/adguardhome:v0.107.71
Compose project: adguardhome (managed via Portainer)
Ports
| Port | Protocol | Purpose |
|---|---|---|
53 |
TCP+UDP | DNS resolver (host-bound) |
80 |
TCP | AdGuard web UI (HTTP, internal) |
443 |
TCP | DNS-over-HTTPS |
853 |
TCP | DNS-over-TLS |
3000 |
TCP | Initial setup port |
Only port 53 is bound to the host. Other ports are exposed only within traefik-net.
Traefik Labels
traefik.enable: "true"
traefik.http.routers.adguard.rule: Host(`adguard.home.local`)
traefik.http.routers.adguard.entrypoints: websecure
traefik.http.routers.adguard.tls.certresolver: step-ca
traefik.http.services.adguard.loadbalancer.server.port: 80
Internal-only route — no external Traefik router.
Critical Wildcard DNS Entry
AdGuard Home (both primary and secondary) must have a DNS Rewrite configured as:
*.home.local → 192.168.1.85
This single wildcard record means Traefik receives all *.home.local HTTP requests
and routes them by hostname. Without it, no internal service domain resolves.
All custom DNS rewrites (for individual hostnames outside the wildcard) must be kept in sync between the Centerpoint and CorSec instances.
Upstream DNS Resolvers
Upstream DNS (for forwarding public queries) is configured in AdGuard's settings UI. Common configuration:
- Primary:
https://dns.cloudflare.com/dns-query(DoH) - Secondary:
https://dns.google/dns-query(DoH)
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/adguard/conf |
/opt/adguardhome/conf |
Configuration (AdGuardHome.yaml) |
/home/jeeves/docker/adguard/work |
/opt/adguardhome/work |
Query logs and statistics database |
Networks
AdGuard Home is on traefik-net for its web UI route and has port 53 bound
directly to the host for DNS service.
Dependencies
- None (AdGuard must start first; it has no upstream service dependencies)
- AdGuard's own DNS resolution bootstraps via the upstream DoH resolvers using hardcoded IPs if DNS is not yet available
High Availability
| Instance | Host | IP | Role |
|---|---|---|---|
| Primary | Centerpoint | 192.168.1.85 |
Primary — full config, rewrites, logging |
| Secondary | CorSec (Home Assistant) | 192.168.1.64 |
Backup — same rewrites, ad-block lists |
Client devices should configure both DNS servers in priority order. The secondary takes over automatically if the primary becomes unreachable.
DNS rewrites must be manually kept in sync between the two instances — there is no automated synchronisation.
Notes / Gotchas
- The
work/datadirectory requires root-level permissions — the container runs as root. This is expected behaviour. - If port
53is already in use on the host (e.g.systemd-resolved), AdGuard will fail to start. On Ubuntu 24.04, disable the stub listener:sudo systemctl disable --now systemd-resolved - Client-specific rules (blocking, bypass, custom upstream) are stored in
AdGuardHome.yamland are preserved across container upgrades as long as theconf/bind mount is intact. - AdGuard Home should be pinned to a specific version tag rather than
latestto avoid schema migrations breaking the config on unexpected upgrades. - When adding a new
*.home.localservice, no DNS change is needed (the wildcard covers it). New custom hostnames outside the wildcard pattern must be added to both the primary and secondary AdGuard instances.
FUTURE WORK
- Update the DNS blocklist to include more sites
- Provide a summary of sites blocked via AI for review and/or include in Grafana for trend analysis to know if the numbers go up or down over time- find rogue machines
Last Updated: 2026-06-16
CrowdSec
Overview
CrowdSec is a collaborative intrusion detection and prevention system integrated directly into Traefik. It analyses Traefik access logs in real time, detects attack patterns (brute force, CVE exploitation, bad bots, etc.), and instructs Traefik to block flagged IPs before they reach any backend service.
The stack consists of two containers:
crowdsec— the Security Engine (LAPI + agent). Ingests logs, applies detection scenarios, maintains a decision list, and shares signals with the CrowdSec community.crowdsec-bouncer-traefik— the Traefik bouncer. Traefik forwards every incoming request to it via ForwardAuth; the bouncer checks the IP against CrowdSec's local decision database and returns allow/deny.
Access
Neither CrowdSec container has a web UI. Management is via the cscli CLI inside
the crowdsec container:
docker exec -it crowdsec cscli decisions list
docker exec -it crowdsec cscli alerts list
docker exec -it crowdsec cscli metrics
Configuration
Compose project: crowdsec (managed via Portainer)
Containers
crowdsec — Security Engine
Image: crowdsecurity/crowdsec:latest
| Property | Value |
|---|---|
| Network | traefik-net |
| Restart policy | unless-stopped |
Key environment variables:
| Variable | Value / Notes |
|---|---|
GID |
1000 |
COLLECTIONS |
crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/whitelist-good-actors |
BOUNCER_KEY_TRAEFIK |
REDACTED — shared secret used by the bouncer to authenticate with LAPI |
Collections installed:
| Collection | Purpose |
|---|---|
crowdsecurity/traefik |
Detects attack patterns in Traefik access logs |
crowdsecurity/http-cve |
Detects exploitation of known HTTP CVEs |
crowdsecurity/whitelist-good-actors |
Whitelists known-good crawlers and services |
crowdsec-bouncer-traefik — Traefik Bouncer
Image: fbonalair/traefik-crowdsec-bouncer:latest
| Property | Value |
|---|---|
| Network | traefik-net |
| Internal port | 8080 (ForwardAuth endpoint) |
Key environment variables:
| Variable | Value / Notes |
|---|---|
GIN_MODE |
release |
CROWDSEC_AGENT_HOST |
crowdsec:8080 — CrowdSec LAPI endpoint |
CROWDSEC_BOUNCER_API_KEY |
REDACTED — must match BOUNCER_KEY_TRAEFIK |
How the Bouncer Integrates with Traefik
The middleware is defined in /home/jeeves/docker/traefik/config/crowdsec.yml:
http:
middlewares:
crowdsec-bouncer:
forwardAuth:
address: http://crowdsec-bouncer-traefik:8080/api/v1/forwardAuth
trustForwardHeader: true
This middleware is referenced on external Traefik routes as crowdsec-bouncer@file.
Log Ingestion
CrowdSec reads Traefik access logs from two bind-mounted paths:
| Host Path | Container Path | Notes |
|---|---|---|
/var/log/traefik |
/var/log/traefik:ro |
Primary log location |
/home/jeeves/docker/traefik/logs |
/logs/traefik:ro |
Secondary / rotated logs |
The acquis.yaml config file at /home/jeeves/docker/crowdsec/config/acquis.yaml
defines which log files to tail and in what format.
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/crowdsec/data |
/var/lib/crowdsec/data |
Decision database and GeoIP data |
/home/jeeves/docker/crowdsec/config |
/etc/crowdsec |
Scenarios, parsers, config |
/var/log/traefik |
/var/log/traefik:ro |
Traefik access log (read-only) |
/home/jeeves/docker/traefik/logs |
/logs/traefik:ro |
Traefik rotated logs (read-only) |
Dependencies
- Traefik (must be running and writing access logs for CrowdSec to process)
crowdsecLAPI must be healthy beforecrowdsec-bouncer-traefikstarts- Internet access required for CrowdSec community hub sync (pulling updated block lists)
Notes / Gotchas
- The
BOUNCER_KEY_TRAEFIK/CROWDSEC_BOUNCER_API_KEYpair must match exactly. If mismatched, the bouncer will fail to authenticate with LAPI and Traefik will receive a 200 (passthrough) response from ForwardAuth — effectively disabling the bouncer silently. - CrowdSec community sharing (
online_api_credentials.yaml) requires an account atapp.crowdsec.net. The credentials file is stored at/home/jeeves/docker/crowdsec/config/online_api_credentials.yaml. crowdsec-bouncer@filemiddleware must be applied to external routes only. Applying it to internal (*.home.local) routes will check LAN IPs against the community block list, which may block legitimate traffic unexpectedly.- Manually ban an IP with:
docker exec -it crowdsec cscli decisions add --ip <IP> --duration 24h --reason "manual" - Remove a ban:
docker exec -it crowdsec cscli decisions delete --ip <IP>
Last Updated: 2026-06-16
Dozzle
Overview
Dozzle is a lightweight, real-time log viewer for Docker containers. It provides a
web UI to stream, search, and follow container logs without needing to SSH into the
host and run docker logs. It is configured to monitor containers on both
Centerpoint and Lusankya (the second homelab server at 192.168.1.114).
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://dozzle.home.local |
LAN access via Step-CA TLS |
No external (internet-facing) Traefik route — LAN and Tailscale access only.
Configuration
Image: amir20/dozzle:latest (v10.6.0 at time of writing)
Compose project: dozzle (managed via Portainer)
Environment Variables
| Variable | Value | Purpose |
|---|---|---|
DOZZLE_HOSTNAME |
Centerpoint |
Display name for the local host |
DOZZLE_REMOTE_HOST |
tcp://192.168.1.114:2375|Lusankya |
Adds Lusankya as a remote Docker host |
DOZZLE_ENABLE_ACTIONS |
false |
Disables container start/stop from UI |
DOZZLE_ENABLE_SHELL |
true |
Enables shell access to containers |
DOZZLE_ENABLE_SHELL=trueallows executing shell commands inside any container from the Dozzle UI. This is a powerful capability — ensure Dozzle is not accessible externally.
Traefik Labels
traefik.enable: "true"
traefik.http.routers.dozzle.rule: Host(`dozzle.home.local`)
traefik.http.routers.dozzle.entrypoints: websecure
traefik.http.routers.dozzle.tls.certresolver: step-ca
traefik.http.services.dozzle.loadbalancer.server.port: 8080
Internal-only route. No Authentik ForwardAuth on this route — access is controlled at the network level (LAN + Tailscale only).
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/var/run/docker.sock |
/var/run/docker.sock:ro |
Read-only Docker socket access |
Dozzle mounts the Docker socket read-only. No persistent data volume is required — Dozzle streams logs directly from the Docker daemon and does not store them.
Networks
| Network | Purpose |
|---|---|
traefik-net |
Exposes the Dozzle web UI |
Remote Hosts
Dozzle connects to Lusankya's Docker daemon at tcp://192.168.1.114:2375. This
requires Lusankya's Docker daemon to have TCP exposure enabled. Logs from both
Centerpoint and Lusankya containers are visible in a single Dozzle instance.
Dependencies
- Docker socket on Centerpoint (
/var/run/docker.sock) - Lusankya Docker daemon accessible at
tcp://192.168.1.114:2375 - Traefik on
traefik-netfor thedozzle.home.localroute
Notes / Gotchas
DOZZLE_ENABLE_SHELL=truegives the ability toexecinto any container from the browser. Treat this with the same care as SSH access to the host.- Dozzle does not persist logs. Once a container is removed, its historical logs
are no longer accessible from Dozzle (use
docker logsor a log aggregation stack for archival). - The
latestimage tag tracks the latest release. Dozzle updates frequently — check the changelog before pulling, especially for breaking UI changes. - Lusankya's Docker TCP port (
2375) is unencrypted. This is acceptable on the private LAN but should never be exposed externally.
Last Updated: 2026-06-16
Uptime-Kuma
Overview
Uptime-Kuma is a self-hosted uptime monitoring tool that tracks the availability of services and endpoints, sends alerts on downtime, and displays historical uptime statistics. It serves as the primary observability dashboard for the homelab — monitoring both internal services and external URLs.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | (via Tailscale or LAN direct) | Port 3001 on 192.168.1.85 |
| External | https://uptime.jeeves5454.ddns.net |
Internet-facing, protected by Authentik + GeoBlock + CrowdSec |
Configuration
Image: louislam/uptime-kuma:2
Compose project: uptime-kuma (managed via Portainer; local file at
/home/jeeves/docker/uptime_kuma/docker-compose.yml)
Ports
| Port | Protocol | Purpose |
|---|---|---|
3001 |
TCP | Web UI (also bound to host for direct access) |
Traefik Labels
traefik.enable: "true"
traefik.http.routers.uptime.rule: Host(`uptime.jeeves5454.ddns.net`)
traefik.http.routers.uptime.entrypoints: websecure
traefik.http.routers.uptime.tls.certresolver: letsencrypt
traefik.http.routers.uptime.middlewares: authentik-auth@docker,plex-geoblock@file,crowdsec-bouncer@file,uptime-headers
traefik.http.middlewares.uptime-headers.headers.customrequestheaders.X-Forwarded-Proto: https
traefik.http.services.uptime.loadbalancer.server.port: 3001
The uptime-headers middleware injects X-Forwarded-Proto: https — required
because Uptime-Kuma needs to know the original scheme for correct redirect handling.
External access is gated behind Authentik SSO, GeoBlock (CA/US/IN), and CrowdSec.
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/uptime_kuma/uptime-kuma-data |
/app/data |
SQLite DB, config, logs |
The data directory stores:
kuma.db— SQLite database with monitor config, history, and incidentserror.log— Uptime-Kuma application error log
Networks
| Network | Purpose |
|---|---|
traefik-net |
External Traefik route |
uptime-kuma_monitoring |
Internal per-stack network |
Dependencies
- Traefik on
traefik-netfor the external HTTPS route - Authentik at
auth.jeevesconsults.cafor SSO on the external route - Network connectivity to all monitored endpoints
Notes / Gotchas
- Uptime-Kuma v2 uses SQLite. The
kuma.dbfile is the entire state of the service — back it up before any upgrade. - The
X-Forwarded-Protoheader middleware (uptime-headers) is specifically required for Uptime-Kuma to generate correct URLs in status page embeds and notifications. - Monitor configurations (which services to check, intervals, notifications) are all stored in the UI and persisted in the SQLite database. There is no YAML-based config file.
- If Uptime-Kuma reports false positives for internal services, verify that DNS
resolution works correctly from within the Uptime-Kuma container on
traefik-net. - Notification integrations (email, Telegram, Slack, etc.) are configured in the UI under Settings → Notifications.
Last Updated: 2026-06-16
Homepage.Dev
Overview
Homepage (gethomepage.dev) is the self-hosted service dashboard for Centerpoint. It provides a single-pane-of-glass view of all running services, organised into three tabs that reflect the different access methods available for each service. It also displays live Docker container status by connecting directly to the Docker socket.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://centerpoint.home.local |
LAN access via Step-CA TLS |
| Direct | http://192.168.1.85:3003 |
Plain HTTP fallback |
No external (internet-facing) Traefik route.
Configuration
Image: ghcr.io/gethomepage/homepage:latest (v1.13.2 at time of writing)
Compose project: homepage (Portainer-managed; local file at
/home/jeeves/docker/homepage/docker-compose.yml)
Ports
| Port | Protocol | Purpose |
|---|---|---|
3003 |
TCP | Web UI (mapped from internal 3000) |
Traefik Labels
traefik.enable: "true"
traefik.http.routers.homepage.rule: Host(`centerpoint.home.local`)
traefik.http.routers.homepage.entrypoints: websecure
traefik.http.routers.homepage.tls.certresolver: step-ca
traefik.http.services.homepage.loadbalancer.server.port: 3000
Internal-only route on centerpoint.home.local.
Environment Variables
| Variable | Value |
|---|---|
PUID |
1000 |
PGID |
1000 |
LOG_TARGETS |
stdout |
HOMEPAGE_ALLOWED_HOSTS |
gethomepage.dev,192.168.1.64:3003,localhost:3003 |
Dashboard Structure
Homepage is configured with three tabs:
| Tab | Service suffix | Description |
|---|---|---|
| External Secure | (plain names) | Services via *.jeeves5454.ddns.net or *.jeevesconsults.ca |
| Internal Secure | (LAN) |
Services via *.home.local through Traefik |
| Internal Unsecured | (IP) |
Services via direct http://IP:port |
The default tab is set to Internal Secure.
Services can appear in multiple tabs if they have multiple access routes. Docker
container status widgets (server: centerpoint, container: <name>) are applied
to services where applicable.
Configuration Files
All config files live under /home/jeeves/docker/homepage/config/:
| File | Purpose |
|---|---|
settings.yaml |
Dashboard title, theme, background, tab layout |
services.yaml |
All service entries grouped by tab and category |
widgets.yaml |
Top-bar info widgets (system stats, weather, etc.) |
bookmarks.yaml |
Bookmark groups (if used) |
docker.yaml |
Docker socket connection config for container status |
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/homepage/config |
/app/config |
All dashboard config files |
/var/run/docker.sock |
/var/run/docker.sock:ro |
Read-only Docker socket for container status |
Dependencies
- Docker socket for live container status display
- Traefik on
traefik-netfor thecenterpoint.home.localroute - API keys for service widgets (stored in
services.yaml— secrets should be kept in asecrets.yamlfile or environment variables, not committed to version control)
Notes / Gotchas
- Homepage requires group names to be globally unique across all tabs. The
(LAN)and(IP)suffixes on group names solve this constraint for the multi-tab layout. HOMEPAGE_ALLOWED_HOSTSmust include any hostname or IP:port used to access the dashboard; otherwise Homepage returns a 403.- The Docker socket mount allows Homepage to show live container state. Homepage accesses it read-only and does not have the ability to start or stop containers.
- Config changes take effect immediately on file save — no container restart required.
- Quick Launch search only finds services listed in
services.yaml. To enable search indexing for descriptions, setsearchDescriptions: trueinsettings.yaml.
Last Updated: 2026-06-16
AI & Automation
Chapter Introduction
Overview
This chapter documents the AI inference, voice processing, and workflow automation services running on Centerpoint. All GPU-accelerated workloads leverage an NVIDIA GeForce RTX 5080 connected to the Mini PC via OcuLink, providing 16 GB of GDDR7 VRAM on the Blackwell architecture (Compute Capability 12.0).
GPU Hardware
| Property | Value |
|---|---|
| GPU | NVIDIA GeForce RTX 5080 |
| VRAM | 16 GB GDDR7 (16,303 MiB) |
| Architecture | Blackwell (Compute Capability 12.0) |
| Connection | OcuLink (external GPU enclosure) |
| NVIDIA Driver | 580.159.03 |
| CUDA Version | 12.9 |
| Container Runtime | nvidia (all GPU containers) |
All containers that use the GPU are launched with runtime: nvidia and
NVIDIA_VISIBLE_DEVICES=all. No device passthrough via --device /dev/dri is
used — the Intel Arc / IPEX-LLM path has been retired.
Services in This Chapter
| Service | Container(s) | GPU | Purpose |
|---|---|---|---|
| Ollama | ollama |
Yes | Local LLM inference backend |
| Open Web UI | open-webui |
No | Chat interface for Ollama and OpenAI APIs |
| Faster-Whisper | faster-whisper |
Yes | Speech-to-text (Whisper large-v3-turbo) |
| Kokoro | kokoro |
Yes | Text-to-speech (TTS) API |
| Riffado | riffado, riffado-db |
No | AI audio podcast app (formerly OpenPlaud) |
| PaperlessAI | paperless-ai |
No | Autonomous document classification via Ollama |
| N8N | n8n |
No | Workflow automation platform |
| Open Notebook | open-notebook, open-notebook-db |
No | AI research notebook (SurrealDB backend) |
| MCP GitHub | mcp-github-proxy, github-mcp-server |
No | GitHub MCP server with OAuth 2.1 |
Compose Project
Most AI stack services (Ollama, Open Web UI, Kokoro, Faster-Whisper, Open Notebook,
Riffado) are managed as a single Portainer compose project called ai-stack on
a shared ai-internal bridge network plus traefik-net. N8N, PaperlessAI, and
MCP GitHub are separate Portainer stacks.
Last Updated: 2026-06-16
Ollama
Overview
Ollama is the local large language model (LLM) inference backend for the homelab. It serves models via an OpenAI-compatible REST API and is consumed by Open Web UI, PaperlessAI, and any other service that needs LLM inference without sending data to external providers.
Ollama runs with full NVIDIA RTX 5080 acceleration via the nvidia container
runtime. All model weights are stored on the local NVMe system drive.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://ollama.home.local |
Traefik-proxied HTTPS |
| Direct | http://192.168.1.85:11434 |
Raw API (no TLS) |
No external (internet-facing) route — LAN and Tailscale access only.
Configuration
Image: ollama/ollama:latest
Compose project: ai-stack
Runtime: nvidia
Ports
| Port | Protocol | Purpose |
|---|---|---|
11434 |
TCP | Ollama REST API (host-bound) |
Traefik Labels
traefik.enable: "true"
traefik.docker.network: traefik-net
traefik.http.routers.ollama.rule: Host(`ollama.home.local`)
traefik.http.routers.ollama.entrypoints: websecure
traefik.http.routers.ollama.tls.certresolver: step-ca
traefik.http.services.ollama.loadbalancer.server.port: 11434
Internal-only route, no authentication middleware — API access is unrestricted on the LAN. Callers must be on the LAN or Tailscale.
Environment Variables
| Variable | Value | Purpose |
|---|---|---|
OLLAMA_HOST |
0.0.0.0 |
Listen on all interfaces |
NVIDIA_VISIBLE_DEVICES |
all |
Expose all NVIDIA GPUs to container |
NVIDIA_DRIVER_CAPABILITIES |
compute,utility |
Required NVIDIA driver caps |
OLLAMA_NUM_GPU |
999 |
Use all available GPU layers |
no_proxy |
localhost,127.0.0.1 |
Bypass proxy for local calls |
GPU Acceleration
Ollama uses the nvidia container runtime. The RTX 5080 provides 16 GB of VRAM,
allowing large models (7B–27B parameter range) to run fully in VRAM without CPU
offloading.
runtime: nvidia
environment:
- NVIDIA_VISIBLE_DEVICES=all
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/ollama/ |
/root/.ollama |
Model weights and config |
Model files are stored under /home/jeeves/docker/ollama/models/ on the local
NVMe system drive (1.8 TB). Large models can consume significant space.
Networks
| Network | Purpose |
|---|---|
ai-stack_ai-internal |
Internal communication with Open Web UI, PaperlessAI |
traefik-net |
Exposes Ollama API via Traefik |
Dependencies
- NVIDIA container runtime (
nvidia) on the Docker daemon - RTX 5080 connected via OcuLink (must be recognised as a CUDA device)
Notes / Gotchas
OLLAMA_NUM_GPU=999is the conventional way to tell Ollama to use as many GPU layers as possible. It does not literally use 999 GPUs.- Models are downloaded via
ollama pull <model>or via the Open Web UI admin panel. Downloaded models persist in the bind-mounted/root/.ollamadirectory. - If the OcuLink connection drops or the GPU is not recognised, Ollama falls back to
CPU inference — responses will be significantly slower. Check with:
docker exec -it ollama ollama ps - The local file at
/home/jeeves/docker/ai-stack/ollama-intel-arc/docker-compose.ymlis the legacy IPEX-LLM config and is no longer in use. The current stack is managed via Portainer and usesollama/ollama:latestwith CUDA.
Last Updated: 2026-06-16
Open-WebUI
Overview
Open Web UI is the primary chat and AI management interface for the homelab. It provides a ChatGPT-style web frontend connected to the local Ollama backend, with support for conversation history, model selection, RAG (document chat), image generation, and tool use. It can also proxy to external OpenAI-compatible APIs.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://ai.home.local |
LAN access via Step-CA TLS |
| External | https://ai.jeeves5454.ddns.net |
Internet-facing — Authentik SSO + GeoBlock + CrowdSec |
Configuration
Image: ghcr.io/open-webui/open-webui:main
Compose project: ai-stack
Ports
| Port | Protocol | Purpose |
|---|---|---|
3015 |
TCP | Web UI (mapped from internal 8080) |
Traefik Labels
# Internal route
traefik.http.routers.openwebui-internal.rule: Host(`ai.home.local`)
traefik.http.routers.openwebui-internal.entrypoints: websecure
traefik.http.routers.openwebui-internal.tls.certresolver: step-ca
traefik.http.routers.openwebui-internal.service: openwebui-svc
# External route
traefik.http.routers.openwebui-external.rule: Host(`ai.jeeves5454.ddns.net`)
traefik.http.routers.openwebui-external.entrypoints: websecure
traefik.http.routers.openwebui-external.tls.certresolver: letsencrypt
traefik.http.routers.openwebui-external.middlewares: authentik-auth@docker,plex-geoblock@file,crowdsec-bouncer@file
traefik.http.routers.openwebui-external.service: openwebui-svc
traefik.http.services.openwebui-svc.loadbalancer.server.port: 8080
Environment Variables
| Variable | Value / Notes |
|---|---|
WEBUI_AUTH |
False — authentication handled by Authentik |
ENABLE_OLLAMA_API |
True |
ENABLE_OPENAI_API |
True |
ENABLE_IMAGE_GENERATION |
True |
IMAGE_GENERATION_ENGINE |
automatic1111 |
IMAGE_GENERATION_MODEL |
dreamshaper_8 |
IMAGE_SIZE |
400x400 |
IMAGE_STEPS |
8 |
AUTOMATIC1111_BASE_URL |
http://stable-diffusion:7860/ |
AUTOMATIC1111_CFG_SCALE |
2 |
AUTOMATIC1111_SAMPLER |
DPM++ SDE |
AUTOMATIC1111_SCHEDULER |
Karras |
WEBUI_AUTH=Falsedisables Open Web UI's own login page. Authentication is delegated entirely to Authentik ForwardAuth on the external route. On the internal LAN route, the interface is open — access is controlled by network boundary only.
Volumes / Bind Mounts
| Host Path / Volume | Container Path | Purpose |
|---|---|---|
open_webui_open-webui-data |
/app/backend/data |
Conversation history, settings, uploaded docs (named volume, external) |
Networks
| Network | Purpose |
|---|---|
ai-stack_ai-internal |
Reaches Ollama backend on ai-internal network |
traefik-net |
Exposes the web UI via Traefik |
Dependencies
ollama— must be running for model inference; Open Web UI will start without it but model requests will fail- Authentik — required for external route SSO; LAN route is unaffected if Authentik is down
Notes / Gotchas
- The
mainimage tag tracks the latest development build. For stability, consider pinning to a tagged release (e.g.v0.6.x). WEBUI_AUTH=Falsemeans anyone on the LAN can access the internal URL without credentials. If untrusted devices are on the LAN, consider enabling WEBUI_AUTH and creating user accounts, or adding Authentik middleware to the internal route as well.- Conversation history and user settings are stored in the named Docker volume. Back this up before upgrades.
- Open Web UI admin panel is at
https://ai.home.local/admin/— first user to register (if auth is enabled) becomes the admin. - Image generation requires the stable-diffusion container to be running (separate
service, also on
ai-internalnetwork).
Last Updated: 2026-06-16
Faster Whisper
Overview
Faster-Whisper is the speech-to-text transcription service for the homelab. It
runs OpenAI's Whisper model via the faster-whisper library (CTranslate2 backend),
which offers significantly faster inference than the original Whisper implementation
at equivalent or lower VRAM usage.
It is used by N8N automation workflows for audio transcription tasks (e.g. podcast processing in the Minuspod pipeline).
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://whisper.home.local |
LAN access via Step-CA TLS |
No external route — internal automation use only.
Configuration
Image: hwdsl2/whisper-server:cuda
Compose project: ai-stack
Runtime: nvidia
CUDA Version: 12.9
Ports
| Port | Protocol | Purpose |
|---|---|---|
9015 |
TCP | HTTP API (mapped from internal 9000) |
Traefik Labels
traefik.enable: "true"
traefik.docker.network: traefik-net
traefik.http.routers.whisper.rule: Host(`whisper.home.local`)
traefik.http.routers.whisper.entrypoints: websecure
traefik.http.routers.whisper.tls.certresolver: step-ca
traefik.http.services.whisper.loadbalancer.server.port: 9000
Environment Variables
| Variable | Value | Purpose |
|---|---|---|
WHISPER_MODEL |
large-v3-turbo |
Whisper model variant to load |
WHISPER_DEVICE |
cuda |
Run inference on GPU |
WHISPER_COMPUTE_TYPE |
float16 |
FP16 precision (optimal for CUDA) |
WHISPER_LANGUAGE |
en |
Default transcription language |
NVIDIA_VISIBLE_DEVICES |
all |
Expose all NVIDIA GPUs |
NVIDIA_DRIVER_CAPABILITIES |
compute,utility |
Required NVIDIA driver capabilities |
Model: large-v3-turbo — the distilled variant of Whisper large-v3, offering
near-large accuracy at roughly 3× the speed and reduced VRAM usage.
GPU Acceleration
runtime: nvidia
environment:
- NVIDIA_VISIBLE_DEVICES=all
- WHISPER_DEVICE=cuda
- WHISPER_COMPUTE_TYPE=float16
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/ai-stack/whisper/files |
/var/lib/whisper |
Transcription input/output files |
Model weights are downloaded to a temp directory inside the container on first start and cached within the container layer (not persisted in a named volume).
Networks
| Network | Purpose |
|---|---|
ai-stack_ai-internal |
Internal access from N8N workflows |
traefik-net |
Exposes API via Traefik |
API Usage
The whisper-server exposes a simple HTTP POST endpoint:
curl -X POST https://whisper.home.local/inference \
-F file=@audio.mp3 \
-F response_format=json
Response:
{"text": "Transcribed text here..."}
Dependencies
- NVIDIA container runtime with RTX 5080 access
- N8N (primary consumer via HTTP calls)
Notes / Gotchas
- The
large-v3-turbomodel is downloaded on first container start. This can take several minutes and the container will appear unresponsive until the download completes. float16compute type requires a GPU with FP16 support. The RTX 5080 (Blackwell) supports this natively. On CPU-only fallback, useint8instead.- The
filesbind mount (/var/lib/whisper) can be used to pre-stage audio files for batch transcription if needed.
Last Updated: 2026-06-16
Kokoro
Overview
Kokoro is a high-quality, locally-hosted text-to-speech (TTS) service. It exposes
an OpenAI-compatible TTS API, making it a drop-in replacement for external TTS
services in any application that supports the /v1/audio/speech endpoint.
It runs the Kokoro TTS model via the kokoro-fastapi server with full RTX 5080
GPU acceleration.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://kokoro.home.local |
LAN access via Step-CA TLS |
No external route — internal service only.
Configuration
Image: ghcr.io/remsky/kokoro-fastapi-gpu:latest-cu128
Compose project: ai-stack
Runtime: nvidia
CUDA Version: 12.8 (image) / 12.9 (host driver, backward compatible)
Ports
| Port | Protocol | Purpose |
|---|---|---|
8880 |
TCP | Kokoro FastAPI TTS API |
Traefik Labels
traefik.enable: "true"
traefik.docker.network: traefik-net
traefik.http.routers.kokoro.rule: Host(`kokoro.home.local`)
traefik.http.routers.kokoro.entrypoints: websecure
traefik.http.routers.kokoro.tls.certresolver: step-ca
traefik.http.services.kokoro.loadbalancer.server.port: 8880
GPU Acceleration
runtime: nvidia
environment:
- NVIDIA_VISIBLE_DEVICES=all
- DEVICE=gpu
- USE_GPU=true
The image tag latest-cu128 targets CUDA 12.8. The host driver (580.159.03) is
fully forward-compatible with this image.
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/kokoro |
/app/data |
Voice models and cached data |
Networks
| Network | Purpose |
|---|---|
ai-stack_ai-internal |
Internal access from Open Web UI and N8N |
traefik-net |
Exposes API via Traefik |
API Usage
Kokoro exposes an OpenAI-compatible TTS endpoint:
curl -X POST https://kokoro.home.local/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"model": "kokoro",
"input": "Hello from the homelab.",
"voice": "af_sky",
"response_format": "mp3"
}' --output speech.mp3
Available voices and model details are listed at https://kokoro.home.local/docs
(Swagger UI).
Dependencies
- NVIDIA container runtime with RTX 5080 access
Notes / Gotchas
- The
latest-cu128tag pulls the latest build compiled against CUDA 12.8. NVIDIA driver 580.x supports CUDA 12.9 on the host, which is backward-compatible. - Voice model files are cached in the
/app/databind mount on first use — initial synthesis requests may be slower while models are downloaded. - Kokoro is OpenAI API-compatible, meaning Open Web UI can be configured to use it as its TTS provider via the admin settings.
Last Updated: 2026-06-16
Riffado (OpenPlaud)
Overview
Riffado (formerly known as OpenPlaud) is a self-hosted AI audio and podcast management application. It handles audio file storage, playback, and AI-assisted processing within the homelab ecosystem.
Audio files are stored on the Ceph OSD volume (1TB_Vol1) rather than the system
NVMe, keeping large media off the primary drive.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://riffado.home.local |
LAN access via Step-CA TLS |
| External | https://riffado.jeeves5454.ddns.net |
Internet-facing — Authentik SSO + GeoBlock + CrowdSec |
Containers in This Stack
| Container | Image | Role |
|---|---|---|
riffado |
ghcr.io/riffado/riffado:latest |
Application server |
riffado-db |
postgres:16-alpine |
PostgreSQL database backend |
Configuration
Compose project: ai-stack
Environment Variables
| Variable | Value / Notes |
|---|---|
APP_URL |
https://riffado.jeeves5454.ddns.net |
HOSTNAME |
0.0.0.0 |
DATABASE_URL |
postgresql://postgres:**REDACTED**@riffado-db:5432/riffado |
DEFAULT_STORAGE_TYPE |
local |
LOCAL_STORAGE_PATH |
/app/storage |
DISABLE_REGISTRATION |
true |
BETTER_AUTH_SECRET |
REDACTED |
ENCRYPTION_KEY |
REDACTED |
NODE_ENV |
production |
DISABLE_REGISTRATION=true prevents new accounts from being created — access is
limited to pre-provisioned users and gated by Authentik on the external route.
Traefik Labels
# External route
traefik.http.routers.riffado-ext.rule: Host(`riffado.jeeves5454.ddns.net`)
traefik.http.routers.riffado-ext.entrypoints: websecure
traefik.http.routers.riffado-ext.tls.certresolver: letsencrypt
traefik.http.routers.riffado-ext.middlewares: authentik-auth@docker,plex-geoblock@file,crowdsec-bouncer@file
traefik.http.routers.riffado-ext.service: riffado-svc
# Internal route
traefik.http.routers.riffado-int.rule: Host(`riffado.home.local`)
traefik.http.routers.riffado-int.entrypoints: websecure
traefik.http.routers.riffado-int.tls.certresolver: step-ca
traefik.http.routers.riffado-int.service: riffado-svc
traefik.http.services.riffado-svc.loadbalancer.server.port: 3000
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/media/jeeves/1TB_Vol1/docker/riffado/audio |
/app/storage |
Audio file storage (Ceph OSD) |
Audio files are stored on the Ceph-managed OSD volume (nvme2n1, mounted at
/media/jeeves/1TB_Vol1). This keeps large audio files off the system NVMe and
on the dedicated storage volume.
Sub-section: PostgreSQL Database
riffado-db is a dedicated Postgres 16-alpine sidecar managing all Riffado
application state: user accounts, playlists, audio metadata, and processing
history. It is not shared with any other service.
| Host Path / Volume | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/riffado/db |
/var/lib/postgresql/data |
PostgreSQL data files |
The database container is on the riffado-internal network only — it is never
exposed to traefik-net.
Networks
| Network | Purpose |
|---|---|
traefik-net |
Exposes the Riffado web UI |
riffado-internal |
riffado ↔ riffado-db communication |
Dependencies
riffado-db(must be healthy beforeriffadostarts)- Authentik for SSO on external route
- Ceph OSD volume must be mounted at
/media/jeeves/1TB_Vol1before the container starts, otherwise the audio storage path is unavailable
Notes / Gotchas
- If the Ceph OSD volume is unmounted or degraded, audio file operations will fail even though the container itself runs normally.
APP_URLmust match the externally accessible URL. Changing this after initial setup requires updating any stored links.- The
BETTER_AUTH_SECRETandENCRYPTION_KEYmust remain constant — changing them invalidates all existing sessions and encrypted data.
Last Updated: 2026-06-16
Paperless-AI
Overview
PaperlessAI is an AI companion service for Paperless-NGX that automates document classification. It monitors the Paperless document inbox and uses a local Ollama model to automatically assign tags, document types, correspondents, and titles to newly ingested documents — eliminating the need for manual categorisation.
It communicates with Ollama over the internal ai-internal network and with
Paperless-NGX via its API over the external HTTPS URL.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://paperlessai.home.local |
Authentik SSO required |
| External | https://paperlessai.jeeves5454.ddns.net |
Authentik SSO + GeoBlock + CrowdSec |
Configuration
Image: clusterzx/paperless-ai:latest
Compose project: Paperless stack (managed via Portainer, separate from ai-stack)
Environment Variables
| Variable | Value / Notes |
|---|---|
LLM_PROVIDER |
ollama |
OLLAMA_URL |
http://ollama.home.local:11434 |
OLLAMA_MODEL |
gemma4:12b |
PAPERLESS_URL |
https://paperless.jeeves5454.ddns.net |
PAPERLESS_API_KEY |
REDACTED |
AUTO_MODE |
true — processes documents automatically |
AUTO_TAG |
ai-processed — applied to every AI-classified document |
NODE_ENV |
production |
AUTO_MODE=true means PaperlessAI polls the Paperless inbox on a schedule and
processes new documents without any manual trigger. The ai-processed tag is
applied to documents after classification, allowing easy filtering in Paperless.
Traefik Labels
# External route
traefik.http.routers.paperlessai-ext.rule: Host(`paperlessai.jeeves5454.ddns.net`)
traefik.http.routers.paperlessai-ext.entrypoints: websecure
traefik.http.routers.paperlessai-ext.tls.certresolver: letsencrypt
traefik.http.routers.paperlessai-ext.middlewares: plex-geoblock@file,crowdsec-bouncer@file,authentik-auth@docker
traefik.http.routers.paperlessai-ext.service: paperlessai-svc
# Internal route
traefik.http.routers.paperlessai-int.rule: Host(`paperlessai.home.local`)
traefik.http.routers.paperlessai-int.entrypoints: websecure
traefik.http.routers.paperlessai-int.tls.certresolver: step-ca
traefik.http.routers.paperlessai-int.middlewares: authentik-auth@docker
traefik.http.routers.paperlessai-int.service: paperlessai-svc
traefik.http.services.paperlessai-svc.loadbalancer.server.port: 3000
Note that Authentik middleware is applied on both internal and external routes.
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/paperless/paperless-ai-data |
/app/data |
PaperlessAI configuration and state |
Networks
PaperlessAI must be on the same network as Ollama to use http://ollama.home.local:11434.
It reaches Paperless-NGX via its external HTTPS URL (outbound through Traefik).
Dependencies
- Ollama — must be running with the
gemma4:12bmodel loaded for classification to work. Pull the model first:docker exec -it ollama ollama pull gemma4:12b - Paperless-NGX — must be accessible at
https://paperless.jeeves5454.ddns.netwith a valid API key - Authentik for SSO on both routes
Notes / Gotchas
- If Ollama is down or the model isn't loaded, documents will queue and not be processed until Ollama is available again.
- The
AUTO_TAG(ai-processed) helps track which documents were classified by AI vs manually. Review newly tagged documents periodically to validate AI accuracy. gemma4:12b(12 billion parameters) fits comfortably within the RTX 5080's 16 GB VRAM. If Ollama is under pressure from simultaneous inference requests, PaperlessAI classification may be slower.- PaperlessAI connects to Paperless via the external URL
(
https://paperless.jeeves5454.ddns.net). Ensure this domain remains reachable from within the container network.
Last Updated: 2026-06-16
n8n
Overview
N8N is the workflow automation platform for the homelab. It connects services together through visual, node-based workflows — handling tasks such as document pipeline automation, audio transcription orchestration (Minuspod), API integrations, scheduled jobs, and webhook-triggered processing.
N8N is the glue layer that ties together Whisper, Ollama, Paperless, and external APIs into end-to-end automated pipelines.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://n8n.home.local |
LAN access via Step-CA TLS — basic auth |
No external (internet-facing) Traefik route. Remote access via Tailscale.
Webhooks are received at https://n8n.home.local/webhook/... — internal and
Tailscale-reachable only.
Configuration
Image: n8nio/n8n:latest
Compose project: Standalone (managed via Portainer)
Ports
| Port | Protocol | Purpose |
|---|---|---|
5678 |
TCP | N8N web UI and API (host-bound) |
Traefik Labels
traefik.enable: "true"
traefik.docker.network: traefik-net
traefik.http.routers.n8n.rule: Host(`n8n.home.local`)
traefik.http.routers.n8n.entrypoints: websecure
traefik.http.routers.n8n.tls.certresolver: step-ca
traefik.http.services.n8n.loadbalancer.server.port: 5678
Internal-only route.
Environment Variables
| Variable | Value / Notes |
|---|---|
N8N_HOST |
n8n.home.local |
N8N_PROTOCOL |
https |
N8N_PORT |
5678 |
N8N_EDITOR_BASE_URL |
https://n8n.home.local |
WEBHOOK_URL |
https://n8n.home.local |
N8N_BASIC_AUTH_ACTIVE |
true |
N8N_BASIC_AUTH_USER |
jeeves |
N8N_BASIC_AUTH_PASSWORD |
REDACTED |
N8N_ENCRYPTION_KEY |
REDACTED — encrypts stored credentials |
N8N_PAYLOAD_SIZE_MAX |
16 (MB) |
EXECUTIONS_PROCESS |
main |
GENERIC_TIMEZONE |
America/Toronto |
N8N_ENCRYPTION_KEY encrypts all stored credentials (API keys, passwords) in the
N8N database. This key must remain constant — changing it invalidates all stored
credentials and they must be re-entered.
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/n8n/data |
/home/node/.n8n |
Workflows, credentials, execution history |
All workflow definitions, credentials, and execution logs are stored in this bind-mounted directory. Back up before upgrades.
Networks
| Network | Purpose |
|---|---|
traefik-net |
Exposes N8N UI and webhook endpoint |
N8N calls other services by hostname (e.g. http://ollama.home.local:11434,
https://whisper.home.local) — it must be on traefik-net or have DNS
resolution for these names.
Dependencies
- AdGuard Home for
*.home.localDNS resolution (N8N calls other services by name) - Step-CA / Traefik for the
n8n.home.localroute - Services called by workflows: Ollama, Faster-Whisper, Paperless-NGX, external APIs
Notes / Gotchas
- N8N stores all credentials encrypted with
N8N_ENCRYPTION_KEY. If this key is lost or rotated, all stored API keys and passwords must be re-entered manually. EXECUTIONS_PROCESS=mainruns workflow executions in the main process. For high concurrency, considerEXECUTIONS_PROCESS=ownwith worker processes, but this requires additional setup.- Webhook URLs are internal only (
https://n8n.home.local/webhook/...). External webhooks (e.g. from GitHub, Stripe) will not reach N8N unless a Tailscale exit node or Traefik external route is configured. N8N_PAYLOAD_SIZE_MAX=16limits incoming webhook payload size to 16 MB. Increase this if processing large audio or document payloads via webhook.- The
latesttag may introduce breaking changes between workflow schema versions. Pin to a specific N8N version before any significant workflow investments.
Last Updated: 2026-06-16
Open Notebook
Overview
Open Notebook is a self-hosted AI research notebook application — a local alternative to Google NotebookLM. It allows structured AI-assisted research sessions where sources (documents, URLs, text) are ingested into a notebook and queried via an LLM for synthesis, summaries, and question-answering.
It uses SurrealDB as its backend database for storing notebooks, sources, and conversation state.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://notebook.home.local |
LAN access via Step-CA TLS |
| External | https://notebook.jeeves5454.ddns.net |
Authentik SSO + GeoBlock + CrowdSec |
Containers in This Stack
| Container | Image | Role |
|---|---|---|
open-notebook |
lfnovo/open_notebook:v1-latest |
Application server (Streamlit) |
open-notebook-db |
surrealdb/surrealdb:v2 |
SurrealDB database backend |
Configuration
Compose project: ai-stack
Environment Variables
| Variable | Value / Notes |
|---|---|
API_URL |
https://notebook.home.local |
CORS_ORIGINS |
https://notebook.home.local,https://notebook.jeeves5454.ddns.net |
INTERNAL_API_URL |
http://localhost:5055 |
SURREAL_URL |
ws://open-notebook-db:8000/rpc |
SURREAL_NAMESPACE |
open_notebook |
SURREAL_DATABASE |
open_notebook |
SURREAL_USER |
root |
SURREAL_PASSWORD |
REDACTED |
HOSTNAME |
0.0.0.0 |
Traefik Labels
# External route
traefik.http.routers.open-notebook-ext.rule: Host(`notebook.jeeves5454.ddns.net`)
traefik.http.routers.open-notebook-ext.entrypoints: websecure
traefik.http.routers.open-notebook-ext.tls.certresolver: letsencrypt
traefik.http.routers.open-notebook-ext.middlewares: authentik-auth@docker,plex-geoblock@file,crowdsec-bouncer@file
traefik.http.routers.open-notebook-ext.service: open-notebook-ui-svc
traefik.http.routers.open-notebook-ext.priority: 1
# Internal route
traefik.http.routers.open-notebook-int.rule: Host(`notebook.home.local`)
traefik.http.routers.open-notebook-int.entrypoints: websecure
traefik.http.routers.open-notebook-int.tls.certresolver: step-ca
traefik.http.routers.open-notebook-int.service: open-notebook-ui-svc
traefik.http.routers.open-notebook-int.priority: 1
traefik.http.services.open-notebook-ui-svc.loadbalancer.server.port: 8502
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/media/jeeves/1TB_Vol1/docker/open-notebook/notebook_data |
/app/data |
Notebook content and uploads (Ceph OSD) |
/home/jeeves/docker/open-notebook/surreal_data |
/mydata |
SurrealDB data files |
Notebook data (uploaded sources, generated content) is stored on the Ceph OSD
volume (1TB_Vol1) to keep large research files off the system NVMe.
Sub-section: SurrealDB
open-notebook-db runs SurrealDB v2, a multi-model database used by Open Notebook
to store all notebook definitions, source metadata, embeddings, and conversation
history.
SurrealDB is connected to open-notebook via WebSocket at
ws://open-notebook-db:8000/rpc over the ai-internal network. It is not
exposed to traefik-net — no external access.
Networks
| Network | Purpose |
|---|---|
ai-stack_ai-internal |
open-notebook ↔ open-notebook-db communication |
traefik-net |
Exposes the Open Notebook UI via Traefik |
open-notebook-db is on ai-internal only and never on traefik-net.
Dependencies
open-notebook-db(SurrealDB must be running before the app starts)- Ollama or an external LLM API configured within the app settings
- Authentik for SSO on the external route
- Ceph OSD volume mounted at
/media/jeeves/1TB_Vol1
Notes / Gotchas
- The
v1-latestimage tag tracks the latest v1 stable release. Breaking changes between major versions may require a database migration. - SurrealDB stores its data in the bind-mounted
surreal_datadirectory. This must be backed up before upgrades to SurrealDB v3+ as schema changes are not automatically reversible. - LLM provider settings (which Ollama model to use, API keys for external providers) are configured inside the Open Notebook UI, not via environment variables.
CORS_ORIGINSmust include both the internal and external URLs — missing one will cause browser CORS errors for the corresponding route.
Last Updated: 2026-06-16
09-mcp-github.md
kstack: book: Centerpoint Home Lab chapter: AI & Automation page: MCP GitHub Server tags: [mcp, github, oauth2, ai, claude]
Overview
The MCP GitHub Server exposes GitHub's Model Context Protocol (MCP) server over HTTPS with OAuth 2.1 authentication, making it accessible to Claude.ai and other MCP-compatible AI clients from anywhere on the internet.
The stack consists of two containers:
mcp-github-proxy— a custom Node.js OAuth 2.1 proxy (locally built image) that validates JWTs from Authentik before forwarding requests to the MCP server.github-mcp-server— the official GitHub MCP server running in HTTP mode, internal-only, with access scoped to repos and issues.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://mcp-github.home.local |
LAN access via Step-CA TLS |
| External | https://mcp-github.jeeves5454.ddns.net |
OAuth 2.1 authenticated — no Authentik ForwardAuth |
The external route uses OAuth 2.1 (not Authentik ForwardAuth) as the auth layer. GeoBlock and CrowdSec are still applied. The OAuth 2.1 issuer is Authentik.
Containers in This Stack
| Container | Image | Role |
|---|---|---|
mcp-github-proxy |
mcp-github-proxy:latest (local build) |
OAuth 2.1 proxy + MCP request forwarder |
github-mcp-server |
ghcr.io/github/github-mcp-server:latest |
GitHub MCP server (HTTP mode, internal) |
Configuration
Compose project: mcp-github
Compose file: /home/jeeves/docker/mcp-github/proxy/docker-compose.yml
mcp-github-proxy Environment Variables
| Variable | Value / Notes |
|---|---|
PORT |
3000 |
GITHUB_MCP_URL |
http://github-mcp-server:8080 |
GITHUB_PERSONAL_ACCESS_TOKEN |
REDACTED — PAT with read-only scopes (Contents, Issues, Metadata) |
AUTHENTIK_ISSUER |
https://auth.jeevesconsults.ca/application/o/mcp-github/ |
AUTHENTIK_JWKS_URL |
https://auth.jeevesconsults.ca/application/o/mcp-github/jwks/ |
SERVER_URL |
https://mcp-github.jeeves5454.ddns.net |
github-mcp-server Environment Variables
| Variable | Value / Notes |
|---|---|
GITHUB_PERSONAL_ACCESS_TOKEN |
REDACTED — same read-only PAT as proxy |
GITHUB_TOOLSETS |
repos,issues — restricts available tools |
GITHUB_READ_ONLY |
1 — belt-and-suspenders read-only flag |
Security note:
GITHUB_READ_ONLY=1has a known bug in HTTP mode (github/github-mcp-server #2156) — toolset flags may not fully enforce read-only behaviour. Primary read-only enforcement is the PAT scopes themselves (Contents/Issues/Metadata: read only).
Traefik Labels
# External route — GeoBlock + CrowdSec only (OAuth 2.1 handles auth)
traefik.http.routers.mcp-github-ext.rule: Host(`mcp-github.jeeves5454.ddns.net`)
traefik.http.routers.mcp-github-ext.entrypoints: websecure
traefik.http.routers.mcp-github-ext.tls.certresolver: letsencrypt
traefik.http.routers.mcp-github-ext.middlewares: plex-geoblock@file,crowdsec-bouncer@file
traefik.http.routers.mcp-github-ext.service: mcp-github-proxy-svc
# Internal route
traefik.http.routers.mcp-github-int.rule: Host(`mcp-github.home.local`)
traefik.http.routers.mcp-github-int.entrypoints: websecure
traefik.http.routers.mcp-github-int.tls.certresolver: step-ca
traefik.http.routers.mcp-github-int.service: mcp-github-proxy-svc
traefik.http.services.mcp-github-proxy-svc.loadbalancer.server.port: 3000
Note: Authentik ForwardAuth (authentik-auth@docker) is not used here.
OAuth 2.1 is the authentication mechanism — the proxy validates the Authorization
header JWT against Authentik's JWKS endpoint directly.
Sub-section: GitHub MCP Server
github-mcp-server runs the official GitHub MCP server in HTTP mode on port 8080,
internal-only. It is never directly exposed to Traefik or the host — all requests
arrive through the mcp-github-proxy over the mcp-github-internal network.
The server is restricted to repos and issues toolsets, providing read-only
access to repository content, metadata, and issues. Write operations are not
available via the PAT scopes.
Proxy Image Build
The mcp-github-proxy image is built locally on Centerpoint before deployment:
cd /home/jeeves/docker/mcp-github/proxy
docker build -t mcp-github-proxy:latest .
Source files: /home/jeeves/docker/mcp-github/proxy/{Dockerfile,package.json,src/}
Volumes / Bind Mounts
No persistent volumes required. Both containers are stateless — the proxy holds no state, and the MCP server reads from GitHub's API on every request.
Networks
| Network | Purpose |
|---|---|
traefik-net |
Exposes mcp-github-proxy via Traefik |
mcp-github_mcp-github-internal |
mcp-github-proxy ↔ github-mcp-server communication |
github-mcp-server is on the internal network only — never on traefik-net.
Healthchecks
Both containers have healthchecks defined:
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:<port>/health"]
interval: 30s
timeout: 5s
retries: 3
- Proxy healthcheck:
http://localhost:3000/health - MCP server healthcheck:
http://localhost:8080/health
Authentik OAuth 2.1 Provider Setup
In Authentik, an OAuth2/OIDC provider is configured for MCP GitHub with:
| Setting | Value |
|---|---|
| Redirect URI | https://claude.ai/api/mcp/auth_callback |
| Issuer | https://auth.jeevesconsults.ca/application/o/mcp-github/ |
| JWKS URL | <issuer>/jwks/ |
Dependencies
- Authentik (
auth.jeevesconsults.ca) — proxy validates JWTs against Authentik JWKS - GitHub API (internet access required for the MCP server to function)
- Traefik on
traefik-netfor both routes
Notes / Gotchas
- The GitHub PAT must be rotated before expiry. If it expires, all GitHub MCP tool calls will fail with 401 errors. Update in both container environment variables.
- The proxy image must be rebuilt after any source code changes:
cd /home/jeeves/docker/mcp-github/proxy docker build -t mcp-github-proxy:latest . docker compose up -d --force-recreate mcp-github-proxy - Claude.ai connects to this server via the external URL. The redirect URI
(
https://claude.ai/api/mcp/auth_callback) must be registered in the Authentik OAuth2 provider — adding any other redirect URI will cause the OAuth flow to fail. - GeoBlock allows CA, US, and IN. Claude.ai's servers may originate from other regions — if MCP calls fail, check Traefik logs for GeoBlock rejections and adjust the country allowlist accordingly.
Last Updated: 2026-06-16
h04-media-entertainment
00-chapter-intro.md
kstack: book: Centerpoint Home Lab chapter: Media & Entertainment page: Chapter Introduction tags: [media, plex, jellyfin, immich, audiobookshelf, stash]
Overview
This chapter covers all media consumption and photo management services on
Centerpoint. Media files themselves are not stored locally — they live on the
UnRAID NAS (192.168.1.119) and are bind-mounted into containers from the
NFS shares at /mnt/Multimedia and /mnt/Photos.
Shared Storage
| NFS Mount | Contents | Consumers |
|---|---|---|
/mnt/Multimedia |
Movies, TV, adult content, audiobooks, podcasts | Plex, Jellyfin, Emby, Audiobookshelf, Stash |
/mnt/Photos |
Photo library, Google Photos imports | Immich |
Services in This Chapter
| Service | Container(s) | Status | Purpose |
|---|---|---|---|
| Plex | plex |
Active | Primary media server |
| Jellyfin | jellyfin |
Active | Open-source media server (secondary) |
| Emby | emby |
Offline | Legacy media server — not running |
| Tautulli | tautulli |
Active | Plex analytics and monitoring |
| Immich | immich_server, immich_machine_learning, immich_postgres, immich_redis |
Active | Photo & video library |
| Audiobookshelf | audiobookshelf |
Active | Audiobooks and podcasts |
| Seerr | seerr |
Active | Media request management |
| Stash | stash, stash-vr |
Active | Adult media library + VR frontend |
| Threadfin | threadfin |
Active | IPTV M3U proxy for Plex/Jellyfin |
Last Updated: 2026-06-16
01-plex.md
kstack: book: Centerpoint Home Lab chapter: Media & Entertainment page: Plex tags: [plex, media, streaming]
Overview
Plex Media Server is the primary media server for the homelab, serving movies, TV shows, and other content to Plex clients on the local network and remotely. It uses Plex's own account-based authentication — no Authentik ForwardAuth is applied. GeoBlock is active on the external route.
A second Plex instance running on another device (192.168.1.186) is routed via
Traefik's file provider as plex2.jeeves5454.ddns.net.
Access
| Type | URL | Notes |
|---|---|---|
| External | https://plex.jeeves5454.ddns.net |
GeoBlock (CA/US/IN), no Authentik — Plex account auth |
| Direct | http://192.168.1.85:32400 |
LAN direct access |
No *.home.local internal Traefik route — Plex is accessed externally or by direct
IP on the LAN.
Configuration
Image: lscr.io/linuxserver/plex:latest
Compose project: Standalone (managed via Portainer)
Ports
| Port | Protocol | Purpose |
|---|---|---|
32400 |
TCP | Plex Media Server API and web UI |
Traefik Labels
traefik.enable: "true"
traefik.http.routers.plex.rule: Host(`plex.jeeves5454.ddns.net`)
traefik.http.routers.plex.entrypoints: websecure
traefik.http.routers.plex.tls.certresolver: letsencrypt
traefik.http.routers.plex.middlewares: plex-geoblock@file,plex-headers
traefik.http.middlewares.plex-headers.headers.customrequestheaders.X-Forwarded-Proto: https
traefik.http.services.plex.loadbalancer.server.port: 32400
The plex-headers middleware injects X-Forwarded-Proto: https — required for
Plex to generate correct redirect and callback URLs when behind a reverse proxy.
Environment Variables
| Variable | Value | Purpose |
|---|---|---|
PUID |
1000 |
Run as user ID 1000 |
PGID |
1000 |
Run as group ID 1000 |
TZ |
America/Toronto |
Timezone |
VERSION |
docker |
Use the latest Plex from Docker Hub |
ADVERTISE_IP |
https://plex.jeeves5454.ddns.net:443 |
External URL Plex advertises to clients |
ADVERTISE_IP must match the externally reachable URL for remote streaming to work
correctly when behind Traefik.
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/plex/config |
/config |
Plex database, metadata, settings |
/mnt/Multimedia |
/Multimedia |
All media files (NFS from UnRAID) |
/home/jeeves/docker |
/docker |
Utility bind (admin access) |
Networks
| Network | Purpose |
|---|---|
traefik-net |
Exposes Plex via Traefik external route |
Dependencies
- NFS mount
/mnt/Multimediamust be healthy — if UnRAID is down, Plex will show libraries as unavailable - Internet connectivity for metadata scraping and Plex account authentication
Notes / Gotchas
- Plex uses its own authentication (Plex.tv account). Authentik ForwardAuth is not applied to the external route because it would break the Plex app auth flow.
VERSION=dockeralways pulls the latest Plex release on container start — pin to a specific build number (e.g.VERSION=1.41.x.xxxx-xxxxxxxxx) for stability.- Plex transcoding temp files are written to
/tmpinside the container. For heavy transcoding workloads, consider adding a bind mount for the transcode directory to a fast local path. - Tautulli connects to Plex via the direct API at
http://plex:32400over thetraefik-netbridge.
Last Updated: 2026-06-16
02-jellyfin.md
kstack: book: Centerpoint Home Lab chapter: Media & Entertainment page: Jellyfin tags: [jellyfin, media, streaming, open-source]
Overview
Jellyfin is a free and open-source media server used as a secondary streaming solution alongside Plex. It serves TV shows and movies from the same UnRAID NFS share and is accessible on the LAN only.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://jellyfin.home.local |
LAN access via Step-CA TLS |
| Direct | http://192.168.1.85:8096 |
LAN direct access |
No external (internet-facing) route.
Configuration
Image: jellyfin/jellyfin:latest
Compose project: Standalone (managed via Portainer)
Ports
| Port | Protocol | Purpose |
|---|---|---|
8096 |
TCP | Jellyfin web UI and API (host-bound) |
Traefik Labels
traefik.enable: "true"
traefik.http.routers.jellyfin.rule: Host(`jellyfin.home.local`)
traefik.http.routers.jellyfin.entrypoints: websecure
traefik.http.routers.jellyfin.tls: "true"
traefik.http.routers.jellyfin.tls.certresolver: step-ca
traefik.http.services.jellyfin.loadbalancer.server.port: 8096
Environment Variables
| Variable | Value | Purpose |
|---|---|---|
PUID |
1000 |
User ID |
PGID |
1000 |
Group ID |
TZ |
America/Toronto |
Timezone |
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/jellyfin/config |
/config |
Jellyfin database and settings |
/home/jeeves/docker/jellyfin/cache |
/cache |
Thumbnail and transcode cache |
/mnt/Multimedia/TV |
/media/tv:ro |
TV series (NFS from UnRAID, read-only) |
/mnt/Multimedia/Movies |
/media/movies:ro |
Movies (NFS from UnRAID, read-only) |
Networks
| Network | Purpose |
|---|---|
traefik-net |
Exposes Jellyfin via Traefik |
Dependencies
- NFS mount
/mnt/Multimediamust be healthy - Step-CA for
jellyfin.home.localTLS certificate
Notes / Gotchas
- Media mounts are read-only (
:ro) — Jellyfin cannot modify or delete source files. - Jellyfin and Plex both mount the same NFS directories. Metadata and library scans are independent between the two servers.
- Hardware transcoding is available on Jellyfin but not explicitly configured — the
jellyfin/jellyfin:latestimage supports it via FFmpeg with Intel/NVIDIA hooks. Configure in Admin → Dashboard → Playback if needed. - Port
8096is also used by Emby (currently offline). Only one can be host-bound at a time — ensure Emby is stopped before starting Jellyfin and vice versa.
Last Updated: 2026-06-16
03-emby.md
kstack: book: Centerpoint Home Lab chapter: Media & Entertainment page: Emby (Offline) tags: [emby, media, offline]
Overview
Status: OFFLINE — The Emby container is present on Centerpoint but is not currently running. It has been superseded by Jellyfin for open-source media serving needs. The container and its configuration are retained.
Emby was previously used as an alternative media server. Its configuration and
data remain intact and the container can be restarted if needed, subject to the
port 8096 conflict with Jellyfin (see Notes).
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://emby.home.local |
Traefik labels present — not routed while offline |
Configuration
Image: emby/embyserver:latest
Status: exited (container stopped)
Traefik Labels (inactive while container is stopped)
traefik.enable: "true"
traefik.http.routers.emby.rule: Host(`emby.home.local`)
traefik.http.routers.emby.entrypoints: websecure
traefik.http.routers.emby.tls.certresolver: step-ca
traefik.http.services.emby.loadbalancer.server.port: 8096
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/emby/config |
/config |
Emby database and library config |
/mnt/Multimedia/ST |
/data |
Media files (same NFS as Stash) |
Notes / Gotchas
- Emby and Jellyfin both use port
8096internally. Only one can be host-bound at a time — starting Emby while Jellyfin is running will fail with a port conflict. - If reactivating Emby, stop Jellyfin first or remap Emby to a different host port.
- The
emby/embyserver:latestimage tag pulls the latest release — verify compatibility with the existing config before restarting after a long pause.
Last Updated: 2026-06-16
04-tautulli.md
kstack: book: Centerpoint Home Lab chapter: Media & Entertainment page: Tautulli tags: [tautulli, plex, monitoring, analytics]
Overview
Tautulli is the monitoring and analytics companion for Plex Media Server. It tracks play history, user activity, and library statistics, and can send notifications (email, Telegram, etc.) on media events such as new content additions, playback starts, or user logins.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://tautulli.home.local |
LAN access via Step-CA TLS |
No external route — LAN and Tailscale access only.
Configuration
Image: lscr.io/linuxserver/tautulli:latest
Compose project: Standalone (managed via Portainer)
Traefik Labels
traefik.enable: "true"
traefik.http.routers.tautulli.rule: Host(`tautulli.home.local`)
traefik.http.routers.tautulli.entrypoints: websecure
traefik.http.routers.tautulli.tls.certresolver: step-ca
traefik.http.services.tautulli.loadbalancer.server.port: 8181
Environment Variables
| Variable | Value | Purpose |
|---|---|---|
PUID |
1000 |
User ID |
PGID |
1000 |
Group ID |
TZ |
America/Toronto |
Timezone |
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/Tautulli/local_tautulli |
/config |
Tautulli database and configuration |
Networks
| Network | Purpose |
|---|---|
traefik-net |
Exposes Tautulli UI; also reaches Plex container |
Dependencies
- Plex Media Server — Tautulli connects to the Plex API to retrieve playback data.
Configure the Plex connection in Tautulli Settings → Plex Media Server using the
container hostname
plexand port32400.
Notes / Gotchas
- Tautulli's database (
tautulli.db) contains full play history. Back it up before upgrades. - Notification agents (Telegram, email, etc.) and their credentials are stored in
the Tautulli database. If the
/configbind mount is lost, all notification integrations must be reconfigured.
Last Updated: 2026-06-16
05-immich.md
kstack: book: Centerpoint Home Lab chapter: Media & Entertainment page: Immich tags: [immich, photos, media, cuda, gpu, self-hosted]
Overview
Immich is the self-hosted photo and video library for the homelab — a local alternative to Google Photos. It provides automatic mobile backup, face recognition, smart search, and album management. The machine learning container runs on the RTX 5080 via CUDA for accelerated facial recognition and CLIP-based smart search.
Photo storage lives on the UnRAID NAS NFS mount at /mnt/Photos.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://photos.home.local |
LAN access via Step-CA TLS |
| External | https://photos.jeevesconsults.ca |
GeoBlock (CA/US/IN) + CrowdSec — no Authentik ForwardAuth |
Immich uses its own user authentication — Authentik ForwardAuth is not applied because it would break the mobile app OAuth flow.
Containers in This Stack
| Container | Image | GPU | Role |
|---|---|---|---|
immich_server |
ghcr.io/immich-app/immich-server:release |
No | Main API and web server |
immich_machine_learning |
ghcr.io/immich-app/immich-machine-learning:release-cuda |
Yes | Face recognition + CLIP search |
immich_postgres |
ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 |
No | PostgreSQL with pgvecto.rs |
immich_redis |
valkey/valkey:8-bookworm |
No | Job queue and cache |
Configuration
Compose file: /home/jeeves/docker/immich/docker-compose.yml
Compose project: immich
Ports
| Port | Protocol | Purpose |
|---|---|---|
2283 |
TCP | Immich web UI and API (host-bound) |
Traefik Labels
# External route
traefik.http.routers.immich.rule: Host(`photos.jeevesconsults.ca`)
traefik.http.routers.immich.entrypoints: websecure
traefik.http.routers.immich.tls.certresolver: letsencrypt
traefik.http.routers.immich.middlewares: plex-geoblock@file,crowdsec-bouncer@file,immich-headers
traefik.http.middlewares.immich-headers.headers.customrequestheaders.X-Forwarded-Proto: https
# Internal route
traefik.http.routers.immich-internal.rule: Host(`photos.home.local`)
traefik.http.routers.immich-internal.entrypoints: websecure
traefik.http.routers.immich-internal.tls.certresolver: step-ca
traefik.http.services.immich.loadbalancer.server.port: 2283
Key Environment Variables (.env file)
| Variable | Value / Notes |
|---|---|
UPLOAD_LOCATION |
/mnt/Photos/immich-library |
DB_HOSTNAME |
immich_postgres |
DB_USERNAME |
postgres |
DB_PASSWORD |
REDACTED |
DB_DATABASE_NAME |
immich |
REDIS_HOSTNAME |
immich_redis |
TZ |
America/Toronto |
IMMICH_VERSION |
release (pinned to latest stable) |
Volumes / Bind Mounts
| Host Path / Volume | Container Path | Purpose |
|---|---|---|
/mnt/Photos/immich-library |
/data |
Primary upload library (NFS) |
/mnt/Photos/Plex |
/mnt/Photos/Plex:rw |
Plex photo library (external library) |
/mnt/Photos/Google_Photos |
/mnt/Photos/Google_Photos:rw |
Google Photos import folder |
/etc/localtime |
/etc/localtime:ro |
Host timezone sync |
model-cache (named volume) |
/cache |
ML model weight cache (machine learning container) |
${DB_DATA_LOCATION} (from .env) |
/var/lib/postgresql/data |
PostgreSQL data |
Sub-section: Machine Learning (CUDA)
immich_machine_learning runs with runtime: nvidia, giving it access to the RTX
5080 for:
- Face detection and recognition — identifies and clusters faces across the library
- CLIP embeddings — powers smart search ("photos of dogs at the beach")
| Environment Variable | Value | Purpose |
|---|---|---|
NVIDIA_VISIBLE_DEVICES |
all |
GPU access |
NVIDIA_DRIVER_CAPABILITIES |
compute,utility |
CUDA compute caps |
MACHINE_LEARNING_DEVICE_ID |
0 |
Use GPU device 0 |
ML model files are cached in the model-cache named Docker volume. Models are
downloaded from HuggingFace on first use and cached for subsequent runs.
Sub-section: PostgreSQL (pgvecto.rs)
Immich uses a custom PostgreSQL 14 image with the pgvecto.rs and pgvectors
extensions pre-installed. These extensions power the vector similarity search that
underlies CLIP smart search and face clustering.
ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
The shm_size: 128mb allocation is required for PostgreSQL's shared memory.
Sub-section: Redis / Valkey
immich_redis uses Valkey (the Redis fork) as the job queue and cache backend.
It handles background job scheduling for ML processing, thumbnail generation, and
library scans.
Dependencies
- NFS mounts
/mnt/Photos(UnRAID) must be healthy immich_postgres→immich_redis→immich_serverstartup order- NVIDIA container runtime for
immich_machine_learning
Notes / Gotchas
- Immich does not use Authentik ForwardAuth — it has built-in multi-user auth.
The mobile app connects directly to
https://photos.jeevesconsults.ca. - The
X-Forwarded-Proto: httpsheader (immich-headersmiddleware) is required for Immich to generate correct share links and OAuth callbacks. - The PostgreSQL image is pinned to a specific digest — do not change it arbitrarily. Immich releases are tied to specific database schema versions.
- Library scan and ML job processing can be CPU/GPU intensive. Schedule large library scans during off-peak hours via Admin → Jobs.
- External libraries (Plex photos, Google Photos) are read-only imports. Changes to files in these directories are picked up on the next library scan.
Last Updated: 2026-06-16
06-audiobookshelf.md
kstack: book: Centerpoint Home Lab chapter: Media & Entertainment page: Audiobookshelf tags: [audiobookshelf, audiobooks, podcasts, media]
Overview
Audiobookshelf is a self-hosted audiobook and podcast server. It manages and streams audiobook and podcast content from the UnRAID NFS share, tracks listening progress across devices, and supports mobile apps for on-the-go listening.
A notable configuration detail: the Step-CA root certificate is injected into the
container's trust store so that Audiobookshelf can make HTTPS calls to internal
*.home.local services (e.g. for metadata lookups or integrations).
Access
| Type | URL | Notes |
|---|---|---|
| External | https://audio.jeeves5454.ddns.net |
GeoBlock (CA/US/IN) + CrowdSec — no Authentik |
No *.home.local internal Traefik route — accessible externally or via direct LAN
IP. Audiobookshelf uses its own account-based authentication.
Configuration
Image: ghcr.io/advplyr/audiobookshelf:latest
Compose project: Standalone (managed via Portainer)
Traefik Labels
traefik.enable: "true"
traefik.http.routers.audio.rule: Host(`audio.jeeves5454.ddns.net`)
traefik.http.routers.audio.entrypoints: websecure
traefik.http.routers.audio.tls.certresolver: letsencrypt
traefik.http.routers.audio.middlewares: plex-geoblock@file,crowdsec-bouncer@file,audio-headers
traefik.http.middlewares.audio-headers.headers.customrequestheaders.X-Forwarded-Proto: https
traefik.http.services.audio.loadbalancer.server.port: 80
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/audiobookshelf/config |
/config |
App database and settings |
/home/jeeves/docker/audiobookshelf/metadata |
/metadata |
Cover art and cached metadata |
/mnt/Multimedia/Audio/Audio_Books |
/audiobooks |
Audiobook files (NFS from UnRAID) |
/mnt/Multimedia/Audio/podcasts |
/podcasts |
Podcast episode files (NFS from UnRAID) |
/home/jeeves/docker/step-ca/config/certs/root_ca.crt |
/usr/local/share/ca-certificates/step-ca.crt:ro |
Step-CA root cert trust injection |
The Step-CA root certificate is bind-mounted into the container's CA trust directory,
allowing Audiobookshelf to trust *.home.local TLS certificates when making outbound
HTTPS requests to internal services.
Networks
| Network | Purpose |
|---|---|
traefik-net |
Exposes the Audiobookshelf UI |
Dependencies
- NFS mount
/mnt/Multimedia/Audiomust be healthy for content to be accessible - Step-CA root certificate at
/home/jeeves/docker/step-ca/config/certs/root_ca.crtmust exist for the bind mount to succeed
Notes / Gotchas
- Audiobookshelf has its own user authentication — no Authentik ForwardAuth is applied. Manage users in the Admin panel.
- The
X-Forwarded-Proto: httpsmiddleware is required for Audiobookshelf to generate correct callback and share URLs when behind Traefik. - Podcast episode downloads are stored in
/mnt/Multimedia/Audio/podcasts. Ensure UnRAID has sufficient free space for automatic podcast downloads. - If Step-CA root cert is updated (e.g. after CA renewal), the container must be restarted to pick up the new cert from the bind mount.
- Listening progress is stored in the SQLite database under
/config. Back up this directory before upgrades to preserve progress data.
Last Updated: 2026-06-16
07-seerr.md
kstack: book: Centerpoint Home Lab chapter: Media & Entertainment page: Seerr tags: [seerr, overseerr, media-requests, plex, jellyfin]
Overview
Seerr is a media request management application — a maintained fork of Overseerr. It provides a user-friendly interface for requesting movies and TV shows, which are then forwarded to the *arr stack (Radarr, Sonarr) for automated download and delivery to Plex and Jellyfin. It also surfaces Plex availability status so users can see what is already in the library before requesting.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://seerr.home.local |
LAN access via Step-CA TLS |
| External | https://seerr.jeeves5454.ddns.net |
Authentik SSO + GeoBlock + CrowdSec |
Configuration
Image: ghcr.io/seerr-team/seerr:latest
Compose project: arr stack (managed via Portainer alongside the *arr services)
Traefik Labels
# External route
traefik.http.routers.seerr-external.rule: Host(`seerr.jeeves5454.ddns.net`)
traefik.http.routers.seerr-external.entrypoints: websecure
traefik.http.routers.seerr-external.tls.certresolver: letsencrypt
traefik.http.routers.seerr-external.middlewares: authentik-auth@docker,plex-geoblock@file,crowdsec-bouncer@file
traefik.http.routers.seerr-external.service: seerr-svc
# Internal route
traefik.http.routers.seerr-internal.rule: Host(`seerr.home.local`)
traefik.http.routers.seerr-internal.entrypoints: websecure
traefik.http.routers.seerr-internal.tls.certresolver: step-ca
traefik.http.routers.seerr-internal.service: seerr-svc
traefik.http.services.seerr-svc.loadbalancer.server.port: 5055
Environment Variables
| Variable | Value | Purpose |
|---|---|---|
PORT |
5055 |
Application port |
LOG_LEVEL |
debug |
Logging verbosity |
TZ |
America/Toronto |
Timezone |
NODE_ENV |
production |
Runtime environment |
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/arr/Seer/config |
/app/config |
Seerr database and configuration |
Networks
| Network | Purpose |
|---|---|
traefik-net |
Exposes Seerr UI |
media-network |
Internal network shared with *arr stack services (Sonarr, Radarr, etc.) |
The media-network attachment allows Seerr to communicate directly with Sonarr,
Radarr, and Plex by container hostname without going through Traefik.
Dependencies
- Plex Media Server — for library availability checks (connect via API)
- Sonarr, Radarr — to forward TV and movie requests
- Authentik for SSO on the external route
Notes / Gotchas
- Seerr is a fork of Overseerr — configuration and API are compatible but issue
tracking and releases are at
github.com/seerr-team/seerr. - User permissions and notification settings are configured inside the Seerr UI. When Authentik is used for ForwardAuth, Seerr should be configured to use "Sign-in with Plex" or local accounts for user identity — not OIDC directly.
- Seerr's config database at
/app/configcontains API keys for Plex, Sonarr, and Radarr. Back it up before upgrades.
Last Updated: 2026-06-16
09-threadfin.md
kstack: book: Centerpoint Home Lab chapter: Media & Entertainment page: Threadfin tags: [threadfin, iptv, m3u, xmltv, plex, jellyfin]
Overview
Threadfin is an M3U proxy and IPTV middleware that translates IPTV streams into a format consumable by Plex DVR and Jellyfin Live TV. It manages M3U playlists, XMLTV guide data, and presents a virtual tuner device to media servers via HDHR (HDHomeRun) emulation.
Access
| Type | URL / Endpoint | Notes |
|---|---|---|
| Web UI | http://192.168.1.85:34400 |
Direct LAN access — no Traefik route |
| HDHR | http://192.168.1.85:34400 |
HDHomeRun device emulation endpoint |
No Traefik route is configured for Threadfin — Plex and Jellyfin connect to it via direct IP and port on the LAN.
Configuration
Image: fyb3roptik/threadfin:latest
Compose project: Standalone (managed via Portainer)
Ports
| Port | Protocol | Purpose |
|---|---|---|
34400 |
TCP | Threadfin web UI and HDHR endpoint (host-bound) |
Environment Variables
| Variable | Value | Purpose |
|---|---|---|
TZ |
America/Toronto |
Timezone |
THREADFIN_PORT |
34400 |
Listen port |
THREADFIN_BRANCH |
main |
Update channel |
THREADFIN_BIND_IP_ADDRESS |
0.0.0.0 |
Bind to all interfaces |
THREADFIN_DEBUG |
0 |
Debug logging off |
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/threadfin/config |
/home/threadfin/conf |
M3U playlists, XMLTV data, settings |
Networks
| Network | Purpose |
|---|---|
traefik-net |
Present (container is on traefik-net but no labels) |
Threadfin is on traefik-net for network reachability but has no Traefik labels —
it is accessed by direct IP.
Plex / Jellyfin Integration
Plex:
- In Plex Settings → Live TV & DVR → Set Up Plex Tuner
- Enter the Threadfin HDHR URL:
http://192.168.1.85:34400/device.xml - Plex discovers the virtual tuner and available channels
Jellyfin:
- Admin Dashboard → Live TV → Add Tuner Device
- Select HDHomeRun and enter
http://192.168.1.85:34400
Dependencies
- M3U source URL (IPTV provider) configured in Threadfin UI
- XMLTV guide source configured in Threadfin UI
- Plex or Jellyfin must be able to reach
192.168.1.85:34400on the LAN
Notes / Gotchas
- M3U playlists and XMLTV guide data are refreshed on a schedule configured in the Threadfin UI. If the IPTV provider URL changes, update it in Threadfin settings — Plex and Jellyfin do not need to be reconfigured.
- Threadfin does not store or proxy video streams itself; it redirects the media server directly to the IPTV stream URL. Buffering and quality are determined by the stream source.
- Port
34400is non-standard. Ensure no firewall rules block LAN-to-LAN traffic on this port between Plex/Jellyfin and Centerpoint.
Last Updated: 2026-06-16
h05-media-management
00-chapter-intro.md
kstack: book: Centerpoint Home Lab chapter: Media Management page: Chapter Introduction tags: [media-management, arr, sonarr, radarr, prowlarr, nzbget]
Overview
This chapter documents the automated media acquisition and management stack — commonly referred to as the *arr ecosystem. These services work together to monitor, search, download, rename, and organise media content automatically, then deliver it to Plex and Jellyfin.
Download Pipeline
Seerr (request) ──► Sonarr / Radarr / Whisparr / LazyLibrarian
│
▼
Prowlarr (indexer search)
│
▼
NZBGet (Usenet download)
│
▼
/media/jeeves/1TB_Vol2/downloads (Ceph OSD — temp)
│
Post-processing
│
▼
/mnt/Multimedia (NFS → UnRAID — permanent)
│
Plex / Jellyfin
Downloads land temporarily on the Ceph OSD volume (nvme2n1, mounted at
/media/jeeves/1TB_Vol2) for high-speed write performance. After post-processing,
completed media is moved/hardlinked to the UnRAID NFS share at /mnt/Multimedia
for permanent storage.
Shared Networks
All services in this chapter are on two Docker networks:
| Network | Purpose |
|---|---|
traefik-net |
Web UI access via Traefik |
media-network |
Internal service-to-service communication (no Traefik hop) |
media-network allows *arr services to reach each other and NZBGet directly by
container hostname without going through Traefik.
Services in This Chapter
| Service | Container | Status | Purpose |
|---|---|---|---|
| Sonarr | sonarr |
Active | TV show monitoring and management |
| Radarr | radarr |
Active | Movie monitoring and management |
| Prowlarr | prowlarr |
Active | Indexer aggregator for all *arr apps |
| Bazarr | bazarr |
Active | Subtitle management |
| NZBGet | nzbget |
Active | Usenet download client |
| Whisparr | whisparr |
Active | Adult content management (Sonarr fork) |
| LazyLibrarian | lazylibrarian |
Active | Book and magazine management |
| Mylar3 | mylar3 |
Active | Comics management |
| Audiobookrequest | audiobookrequest |
Active | Audiobook request portal |
| Profilarr | profilarr |
Active | Quality profile manager for *arr apps |
| Dispatcharr | dispatcharr |
Active | IPTV channel and stream dispatcher |
| Pulsarr | pulsarr |
Active | Watchlist automation and notifications |
| FlareSolverr | flaresolverr |
Offline | Cloudflare bypass proxy for Prowlarr |
Last Updated: 2026-06-16
01-sonarr.md
kstack: book: Centerpoint Home Lab chapter: Media Management page: Sonarr tags: [sonarr, tv, arr, media-management]
Overview
Sonarr monitors RSS feeds and indexers (via Prowlarr) for new TV show episodes, automatically searches for and downloads them via NZBGet, then renames and organises the files into the media library on UnRAID.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://sonarr.home.local |
LAN access via Step-CA TLS |
| External | https://sonarr.jeeves5454.ddns.net |
Authentik SSO + GeoBlock + CrowdSec |
Configuration
Image: lscr.io/linuxserver/sonarr:latest
Compose project: arr stack (managed via Portainer)
Traefik Labels
# External route
traefik.http.routers.sonarr-external.rule: Host(`sonarr.jeeves5454.ddns.net`)
traefik.http.routers.sonarr-external.entrypoints: websecure
traefik.http.routers.sonarr-external.tls.certresolver: letsencrypt
traefik.http.routers.sonarr-external.middlewares: authentik-auth@docker,plex-geoblock@file,crowdsec-bouncer@file
traefik.http.routers.sonarr-external.service: sonarr-svc
# Internal route
traefik.http.routers.sonarr-internal.rule: Host(`sonarr.home.local`)
traefik.http.routers.sonarr-internal.entrypoints: websecure
traefik.http.routers.sonarr-internal.tls.certresolver: step-ca
traefik.http.routers.sonarr-internal.service: sonarr-svc
traefik.http.services.sonarr-svc.loadbalancer.server.port: 8989
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/arr/Sonarr/config |
/config |
Sonarr database, settings, and series index |
/media/jeeves/1TB_Vol2/downloads |
/downloads |
NZBGet download staging (Ceph OSD) |
/mnt/Multimedia |
/media |
Final media library (NFS from UnRAID) |
Both /downloads and /media must be in the same container path namespace so
Sonarr can hardlink completed downloads rather than copy them — essential for
atomic moves between the download staging area and the media library.
Integration Points
| Service | Connection Method | Purpose |
|---|---|---|
| Prowlarr | API (http://prowlarr:9696) via media-network |
Indexer search |
| NZBGet | API (http://nzbget:6789) via media-network |
Download client |
| Bazarr | API (Bazarr polls Sonarr) | Subtitle fetching |
| Pulsarr | Sonarr webhook / API | Watchlist sync |
| Seerr | API | Receives show requests |
Notes / Gotchas
- Sonarr's root folder for TV series must point to
/media/TV(or equivalent) inside the container, which maps to/mnt/Multimedia/TVon the host via NFS. - Quality profiles are managed centrally via Profilarr and pushed to Sonarr via its API — do not manually edit quality profiles in Sonarr if Profilarr is active.
- API key is stored in
/config/config.xml— required to configure Prowlarr, Bazarr, Seerr, and Pulsarr integrations.
Last Updated: 2026-06-16
02-radarr.md
kstack: book: Centerpoint Home Lab chapter: Media Management page: Radarr tags: [radarr, movies, arr, media-management]
Overview
Radarr is the movie counterpart to Sonarr. It monitors for new and existing movie releases, searches indexers via Prowlarr, downloads via NZBGet, and moves completed movies into the media library on UnRAID.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://radarr.home.local |
LAN access via Step-CA TLS |
| External | https://radarr.jeeves5454.ddns.net |
Authentik SSO + GeoBlock + CrowdSec |
Configuration
Image: lscr.io/linuxserver/radarr:latest
Compose project: arr stack
Traefik Labels
# External route
traefik.http.routers.radarr-external.rule: Host(`radarr.jeeves5454.ddns.net`)
traefik.http.routers.radarr-external.entrypoints: websecure
traefik.http.routers.radarr-external.tls.certresolver: letsencrypt
traefik.http.routers.radarr-external.middlewares: authentik-auth@docker,plex-geoblock@file,crowdsec-bouncer@file
traefik.http.routers.radarr-external.service: radarr-svc
# Internal route
traefik.http.routers.radarr-internal.rule: Host(`radarr.home.local`)
traefik.http.routers.radarr-internal.entrypoints: websecure
traefik.http.routers.radarr-internal.tls.certresolver: step-ca
traefik.http.routers.radarr-internal.service: radarr-svc
traefik.http.services.radarr-svc.loadbalancer.server.port: 7878
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/arr/Radarr/config |
/config |
Radarr database and settings |
/media/jeeves/1TB_Vol2/downloads |
/downloads |
NZBGet download staging (Ceph OSD) |
/mnt/Multimedia |
/media |
Final media library (NFS from UnRAID) |
Integration Points
| Service | Connection Method | Purpose |
|---|---|---|
| Prowlarr | API (http://prowlarr:9696) via media-network |
Indexer search |
| NZBGet | API (http://nzbget:6789) via media-network |
Download client |
| Bazarr | API (Bazarr polls Radarr) | Subtitle fetching |
| Pulsarr | Radarr webhook / API | Watchlist sync |
| Seerr | API | Receives movie requests |
Notes / Gotchas
- Radarr root folder must be set to the path inside the container that maps to
/mnt/Multimedia/Movieson the host. - Hardlinking between
/downloadsand/mediarequires both paths to be on the same filesystem. The current setup has downloads on Ceph OSD and the final library on NFS — hardlinking is not possible across these. Radarr performs a copy+delete instead. This is slower but correct. - Quality profiles are managed via Profilarr — avoid manual edits in Radarr UI.
Last Updated: 2026-06-16
03-prowlarr.md
kstack: book: Centerpoint Home Lab chapter: Media Management page: Prowlarr tags: [prowlarr, indexer, arr, media-management]
Overview
Prowlarr is the indexer aggregation layer for the *arr stack. It manages all Usenet and torrent indexer connections in one place and syncs them automatically to Sonarr, Radarr, Whisparr, and LazyLibrarian — eliminating the need to configure indexers individually in each application.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://prowlarr.home.local |
LAN access via Step-CA TLS |
No external route — internal management only.
Configuration
Image: lscr.io/linuxserver/prowlarr:latest
Compose project: arr stack
Traefik Labels
traefik.enable: "true"
traefik.http.routers.prowlarr.rule: Host(`prowlarr.home.local`)
traefik.http.routers.prowlarr.entrypoints: websecure
traefik.http.routers.prowlarr.tls.certresolver: step-ca
traefik.http.services.prowlarr.loadbalancer.server.port: 9696
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/arr/Prowlarr/config |
/config |
Indexer definitions and settings |
Integration Points
Prowlarr pushes its configured indexers to downstream *arr apps via their APIs. Each app is added in Prowlarr Settings → Apps:
| App | API Endpoint (media-network) |
|---|---|
| Sonarr | http://sonarr:8989 |
| Radarr | http://radarr:7878 |
| Whisparr | http://whisparr:6969 |
| LazyLibrarian | http://lazylibrarian:5299 |
When an indexer is added or updated in Prowlarr, it is automatically propagated to all connected apps. No manual indexer configuration in individual *arr apps is needed.
Notes / Gotchas
- FlareSolverr (currently offline) is used by Prowlarr to bypass Cloudflare-protected indexers. When FlareSolverr is down, any Cloudflare-protected indexer will fail. Configure the FlareSolverr proxy URL in Prowlarr Settings → Indexers → Proxies.
- Prowlarr's API key is required when adding Prowlarr as the indexer source in each *arr application. Find it in Prowlarr Settings → General → Security.
- Indexer categories must be correctly mapped in Prowlarr for each app (e.g. TV categories for Sonarr, movie categories for Radarr) — misconfigured categories result in wrong content being returned in searches.
Last Updated: 2026-06-16
04-bazarr.md
kstack: book: Centerpoint Home Lab chapter: Media Management page: Bazarr tags: [bazarr, subtitles, arr, media-management]
Overview
Bazarr automatically downloads subtitles for movies and TV shows managed by Radarr and Sonarr. It monitors both libraries and fetches subtitles from configured providers (OpenSubtitles, Subscene, etc.) whenever a new file is added or an existing file is missing subtitles.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://bazarr.home.local |
LAN access via Step-CA TLS |
No external route — internal management only.
Configuration
Image: lscr.io/linuxserver/bazarr:latest
Compose project: arr stack
Traefik Labels
traefik.enable: "true"
traefik.http.routers.bazarr.rule: Host(`bazarr.home.local`)
traefik.http.routers.bazarr.entrypoints: websecure
traefik.http.routers.bazarr.tls.certresolver: step-ca
traefik.http.services.bazarr.loadbalancer.server.port: 6767
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/arr/Bazarr/config |
/config |
Bazarr database and settings |
/mnt/Multimedia |
/media |
Media library for subtitle placement (NFS from UnRAID) |
The /media mount mirrors the Sonarr/Radarr mount so Bazarr can write subtitle
files directly alongside the video files in the library.
Integration Points
| Service | Connection | Purpose |
|---|---|---|
| Sonarr | API (http://sonarr:8989) via media-network |
TV library sync |
| Radarr | API (http://radarr:7878) via media-network |
Movie library sync |
Notes / Gotchas
- Subtitle provider API keys (OpenSubtitles, etc.) are stored in Bazarr's settings. These are rate-limited — avoid aggressive subtitle searches on large libraries.
- Bazarr writes subtitle
.srtor.assfiles next to the video files in/mnt/Multimedia. Plex and Jellyfin pick these up automatically on the next library scan. - If Sonarr or Radarr is restarted, Bazarr may take a few minutes to re-sync its library view via the API.
Last Updated: 2026-06-16
05-nzbget.md
kstack: book: Centerpoint Home Lab chapter: Media Management page: NZBGet tags: [nzbget, usenet, download-client, arr, media-management]
Overview
NZBGet is the Usenet download client for the homelab. It receives download jobs from Sonarr, Radarr, Whisparr, and LazyLibrarian, downloads from configured Usenet providers, unpacks archives, and notifies the requesting *arr application when complete.
Downloads are staged to the Ceph OSD volume (1TB_Vol2) for high-speed local
writes before being processed and moved to UnRAID.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://nzbget.home.local |
LAN access via Step-CA TLS |
| External | https://nzb.jeeves5454.ddns.net |
Authentik SSO + GeoBlock + CrowdSec |
Configuration
Image: lscr.io/linuxserver/nzbget:latest
Compose project: arr stack
Traefik Labels
# External route
traefik.http.routers.nzbget-external.rule: Host(`nzb.jeeves5454.ddns.net`)
traefik.http.routers.nzbget-external.entrypoints: websecure
traefik.http.routers.nzbget-external.tls.certresolver: letsencrypt
traefik.http.routers.nzbget-external.middlewares: authentik-auth@docker,plex-geoblock@file,crowdsec-bouncer@file
traefik.http.routers.nzbget-external.service: nzbget-svc
# Internal route
traefik.http.routers.nzbget-internal.rule: Host(`nzbget.home.local`)
traefik.http.routers.nzbget-internal.entrypoints: websecure
traefik.http.routers.nzbget-internal.tls.certresolver: step-ca
traefik.http.routers.nzbget-internal.service: nzbget-svc
traefik.http.services.nzbget-svc.loadbalancer.server.port: 6789
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/arr/NZBGet/config |
/config |
NZBGet settings and queue database |
/media/jeeves/1TB_Vol2/downloads |
/downloads |
Download staging directory (Ceph OSD) |
/mnt/Multimedia |
/media |
Media library (used by post-process scripts) |
Download Directory Layout
NZBGet organises downloads under /downloads with category subdirectories:
/media/jeeves/1TB_Vol2/downloads/
├── usenet/
│ ├── intermediate/ ← In-progress downloads
│ └── complete/
│ ├── tv/ ← Completed TV episodes → Sonarr picks up
│ ├── movies/ ← Completed movies → Radarr picks up
│ ├── books/ ← Completed books → LazyLibrarian picks up
│ └── adult/ ← Completed adult content → Whisparr picks up
Notes / Gotchas
- Usenet provider credentials (server address, port, username, password) are stored in NZBGet's settings — these are REDACTED in all documentation.
- NZBGet's built-in web UI has its own username/password in addition to the Authentik ForwardAuth layer on the external route. Both must be configured.
- The Ceph OSD download volume provides fast local NVMe performance for downloads
before they are moved to the slower NFS path. Ensure adequate free space is
maintained on
1TB_Vol2. - Each *arr app connects to NZBGet via
http://nzbget:6789on themedia-networkusing the NZBGet API credentials configured in the *arr download client settings.
Last Updated: 2026-06-16
06-whisparr.md
kstack: book: Centerpoint Home Lab chapter: Media Management page: Whisparr tags: [whisparr, arr, media-management, adult-content]
Overview
Whisparr is a Sonarr fork purpose-built for managing adult content. It integrates with Prowlarr for indexer searches and NZBGet for downloads, following the same *arr workflow as Sonarr but with metadata sources appropriate for its content type. Downloaded content is delivered to the same media volume that Stash monitors.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://whisparr.home.local |
LAN access via Step-CA TLS |
No external route — internal management only.
Configuration
Image: ghcr.io/hotio/whisparr:v3
Compose project: arr stack
Traefik Labels
traefik.enable: "true"
traefik.http.routers.whisparr.rule: Host(`whisparr.home.local`)
traefik.http.routers.whisparr.entrypoints: websecure
traefik.http.routers.whisparr.tls.certresolver: step-ca
traefik.http.services.whisparr.loadbalancer.server.port: 6969
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/arr/Whisparr/config |
/config |
Whisparr database and settings |
/media/jeeves/1TB_Vol2/downloads |
/downloads |
NZBGet download staging (Ceph OSD) |
/mnt/Multimedia |
/data |
Media library including adult content (NFS from UnRAID) |
Integration Points
| Service | Connection Method | Purpose |
|---|---|---|
| Prowlarr | API (http://prowlarr:9696) via media-network |
Indexer search |
| NZBGet | API (http://nzbget:6789) via media-network |
Download client |
Notes / Gotchas
- The
hotio/whisparr:v3image is a community-maintained build. Monitor the repository for updates as this is not an official Servarr project. - Whisparr's library root folder should point to the same path that Stash monitors
(
/mnt/Multimedia/ST) so content is automatically picked up by Stash after import. - Prowlarr indexer categories for adult content must be configured correctly in Prowlarr and mapped to Whisparr in the Apps settings.
Last Updated: 2026-06-16
07-lazylibrarian.md
kstack: book: Centerpoint Home Lab chapter: Media Management page: LazyLibrarian tags: [lazylibrarian, books, ebooks, magazines, arr, media-management]
Overview
LazyLibrarian manages ebook, audiobook, and magazine acquisition. It monitors
authors and publications, searches for releases via Prowlarr, downloads via NZBGet,
and organises the completed files into the books library on UnRAID. Despite its
internal name, it is accessed externally via the readarr.jeevesconsults.ca domain.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://readarr.home.local |
LAN access via Step-CA TLS |
| External | https://readarr.jeevesconsults.ca |
Authentik SSO + GeoBlock + CrowdSec |
The external domain uses
readarr.jeevesconsults.ca(notlazylibrarian.*).
Configuration
Image: lscr.io/linuxserver/lazylibrarian:latest
Compose project: arr stack
Traefik Labels
# External route
traefik.http.routers.lazylibrarian-external.rule: Host(`readarr.jeevesconsults.ca`)
traefik.http.routers.lazylibrarian-external.entrypoints: websecure
traefik.http.routers.lazylibrarian-external.tls.certresolver: letsencrypt
traefik.http.routers.lazylibrarian-external.middlewares: authentik-auth@docker,plex-geoblock@file,crowdsec-bouncer@file
traefik.http.routers.lazylibrarian-external.service: lazylibrarian-svc
# Internal route
traefik.http.routers.lazylibrarian-internal.rule: Host(`readarr.home.local`)
traefik.http.routers.lazylibrarian-internal.entrypoints: websecure
traefik.http.routers.lazylibrarian-internal.tls.certresolver: step-ca
traefik.http.routers.lazylibrarian-internal.service: lazylibrarian-svc
traefik.http.services.lazylibrarian-svc.loadbalancer.server.port: 5299
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/arr/lazylibrarian/config |
/config |
LazyLibrarian database and settings |
/media/jeeves/1TB_Vol2/downloads/usenet/complete/books |
/downloads |
Completed book downloads (Ceph OSD) |
/mnt/Multimedia/Books/Lazylibrarian |
/books |
Final book library (NFS from UnRAID) |
Integration Points
| Service | Connection Method | Purpose |
|---|---|---|
| Prowlarr | API (http://prowlarr:9696) via media-network |
Indexer search |
| NZBGet | API (http://nzbget:6789) via media-network |
Download client |
Notes / Gotchas
- LazyLibrarian covers ebooks, audiobook metadata management (separate from Audiobookshelf's podcast/streaming role), and magazines. Configure each content type under its own library section within LazyLibrarian.
- The external domain (
readarr.jeevesconsults.ca) was chosen for cleaner branding — update DNS and Traefik labels if the domain ever changes. - Calibre integration can be enabled in LazyLibrarian settings if a Calibre server is available on the network for ebook format conversion.
Last Updated: 2026-06-16
08-mylar3.md
kstack: book: Centerpoint Home Lab chapter: Media Management page: Mylar3 tags: [mylar3, comics, cbz, arr, media-management]
Overview
Mylar3 is the automated comic book downloader and manager for the homelab. It monitors comic series, searches for new issues via Usenet indexers (through NZBGet), and organises downloaded CBZ/CBR files into the comics library on UnRAID.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://mylar.home.local |
LAN access via Step-CA TLS |
No external route — internal management only.
Configuration
Image: lscr.io/linuxserver/mylar3:latest
Compose project: arr stack
Traefik Labels
traefik.enable: "true"
traefik.http.routers.mylar3.rule: Host(`mylar.home.local`)
traefik.http.routers.mylar3.entrypoints: websecure
traefik.http.routers.mylar3.tls.certresolver: step-ca
traefik.http.services.mylar3.loadbalancer.server.port: 8090
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/arr/mylar/config |
/config |
Mylar3 database and settings |
/media/jeeves/1TB_Vol2/downloads/usenet/complete/books |
/downloads |
Completed downloads (Ceph OSD, shared with books) |
/mnt/Multimedia/Books/Comics |
/comics |
Comics library (NFS from UnRAID) |
Integration Points
| Service | Connection Method | Purpose |
|---|---|---|
| NZBGet | API (http://nzbget:6789) via media-network |
Download client |
Mylar3 connects directly to NZBGet rather than through Prowlarr. Indexers are configured directly in Mylar3 settings.
Notes / Gotchas
- Mylar3 uses ComicVine as its metadata source — a free API key from
comicvine.gamespot.comis required and stored in the Mylar3 config. - The
/downloadspath is shared with LazyLibrarian (both point to the books completion directory). Ensure Mylar3's category in NZBGet is distinct to avoid processing conflicts. - Mylar3's database (
mylar.db) stores the full comic watchlist and download history. Back it up before upgrades.
Last Updated: 2026-06-16
09-audiobookrequest.md
kstack: book: Centerpoint Home Lab chapter: Media Management page: Audiobookrequest tags: [audiobookrequest, audiobooks, requests, media-management]
Overview
Audiobookrequest is a self-hosted audiobook request portal — similar in concept to Seerr but for audiobooks. Users can search for and request audiobooks, which are then queued for download via the configured acquisition tools (LazyLibrarian).
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://audiobookseer.home.local |
LAN access via Step-CA TLS |
No external route — LAN and Tailscale access only.
Configuration
Image: markbeep/audiobookrequest:1
Compose project: arr stack
Traefik Labels
traefik.enable: "true"
traefik.http.routers.audiobookrequest.rule: Host(`audiobookseer.home.local`)
traefik.http.routers.audiobookrequest.entrypoints: websecure
traefik.http.routers.audiobookrequest.tls.certresolver: step-ca
traefik.http.services.audiobookrequest.loadbalancer.server.port: 8000
Key Environment Variables
| Variable | Value | Purpose |
|---|---|---|
TZ |
America/Toronto |
Timezone |
ABR_APP__PORT |
8000 |
Application listen port |
ABR_APP__FORCE_LOGIN_TYPE |
forms |
Forces form-based login |
ABR_APP__VERSION |
1.10.5 |
Application version |
ABR_APP__FORCE_LOGIN_TYPE=forms disables any SSO/header-based login and enforces
the standard username/password login form.
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/arr/audiobookrequest/config |
/config |
Request database and settings |
Notes / Gotchas
- User accounts and request history are stored in the
/configbind mount. - Audiobookrequest is pinned to image tag
1— check for newer tagged releases before upgrading. - Integration with LazyLibrarian (for fulfilling requests) must be configured in Audiobookrequest settings with the LazyLibrarian API endpoint and key.
Last Updated: 2026-06-16
10-profilarr.md
kstack: book: Centerpoint Home Lab chapter: Media Management page: Profilarr tags: [profilarr, quality-profiles, arr, media-management]
Overview
Profilarr is a quality profile and custom format manager for the *arr stack. It maintains a centralised set of quality profiles and custom format definitions and syncs them across Sonarr, Radarr, and other *arr apps via their APIs — ensuring consistent quality standards across all services without manual duplication.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://profilarr.home.local |
LAN access via Step-CA TLS |
No external route — internal management only.
Configuration
Image: santiagosayshey/profilarr:latest
Compose project: arr stack
Traefik Labels
traefik.enable: "true"
traefik.http.routers.profilarr.rule: Host(`profilarr.home.local`)
traefik.http.routers.profilarr.entrypoints: websecure
traefik.http.routers.profilarr.tls.certresolver: step-ca
traefik.http.services.profilarr.loadbalancer.server.port: 6868
Environment Variables
| Variable | Value | Purpose |
|---|---|---|
TZ |
America/Toronto |
Timezone |
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/arr/profilarr/config |
/config |
Profile definitions and sync state |
Notes / Gotchas
- Quality profiles in Sonarr and Radarr should not be edited manually when Profilarr is managing them — changes will be overwritten on the next sync.
- Profilarr can import community-maintained profile packs (e.g. TRaSH Guides profiles) and push them to all connected *arr apps.
- API keys for each connected *arr application must be configured in Profilarr settings.
Last Updated: 2026-06-16
11-dispatcharr.md
kstack: book: Centerpoint Home Lab chapter: Media Management page: Dispatcharr tags: [dispatcharr, iptv, m3u, media-management]
Overview
Dispatcharr is an IPTV playlist and stream management tool. It organises M3U channel lists, manages IPTV stream sources, and acts as a companion to Threadfin — providing a more structured interface for managing channel groups, EPG mappings, and stream prioritisation before they are published to Plex and Jellyfin via Threadfin.
Dispatcharr runs in all-in-one (aio) mode, bundling its application server,
Celery task worker, and Redis instance into a single container.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://dispatcharr.home.local |
LAN access via Step-CA TLS |
No external route — internal management only.
Configuration
Image: ghcr.io/dispatcharr/dispatcharr:latest
Compose project: arr stack
Traefik Labels
traefik.enable: "true"
traefik.http.routers.dispatcharr.rule: Host(`dispatcharr.home.local`)
traefik.http.routers.dispatcharr.entrypoints: websecure
traefik.http.routers.dispatcharr.tls.certresolver: step-ca
traefik.http.services.dispatcharr.loadbalancer.server.port: 9191
Key Environment Variables
| Variable | Value | Purpose |
|---|---|---|
DISPATCHARR_ENV |
aio |
All-in-one mode (app + worker + Redis) |
DISPATCHARR_LOG_LEVEL |
info |
Logging verbosity |
REDIS_HOST |
localhost |
Internal Redis (bundled in AIO mode) |
CELERY_BROKER_URL |
redis://localhost:6379/0 |
Celery task queue |
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/arr/dispatcharr/data |
/data |
Dispatcharr database and config |
Notes / Gotchas
- In
aiomode, Redis runs inside the container and does not persist state between restarts beyond what is stored in the/databind mount. Task queue state is lost on container restart. - Dispatcharr integrates with Threadfin as the downstream consumer of its managed M3U playlists. Configure the Dispatcharr output URL in Threadfin as the M3U source.
ghcr.io/dispatcharr/dispatcharr:latestis an actively developed project — check release notes before pulling updates as breaking changes may affect the channel database schema.
Last Updated: 2026-06-16
12-pulsarr.md
kstack: book: Centerpoint Home Lab chapter: Media Management page: Pulsarr tags: [pulsarr, watchlist, notifications, arr, media-management]
Overview
Pulsarr is a watchlist automation and notification bridge for the *arr stack. It monitors Plex and Jellyfin watchlists, automatically adds requested movies and TV shows to Radarr and Sonarr, and sends notifications when content is downloaded and available. It also integrates with TMDB for metadata-enriched notifications.
Access
| Type | URL | Notes |
|---|---|---|
| Internal | https://pulsarr.home.local |
LAN access via Step-CA TLS |
| External | https://pulsarr.jeeves5454.ddns.net |
Authentik SSO + GeoBlock + CrowdSec |
Configuration
Image: lakker/pulsarr:latest
Compose project: arr stack
Traefik Labels
# External route
traefik.http.routers.pulsarr-external.rule: Host(`pulsarr.jeeves5454.ddns.net`)
traefik.http.routers.pulsarr-external.entrypoints: websecure
traefik.http.routers.pulsarr-external.tls.certresolver: letsencrypt
traefik.http.routers.pulsarr-external.middlewares: authentik-auth@docker,plex-geoblock@file,crowdsec-bouncer@file
traefik.http.routers.pulsarr-external.service: pulsarr-svc
# Internal route
traefik.http.routers.pulsarr-internal.rule: Host(`pulsarr.home.local`)
traefik.http.routers.pulsarr-internal.entrypoints: websecure
traefik.http.routers.pulsarr-internal.tls.certresolver: step-ca
traefik.http.routers.pulsarr-internal.service: pulsarr-svc
traefik.http.services.pulsarr-svc.loadbalancer.server.port: 3003
Key Environment Variables
| Variable | Value | Purpose |
|---|---|---|
TZ |
America/Toronto |
Timezone |
port |
3003 |
Application listen port |
tmdbApiKey |
REDACTED | TMDB API key for metadata and notification enrichment |
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/arr/pulsarr/config |
/app/data |
Pulsarr database and configuration |
Integration Points
| Service | Purpose |
|---|---|
| Sonarr | Adds TV shows from watchlists; receives completion events |
| Radarr | Adds movies from watchlists; receives completion events |
| Plex | Reads watchlists from Plex accounts |
| TMDB | Fetches poster art and metadata for notifications |
Notes / Gotchas
- The TMDB API key (
tmdbApiKey) is visible in the container environment — treat it as a secret. Rotate atthemoviedb.orgif exposed. - Pulsarr's watchlist sync requires Plex account API tokens configured in the Pulsarr UI settings.
- Pulsarr is healthy-checked (
(healthy)status indocker ps) — the healthcheck endpoint is configured internally. - Notification channels (Discord, Telegram, etc.) are configured in Pulsarr settings
and stored in the
/app/databind mount.
Last Updated: 2026-06-16
13-flaresolverr.md
kstack: book: Centerpoint Home Lab chapter: Media Management page: FlareSolverr (Offline) tags: [flaresolverr, cloudflare, prowlarr, offline, media-management]
Overview
Status: OFFLINE — The FlareSolverr container is present but currently not running (exited 8 weeks ago). It can be restarted when Cloudflare-protected indexers need to be accessed via Prowlarr.
FlareSolverr is a proxy server that bypasses Cloudflare's anti-bot protection for web-based indexers. Prowlarr routes requests for Cloudflare-protected indexers through FlareSolverr, which uses a headless browser session to solve the challenge and return the page content.
Access
FlareSolverr has no web UI. It is accessed internally by Prowlarr via its HTTP API
at http://flaresolverr:8191.
Configuration
Image: ghcr.io/flaresolverr/flaresolverr:latest
Status: exited
Prowlarr Integration
Once running, configure in Prowlarr:
- Settings → Indexers → Add Proxy
- Type: FlareSolverr
- Host:
http://flaresolverr:8191 - Tag: Apply the tag to any Cloudflare-protected indexer
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/flaresolver |
/config |
FlareSolverr config |
Notes / Gotchas
- FlareSolverr uses a headless Chromium browser. It consumes significant CPU and RAM during challenge solving — resource spikes are normal.
- Cloudflare regularly updates its bot detection. FlareSolverr may stop working after Cloudflare updates and require an image update.
- When FlareSolverr is offline, any Prowlarr indexer tagged with the FlareSolverr proxy will fail silently on search — no error surfaced in Sonarr/Radarr. Check Prowlarr indexer status if searches return no results.
- To restart:
docker start flaresolverror redeploy via Portainer.
Last Updated: 2026-06-16
h06-documents-organization
00-chapter-intro.md
kstack: book: Centerpoint Home Lab chapter: Documents & Organization page: Chapter Introduction tags: [documents, organization, paperless, bookstack, productivity]
Overview
This chapter covers personal productivity, knowledge management, and life-organisation services. These range from document archival and wiki systems through to recipe management, home inventory, plant care, and fitness tracking.
Services in This Chapter
| Service | Container(s) | Purpose |
|---|---|---|
| Paperless-NGX | paperless-webserver-1, paperless-db-1, paperless-broker-1, paperless_gotenberg, paperless_tika |
Document management and archival |
| BookStack | bookstack, bookstack-db |
Self-hosted wiki and documentation |
| Karakeep | karakeep-web |
Bookmarks and read-it-later |
| Memos | memos |
Quick notes and journal |
| Booklore | booklore, booklore-db |
Personal book library and reading tracker |
| Mealie | mealie, mealieaddons |
Recipe management and meal planning |
| Homebox | homebox |
Home inventory management |
| Medikeep | medikeep, medikeep-db |
Personal medical records tracker |
| LubeLogger | lubelogger, lubelogger-db |
Vehicle maintenance log |
| HortusFox | hortusfox, hortusfox-db, hortusfox-cron |
Plant care and garden management |
| LinkStack | linkstack |
Public link-in-bio / personal landing page |
| Reitti | reitti-app, reitti-postgis, reitti-redis, reitti-rabbitmq, reitti-tiles, reitti-photon |
Activity and fitness tracking |
| Swarm-Reitti Bridge | swarm-reitti-bridge |
Custom Foursquare→Reitti check-in sync service |
Last Updated: 2026-06-17
01-paperless-ngx.md
kstack: book: Centerpoint Home Lab chapter: Documents & Organization page: Paperless-NGX tags: [paperless, documents, ocr, archive, productivity]
Overview
Paperless-NGX is the centralised document management and archival system. It ingests scanned documents and PDFs via a watched consume directory and email, applies OCR, tags, correspondents, and document types, then stores the processed results in a structured archive. Gmail OAuth is configured for email ingestion. SSO is provided via Authentik OpenID Connect.
Access
| Type | URL | Auth |
|---|---|---|
| External | https://paperless.jeeves5454.ddns.net |
Authentik OIDC + GeoBlock + CrowdSec |
| Internal | https://paperless.home.local |
Step-CA TLS (internal) |
Authentication uses Authentik OIDC (openid_connect Django allauth provider).
Regular local login is kept enabled (PAPERLESS_DISABLE_REGULAR_LOGIN=false).
Auto-signup for new OIDC users is disabled — accounts must be pre-created.
Containers
Five containers in this stack:
| Container | Image | Role |
|---|---|---|
paperless-webserver-1 |
ghcr.io/paperless-ngx/paperless-ngx:latest |
Web UI + workers |
paperless-db-1 |
postgres:16 |
Primary database |
paperless-broker-1 |
redis:7 |
Celery task queue |
paperless_gotenberg |
gotenberg/gotenberg:8.27 |
DOCX→PDF conversion |
paperless_tika |
apache/tika:latest |
Content extraction |
paperless-webserver-1
Main application container running both the Django web server and Celery workers.
Key environment variables:
| Variable | Value / Notes |
|---|---|
PAPERLESS_URL |
https://paperless.jeeves5454.ddns.net |
PAPERLESS_CSRF_TRUSTED_ORIGINS |
https://paperless.jeeves5454.ddns.net |
PAPERLESS_OAUTH_CALLBACK_BASE_URL |
https://paperless.jeeves5454.ddns.net |
PAPERLESS_DBHOST |
db |
PAPERLESS_REDIS |
redis://broker:6379 |
PAPERLESS_TIKA_ENABLED |
1 |
PAPERLESS_TIKA_ENDPOINT |
http://tika:9998 |
PAPERLESS_TIKA_GOTENBERG_ENDPOINT |
http://gotenberg:3000 |
PAPERLESS_TASK_WORKERS |
2 |
PAPERLESS_THREADS_PER_WORKER |
2 |
PAPERLESS_TIME_ZONE |
America/Toronto |
PAPERLESS_OCR_LANGUAGE |
eng |
PAPERLESS_CONSUMER_RECURSIVE |
true |
PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS |
true |
PAPERLESS_CONSUMER_POLLING |
5 (seconds) |
PAPERLESS_FILENAME_FORMAT |
{{ created_year }}/{{ document_type }}/{{ created_year }}-{{ created_month }}-{{ created_day }}_{{ correspondent }}_{{ title }} |
PAPERLESS_FILENAME_FORMAT_REMOVE_NONE |
true |
PAPERLESS_SOCIAL_AUTO_SIGNUP |
false |
PAPERLESS_ACCOUNT_EMAIL_VERIFICATION |
none |
PAPERLESS_GMAIL_OAUTH_CLIENT_ID |
(visible — treat as semi-public) |
PAPERLESS_GMAIL_OAUTH_CLIENT_SECRET |
REDACTED |
PAPERLESS_APPS |
allauth.socialaccount.providers.openid_connect |
| OIDC provider client secret | REDACTED (inside PAPERLESS_SOCIALACCOUNT_PROVIDERS) |
Bind mounts:
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/paperless/data |
/usr/src/paperless/data |
Index and SQLite state |
/mnt/data/paperless/media |
/usr/src/paperless/media |
Archived document files |
/mnt/data/paperless/consume |
/usr/src/paperless/consume |
Watched consume folder |
/mnt/data/paperless/export |
/usr/src/paperless/export |
Export output folder |
The
media,consume, andexportdirectories live on the NFS-equivalent Ceph volume at/mnt/data/, providing separation from the system drive.
paperless-db-1
PostgreSQL 16 database backend.
Bind mounts:
| Host Path | Container Path |
|---|---|
/home/jeeves/docker/paperless/pgdata |
/var/lib/postgresql/data |
paperless-broker-1
Redis 7 for Celery task queue.
Bind mounts:
| Host Path | Container Path |
|---|---|
/home/jeeves/docker/paperless/redis |
/data |
paperless_gotenberg
Gotenberg 8.27 — converts Office documents (DOCX, XLSX, etc.) to PDF for ingestion. No persistent volumes.
paperless_tika
Apache Tika — extracts content and metadata from complex document formats. No persistent volumes.
Traefik Labels
traefik.http.routers.paperless-external.rule: Host(`paperless.jeeves5454.ddns.net`)
traefik.http.routers.paperless-external.entrypoints: websecure
traefik.http.routers.paperless-external.tls.certresolver: letsencrypt
traefik.http.routers.paperless-external.middlewares: plex-geoblock@file,crowdsec-bouncer@file
traefik.http.routers.paperless-internal.rule: Host(`paperless.home.local`)
traefik.http.routers.paperless-internal.entrypoints: websecure
traefik.http.routers.paperless-internal.tls.certresolver: step-ca
Notes / Gotchas
- Secrets in
PAPERLESS_SOCIALACCOUNT_PROVIDERS(OIDC client secret) andPAPERLESS_GMAIL_OAUTH_CLIENT_SECRETare in plaintext in the Portainer-managed compose environment — rotate if the Portainer env is ever exposed. - The consume directory polling interval is 5 seconds. Drop files into
/mnt/data/paperless/consume(or a subdirectory — subdirectory names become tags automatically viaPAPERLESS_CONSUMER_SUBDIRS_AS_TAGS). - Tika and Gotenberg must be running for complex document formats to process; plain-text PDFs work without them.
- The
PAPERLESS_LOGOUT_REDIRECT_URLis set to the Authentik end-session endpoint — clicking Log Out in Paperless also terminates the Authentik session.
Last Updated: 2026-06-17
02-bookstack.md
kstack: book: Centerpoint Home Lab chapter: Documents & Organization page: BookStack tags: [bookstack, wiki, documentation, mariadb, oidc]
Overview
BookStack is the self-hosted personal knowledge base and documentation wiki — the platform this very documentation is written in. It organises content into Shelves → Books → Chapters → Pages and provides a full Markdown editor. Access is protected by Authentik OIDC on the external route. On the internal route, authentication still passes through BookStack's own OIDC flow (no Traefik ForwardAuth bypass), so the Authentik session is always required.
Access
| Type | URL | Auth |
|---|---|---|
| External | https://wiki.jeeves5454.ddns.net |
Authentik OIDC + GeoBlock + CrowdSec |
| Internal | https://wiki.home.local |
Authentik OIDC (Step-CA TLS) |
BookStack uses its own OIDC integration rather than Traefik ForwardAuth. The
external Traefik route applies GeoBlock and CrowdSec but not
authentik-auth@docker — BookStack manages the OIDC redirect itself.
Containers
| Container | Image | Role |
|---|---|---|
bookstack |
lscr.io/linuxserver/bookstack:latest |
Web application |
bookstack-db |
mariadb:10.11 |
MariaDB database |
bookstack (application)
Key environment variables:
| Variable | Value / Notes |
|---|---|
APP_URL |
https://wiki.jeeves5454.ddns.net |
AUTH_METHOD |
oidc |
AUTH_AUTO_INITIATE |
true — skips BookStack login page, redirects to Authentik |
OIDC_NAME |
Authentik |
OIDC_ISSUER |
https://auth.jeevesconsults.ca/application/o/book-stack-website-s/ |
OIDC_ISSUER_DISCOVER |
true |
OIDC_CLIENT_ID |
QjpMMmpDqNCQIM75np6gGyMkN9Y569AtyNVhfJvx |
OIDC_CLIENT_SECRET |
REDACTED |
OIDC_EXTERNAL_ID_CLAIM |
email |
OIDC_DISPLAY_NAME_CLAIMS |
name |
OIDC_FETCH_AVATAR |
false |
OIDC_END_SESSION_ENDPOINT |
false |
DB_HOST |
bookstack-db |
DB_DATABASE |
bookstack |
DB_USERNAME |
bookstack |
DB_PASSWORD |
REDACTED |
MAIL_HOST |
smtp.gmail.com |
MAIL_PORT |
587 |
MAIL_USERNAME |
jeeves5454@gmail.com |
MAIL_ENCRYPTION |
TLS |
MAIL_FROM |
noreply@jeevesconsults.ca |
PUID / PGID |
1000 |
TZ |
America/Toronto |
Bind mounts:
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/bookstack/config |
/config |
App config, attachments, uploads |
/home/jeeves/docker/bookstack/public |
/public |
Public web assets |
bookstack-db (MariaDB 10.11)
Bind mounts:
| Host Path | Container Path |
|---|---|
/home/jeeves/docker/bookstack/db |
/var/lib/mysql |
Traefik Labels
traefik.http.routers.bookstack-ext.rule: Host(`wiki.jeeves5454.ddns.net`)
traefik.http.routers.bookstack-ext.entrypoints: websecure
traefik.http.routers.bookstack-ext.tls.certresolver: letsencrypt
traefik.http.routers.bookstack-ext.middlewares: plex-geoblock@file,crowdsec-bouncer@file
traefik.http.routers.bookstack-ext.service: bookstack-svc
traefik.http.routers.bookstack-int.rule: Host(`wiki.home.local`)
traefik.http.routers.bookstack-int.entrypoints: websecure
traefik.http.routers.bookstack-int.tls.certresolver: step-ca
traefik.http.routers.bookstack-int.service: bookstack-svc
traefik.http.services.bookstack-svc.loadbalancer.server.port: 80
Notes / Gotchas
APP_URLis set to the external URL even though both internal and external routes exist. This is intentional — it is used for OIDC redirect URIs and email links, which must be reachable externally.AUTH_AUTO_INITIATE: truemeans anyone who hits the URL is immediately redirected to Authentik. There is no BookStack login form shown unless OIDC fails. To bypass OIDC in an emergency, use?prevent_auto_init=trueappended to the URL.- For an existing user to link their Authentik account, go to Admin → Users → select user → set External Authentication ID to their email address.
- The LSIO image uses
DB_USERNAME, notDB_USER. UsingDB_USERsilently falls back to no authentication and causes confusing login failures. APP_KEY(Laravel application key) is stored in the/configdirectory and persists across container restarts. Do not delete the config bind mount or the key must be regenerated and all sessions will be invalidated.
Last Updated: 2026-06-17
03-karakeep.md
kstack: book: Centerpoint Home Lab chapter: Documents & Organization page: Karakeep tags: [karakeep, bookmarks, read-later, productivity]
Overview
Karakeep is a self-hosted bookmark manager and read-it-later service. It crawls
saved URLs for their full text and screenshots, indexes them via Meilisearch,
and supports OCR on images (ocr_langs=eng). Video download is enabled
(yt-dlp backend, no size limit). SMTP is configured for email-to-save via Gmail.
New signups are disabled — account management is manual.
Access
| Type | URL | Auth |
|---|---|---|
| External | https://bookmark.jeevesconsults.ca |
GeoBlock + CrowdSec (own auth) |
External-only — no internal home.local route configured.
Configuration
Image: ghcr.io/karakeep-app/karakeep:latest
Key Environment Variables
| Variable | Value / Notes |
|---|---|
NEXTAUTH_URL |
https://bookmark.jeevesconsults.ca |
NEXTAUTH_URL_INTERNAL |
http://karakeep-web:3000 |
NEXTAUTH_TRUST_HOST |
true |
NEXTAUTH_SECRET |
REDACTED |
DATA_DIR |
/data |
ASSETS_DIR |
/assets |
MEILI_ADDR |
http://meilisearch:7700 |
MEILI_MASTER_KEY |
REDACTED |
BROWSER_WEB_URL |
http://chrome:9222 (headless Chromium) |
OCR_LANGS |
eng |
OCR_CONFIDENCE_THRESHOLD |
75 |
CRAWLER_VIDEO_DOWNLOAD |
true |
CRAWLER_VIDEO_DOWNLOAD_MAX_SIZE |
-1 (unlimited) |
CRAWLER_VIDEO_DOWNLOAD_TIMEOUT_SEC |
7200 |
INFERENCE_ENABLE_AUTO_SUMMARIZATION |
true |
MAX_ASSET_SIZE_MB |
50 |
DISABLE_SIGNUPS |
true |
EMAIL_VERIFICATION_REQUIRED |
false |
SMTP_HOST |
smtp.gmail.com |
SMTP_PORT |
587 |
SMTP_SECURE |
true |
SMTP_USER |
jeeves5454@gmail.com |
SMTP_FROM |
jeeves5454@gmail.com |
SMTP_PASSWORD |
REDACTED |
Traefik Labels
traefik.http.routers.karakeep.rule: Host(`bookmark.jeevesconsults.ca`)
traefik.http.routers.karakeep.entrypoints: websecure
traefik.http.routers.karakeep.tls.certresolver: letsencrypt
traefik.http.routers.karakeep.middlewares: plex-geoblock@file,crowdsec-bouncer@file,karakeep-headers
traefik.http.middlewares.karakeep-headers.headers.customrequestheaders.X-Forwarded-Proto: https
traefik.http.services.karakeep.loadbalancer.server.port: 3000
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/karakeep/data |
/data |
Database and app data |
/mnt/data/karakeep |
/assets |
Saved assets (screenshots, videos) |
Stack Companions
Karakeep requires additional sidecar containers (Meilisearch, Headless Chrome) in the same compose network to function. These are not independently accessible.
Notes / Gotchas
- The
X-Forwarded-Proto: httpsmiddleware is required for Karakeep (Next.js) to trust the proxy and generate correct redirect URIs — without it, HTTPS callbacks will fail. - Video downloads can consume significant storage on
/mnt/data/karakeep. The timeout is 2 hours, which accommodates long-form content. - Meilisearch index data is stored in
/data— this must be backed up alongside the app database for search to remain functional after a restore. DISABLE_SIGNUPS: true— new accounts must be created manually in the admin UI.
Last Updated: 2026-06-17
04-memos.md
kstack: book: Centerpoint Home Lab chapter: Documents & Organization page: Memos tags: [memos, notes, journal, productivity]
Overview
Memos is a lightweight, self-hosted note-taking and micro-journaling app. It provides a simple Twitter/Mastodon-style feed of notes, supports Markdown, tags, and public/private visibility per memo. Internal-only access — no external route is configured.
Access
| Type | URL | Auth |
|---|---|---|
| Internal | https://memos.home.local |
Memos own auth (Step-CA TLS) |
No external route — accessible only on the LAN.
Configuration
Image: neosmemo/memos:stable
Environment Variables
| Variable | Value | Purpose |
|---|---|---|
MEMOS_INSTANCE_URL |
https://memos.home.local |
Canonical URL |
MEMOS_PORT |
5230 |
Listen port |
TZ |
America/Toronto |
Timezone |
Traefik Labels
traefik.http.routers.memos.rule: Host(`memos.home.local`)
traefik.http.routers.memos.entrypoints: websecure
traefik.http.routers.memos.tls.certresolver: step-ca
traefik.http.services.memos.loadbalancer.server.port: 5230
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/memos/data |
/var/opt/memos |
Database and assets |
Memos uses SQLite stored in /var/opt/memos. No external database dependency.
Notes / Gotchas
neosmemo/memos:stabletracks the stable release tag. Upgrading the image will migrate the SQLite database automatically on first startup.- There is no Authentik integration on this service — Memos manages its own accounts internally.
- Memos supports public content sharing via short links; ensure private memos are set to private if personal content is being stored.
Last Updated: 2026-06-17
05-booklore.md
kstack: book: Centerpoint Home Lab chapter: Documents & Organization page: Booklore tags: [booklore, books, library, mariadb, step-ca]
Overview
Booklore is a personal e-book library manager. It indexes books from the NFS
Multimedia share (read-only) and a local bookdrop directory for new additions.
The app is a Java/Spring Boot application (JDK 25 with Shenandoah GC) backed by
MariaDB. It uses its own internal authentication — no Authentik ForwardAuth or
OIDC. The Step-CA root certificate is bind-mounted into the system trust store
to allow Booklore to make HTTPS calls to internal *.home.local services.
Access
| Type | URL | Auth |
|---|---|---|
| External | https://booklore.jeevesconsults.ca |
GeoBlock + CrowdSec (own auth) |
| Internal | https://booklore.home.local |
Own auth (Step-CA TLS) |
Containers
| Container | Image | Role |
|---|---|---|
booklore |
grimmory/grimmory:latest |
Web application |
booklore-db |
lscr.io/linuxserver/mariadb:latest |
MariaDB database |
booklore (application)
Key environment variables:
| Variable | Value / Notes |
|---|---|
DATABASE_USERNAME |
booklore |
DATABASE_PASSWORD |
REDACTED |
DATABASE_URL |
jdbc:mariadb://booklore-db:3306/booklore |
BOOKLORE_PORT |
6060 |
DISK_TYPE |
NETWORK (books from NFS mount) |
USER_ID / GROUP_ID |
1000 |
TZ |
America/Toronto |
APP_VERSION |
v3.2.0 |
Runtime: Java 25 (Temurin JDK 25.0.3) with Shenandoah GC.
JVM tuning: max 60% RAM, Shenandoah compact heuristics, 256MB metaspace cap.
Bind mounts:
| Host Path | Container Path | Purpose |
|---|---|---|
/mnt/Multimedia/Books |
/books |
Book library (read-only via NFS) |
/home/jeeves/docker/booklore/data |
/app/data |
App state and metadata |
/home/jeeves/docker/booklore/bookdrop |
/bookdrop |
Drop zone for new books |
/home/jeeves/docker/step-ca/config/certs/root_ca.crt |
/usr/local/share/ca-certificates/step-ca.crt |
Step-CA root trust injection |
booklore-db (MariaDB LSIO)
Image: lscr.io/linuxserver/mariadb:latest
| Variable | Value |
|---|---|
MYSQL_DATABASE |
booklore |
MYSQL_USER |
booklore |
MYSQL_PASSWORD |
REDACTED |
PUID / PGID |
1000 |
Bind mounts:
| Host Path | Container Path |
|---|---|
/home/jeeves/docker/booklore/mariadb |
/config |
Traefik Labels
traefik.http.routers.booklore-external.rule: Host(`booklore.jeevesconsults.ca`)
traefik.http.routers.booklore-external.entrypoints: websecure
traefik.http.routers.booklore-external.tls.certresolver: letsencrypt
traefik.http.routers.booklore-external.middlewares: plex-geoblock@file,crowdsec-bouncer@file
traefik.http.routers.booklore-external.service: booklore-svc
traefik.http.routers.booklore-internal.rule: Host(`booklore.home.local`)
traefik.http.routers.booklore-internal.entrypoints: websecure
traefik.http.routers.booklore-internal.tls.certresolver: step-ca
traefik.http.routers.booklore-internal.service: booklore-svc
traefik.http.services.booklore-svc.loadbalancer.server.port: 6060
Notes / Gotchas
- The Step-CA root certificate bind mount (
root_ca.crt→/usr/local/share/ca-certificates/) makes the container's Java runtime trust internal TLS certificates. This is required if Booklore makes any HTTPS requests to*.home.localservices. /mnt/Multimedia/Booksis the NFS share from UnRAID — it must be mounted before Booklore starts or the library scan will fail silently. Checkdf -h /mnt/Multimediaif the library appears empty.- Drop new e-books (EPUB, PDF) into
/home/jeeves/docker/booklore/bookdropfor automatic library import. grimmory/grimmory:latestis the container image name for the Booklore project.
Last Updated: 2026-06-17
06-mealie.md
kstack: book: Centerpoint Home Lab chapter: Documents & Organization page: Mealie tags: [mealie, recipes, meal-planning, oidc, authentik]
Overview
Mealie is a self-hosted recipe manager and meal planner. It imports recipes from
URLs, supports manual entry, and provides a weekly meal plan interface. OIDC
authentication is via Authentik, with auto-redirect enabled. The mealieaddons
sidecar (Mealie Addons by razziel89) provides enhanced recipe retrieval, PDF
export via Pandoc, and image embedding.
Access
| Type | URL | Auth |
|---|---|---|
| External | https://recipe.jeevesconsults.ca |
Authentik OIDC + GeoBlock + CrowdSec |
| Internal | https://mealie.home.local |
Authentik OIDC (Step-CA TLS) |
OIDC auto-redirect is enabled (OIDC_AUTO_REDIRECT: true) — the Mealie login
form is bypassed and Authentik is shown directly.
Containers
| Container | Image | Role |
|---|---|---|
mealie |
ghcr.io/mealie-recipes/mealie:latest |
Core application |
mealieaddons |
ghcr.io/razziel89/mealie-addons:latest |
Enhanced retrieval and export |
mealie (application)
Key environment variables:
| Variable | Value / Notes |
|---|---|
BASE_URL |
https://recipe.jeevesconsults.ca |
OIDC_AUTH_ENABLED |
true |
OIDC_AUTO_REDIRECT |
true |
OIDC_PROVIDER_NAME |
Authentik |
OIDC_CONFIGURATION_URL |
https://auth.jeevesconsults.ca/application/o/mealie/.well-known/openid-configuration |
OIDC_CLIENT_ID |
cjTjv6CLnu0pT7qkkthDTjXmaVPYdxMfOKW9d5Wy |
OIDC_CLIENT_SECRET |
REDACTED |
OIDC_USER_GROUP |
family-friends |
OIDC_ADMIN_GROUP |
admins |
OIDC_SIGNUP_ENABLED |
false |
OIDC_REMEMBER_ME |
true |
ALLOW_SIGNUP |
false |
SMTP_HOST |
smtp.gmail.com |
SMTP_PORT |
587 |
SMTP_AUTH_STRATEGY |
TLS |
SMTP_USER |
jeeves5454@gmail.com |
SMTP_PASSWORD |
REDACTED |
SMTP_FROM_EMAIL |
jeeves5454@gmail.com |
PUID / PGID |
1000 |
TZ |
America/Toronto |
Bind mounts:
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/mealie/data |
/app/data |
Database and assets |
mealieaddons
Mealie Addons provides enhanced recipe scraping, Pandoc-based PDF/EPUB export, and image embedding for recipes.
Key environment variables:
| Variable | Value / Notes |
|---|---|
MEALIE_BASE_URL |
https://recipe.jeevesconsults.ca |
MEALIE_RETRIEVAL_URL |
http://mealie:9000 |
MA_SELF_URL |
http://localhost:9000 |
MA_LISTEN_INTERFACE |
:9000 |
MA_IMAGE_ACTION |
embed |
MA_TIMEOUT_SECS |
60 |
MA_RETRIEVAL_LIMIT |
5 |
GIN_MODE |
release |
PANDOC_FLAGS |
--epub-title-page=false |
No bind mounts — stateless.
Traefik route:
traefik.http.routers.mealieaddons.rule: Host(`mealieaddons.home.local`)
traefik.http.routers.mealieaddons.entrypoints: websecure
traefik.http.routers.mealieaddons.tls.certresolver: step-ca
traefik.http.services.mealieaddons.loadbalancer.server.port: 9000
Traefik Labels (mealie)
traefik.http.routers.mealie-ext.rule: Host(`recipe.jeevesconsults.ca`)
traefik.http.routers.mealie-ext.entrypoints: websecure
traefik.http.routers.mealie-ext.tls.certresolver: letsencrypt
traefik.http.routers.mealie-ext.middlewares: plex-geoblock@file,crowdsec-bouncer@file,mealie-headers
traefik.http.routers.mealie-ext.service: mealie-svc
traefik.http.routers.mealie-int.rule: Host(`mealie.home.local`)
traefik.http.routers.mealie-int.entrypoints: websecure
traefik.http.routers.mealie-int.tls.certresolver: step-ca
traefik.http.routers.mealie-int.service: mealie-svc
traefik.http.middlewares.mealie-headers.headers.customrequestheaders.X-Forwarded-Proto: https
traefik.http.services.mealie-svc.loadbalancer.server.port: 9000
Notes / Gotchas
OIDC_USER_GROUP: family-friendslimits OIDC login to members of that Authentik group. Users not in this group can be denied access even with valid Authentik credentials. Manage group membership in Authentik admin.- The
X-Forwarded-Proto: httpsheader middleware is required for Mealie to generate correct OIDC redirect URIs. Without it, HTTPS URLs will not form correctly and the OIDC flow will fail. - Mealie uses SQLite by default when no external database is configured. The
database file lives in
/app/data. Back up this directory before upgrades. mealieaddonsis accessible atmealieaddons.home.local— this is the URL to configure in Mealie settings for the enhanced importer.
Last Updated: 2026-06-17
07-homebox.md
kstack: book: Centerpoint Home Lab chapter: Documents & Organization page: Homebox tags: [homebox, inventory, home-management, authentik]
Overview
Homebox is a home inventory management app for tracking household items,
their locations, warranties, and purchase records. The external route uses
Authentik ForwardAuth for SSO. The internal route applies X-Forwarded-Proto
only (Homebox manages its own session internally). Signups are disabled;
registration is not allowed.
Access
| Type | URL | Auth |
|---|---|---|
| External | https://homebox.jeeves5454.ddns.net |
Authentik ForwardAuth + GeoBlock + CrowdSec |
| Internal | https://homebox.home.local |
Homebox own session (Step-CA TLS) |
Configuration
Image: ghcr.io/sysadminsmedia/homebox:latest
Key Environment Variables
| Variable | Value / Notes |
|---|---|
HBOX_MODE |
production |
HBOX_LOG_LEVEL |
info |
HBOX_LOG_FORMAT |
text |
HBOX_WEB_MAX_UPLOAD_SIZE |
10 (MB) |
HBOX_OPTIONS_ALLOW_REGISTRATION |
false |
HBOX_OPTIONS_ALLOW_ANALYTICS |
false |
HBOX_STORAGE_PREFIX_PATH |
data |
TZ |
America/Toronto |
Traefik Labels
traefik.http.routers.homebox-ext.rule: Host(`homebox.jeeves5454.ddns.net`)
traefik.http.routers.homebox-ext.entrypoints: websecure
traefik.http.routers.homebox-ext.tls.certresolver: letsencrypt
traefik.http.routers.homebox-ext.middlewares: authentik-auth@docker,plex-geoblock@file,crowdsec-bouncer@file,homebox-headers
traefik.http.routers.homebox-ext.service: homebox-svc
traefik.http.routers.homebox-int.rule: Host(`homebox.home.local`)
traefik.http.routers.homebox-int.entrypoints: websecure
traefik.http.routers.homebox-int.tls.certresolver: step-ca
traefik.http.routers.homebox-int.middlewares: homebox-headers
traefik.http.routers.homebox-int.service: homebox-svc
traefik.http.middlewares.homebox-headers.headers.customrequestheaders.X-Forwarded-Proto: https
traefik.http.services.homebox-svc.loadbalancer.server.port: 7745
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/homebox/data |
/data |
SQLite database and uploads |
Homebox uses SQLite (homebox.db) stored in the /data bind mount with WAL
journal mode and foreign key enforcement enabled.
Notes / Gotchas
- The external route applies
authentik-auth@dockerForwardAuth — all external access requires a valid Authentik session. The internal route skips ForwardAuth but applies theX-Forwarded-Protoheader middleware (required for HTTPS-aware links and session cookies). HBOX_OPTIONS_ALLOW_REGISTRATION: falsedisables self-service account creation. New accounts must be created by an admin in the Homebox UI.- Homebox stores photos and attachments in the
/databind mount alongside the SQLite database. Back up the entire directory before upgrades.
Last Updated: 2026-06-17
08-medikeep.md
kstack: book: Centerpoint Home Lab chapter: Documents & Organization page: Medikeep tags: [medikeep, medical, health, postgres, authentik-sso]
Overview
Medikeep is a personal medical records tracker. It stores health records, prescriptions, appointments, and related documents. SSO is configured via Authentik (native OIDC integration — not Traefik ForwardAuth). The external route uses GeoBlock and CrowdSec; the internal route is TLS-only with no additional auth middleware.
Access
| Type | URL | Auth |
|---|---|---|
| External | https://medical.jeeves5454.ddns.net |
Authentik OIDC + GeoBlock + CrowdSec |
| Internal | https://medikeep.home.local |
Authentik OIDC (Step-CA TLS) |
SSO is handled by Medikeep's built-in SSO_ENABLED: true — it redirects to
Authentik on login. Traefik does not apply ForwardAuth to this service.
Containers
| Container | Image | Role |
|---|---|---|
medikeep |
ghcr.io/afairgiant/medikeep:latest |
Web application |
medikeep-db |
postgres:15.8-alpine |
PostgreSQL database |
medikeep (application)
Key environment variables:
| Variable | Value / Notes |
|---|---|
SSO_ENABLED |
true |
SSO_PROVIDER_TYPE |
authentik |
SSO_ISSUER_URL |
https://auth.jeevesconsults.ca/application/o/medikeep/ |
SSO_CLIENT_ID |
EN44sdEMtf29bgTN077W48XsCSpi9bj1Wk0eypI1 |
SSO_CLIENT_SECRET |
REDACTED |
SSO_REDIRECT_URI |
https://medical.jeeves5454.ddns.net/auth/sso/callback |
DB_HOST |
medikeep-db |
DB_PORT |
5432 |
DB_NAME |
medical_records |
DB_USER |
medapp |
DB_PASSWORD |
REDACTED |
LOG_LEVEL |
DEBUG |
LOG_ROTATION_METHOD |
logrotate |
ENABLE_API_DOCS |
false |
DEBUG |
false |
PUID / PGID |
1000 |
TZ |
America/Toronto |
Bind mounts:
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/medikeep/uploads |
/app/uploads |
Document uploads |
/home/jeeves/docker/medikeep/logs |
/app/logs |
Application logs |
/home/jeeves/docker/medikeep/backups |
/app/backups |
Backup output |
medikeep-db (PostgreSQL 15.8)
Image: postgres:15.8-alpine
| Variable | Value |
|---|---|
POSTGRES_DB |
medical_records |
POSTGRES_USER |
medapp |
POSTGRES_PASSWORD |
REDACTED |
Bind mounts:
| Host Path | Container Path |
|---|---|
/home/jeeves/docker/medikeep/postgres/data |
/var/lib/postgresql/data |
Traefik Labels
traefik.http.routers.medikeep-ext.rule: Host(`medical.jeeves5454.ddns.net`)
traefik.http.routers.medikeep-ext.entrypoints: websecure
traefik.http.routers.medikeep-ext.tls.certresolver: letsencrypt
traefik.http.routers.medikeep-ext.middlewares: plex-geoblock@file,crowdsec-bouncer@file
traefik.http.routers.medikeep-ext.service: medikeep-svc
traefik.http.routers.medikeep-int.rule: Host(`medikeep.home.local`)
traefik.http.routers.medikeep-int.entrypoints: websecure
traefik.http.routers.medikeep-int.tls.certresolver: step-ca
traefik.http.routers.medikeep-int.service: medikeep-svc
traefik.http.services.medikeep-svc.loadbalancer.server.port: 8000
Notes / Gotchas
SSO_REDIRECT_URIpoints to the external domain — this is the OAuth callback URL registered in Authentik. Even when accessing via the internalhome.localURL, the OIDC callback will redirect through the external domain.- Medikeep's
LOG_LEVELis set toDEBUG— logs may be verbose. Logs are accessible in the bind-mounted/app/logsdirectory. - Contains sensitive personal medical data. Limit backup exposure and ensure the PostgreSQL data directory is included in regular backup schedules.
Last Updated: 2026-06-17
09-lubelogger.md
kstack: book: Centerpoint Home Lab chapter: Documents & Organization page: LubeLogger tags: [lubelogger, vehicles, maintenance, postgres]
Overview
LubeLogger is a vehicle maintenance and fuel log tracker. It stores service
records, oil changes, tyre rotations, fuel fill-ups, and other vehicle events.
External-only route with GeoBlock and CrowdSec — no internal home.local route.
Authentication is LubeLogger's own built-in user system. Backend is PostgreSQL 16.
Access
| Type | URL | Auth |
|---|---|---|
| External | https://logger.jeeves5454.ddns.net |
GeoBlock + CrowdSec (own auth) |
No internal route configured.
Containers
| Container | Image | Role |
|---|---|---|
lubelogger |
ghcr.io/hargata/lubelogger:latest |
Web application |
lubelogger-db |
postgres:16 |
PostgreSQL database |
lubelogger (application)
Image: ghcr.io/hargata/lubelogger:latest
Runtime: .NET 10.0.8 (ASP.NET Core)
Key environment variables:
| Variable | Value / Notes |
|---|---|
POSTGRES_CONNECTION |
REDACTED (includes password) |
ASPNETCORE_HTTP_PORTS |
8080 |
LC_ALL / LANG |
en_US.UTF-8 |
Bind mounts:
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/lubelogger/data |
/App/data |
App data and uploads |
/home/jeeves/docker/lubelogger/keys |
/root/.aspnet/DataProtection-Keys |
ASP.NET data protection keys |
lubelogger-db (PostgreSQL 16)
Image: postgres:16
| Variable | Value |
|---|---|
POSTGRES_USER |
lubelogger |
POSTGRES_DB |
lubelogger |
POSTGRES_PASSWORD |
REDACTED |
Bind mounts:
| Host Path | Container Path |
|---|---|
/home/jeeves/docker/lubelogger/db |
/var/lib/postgresql/data |
Traefik Labels
traefik.http.routers.lubelogger.rule: Host(`logger.jeeves5454.ddns.net`)
traefik.http.routers.lubelogger.entrypoints: websecure
traefik.http.routers.lubelogger.tls.certresolver: letsencrypt
traefik.http.routers.lubelogger.middlewares: plex-geoblock@file,crowdsec-bouncer@file,lubelogger-headers
traefik.http.middlewares.lubelogger-headers.headers.customrequestheaders.X-Forwarded-Proto: https
traefik.docker.network: traefik-net
traefik.http.services.lubelogger.loadbalancer.server.port: 8080
Notes / Gotchas
POSTGRES_CONNECTIONin the container environment contains the database password in plaintext — treat this as sensitive and avoid logging the environment of this container.- The
DataProtection-Keysbind mount (/lubelogger/keys) must persist across container restarts. If it is lost, all active user sessions will be invalidated and users will need to log in again. - The
X-Forwarded-Proto: httpsmiddleware is required for ASP.NET Core to correctly identify requests as HTTPS when behind a reverse proxy.
Last Updated: 2026-06-17
10-hortusfox.md
kstack: book: Centerpoint Home Lab chapter: Documents & Organization page: HortusFox tags: [hortusfox, plants, garden, mariadb]
Overview
HortusFox is a plant care and garden management app. It tracks plants, watering schedules, fertilisation, and overdue care tasks. A separate cron container triggers scheduled task checks. Internal-only access via Step-CA TLS — no external route. The workspace is named "Jeeves Garden".
Access
| Type | URL | Auth |
|---|---|---|
| Internal | https://hortusfox.home.local |
HortusFox own auth (Step-CA TLS) |
No external route — LAN access only.
Containers
| Container | Image | Role |
|---|---|---|
hortusfox |
ghcr.io/danielbrendel/hortusfox-web:latest |
Web application |
hortusfox-db |
mariadb:11 |
MariaDB database |
hortusfox-cron |
alpine:latest |
Scheduled task runner |
hortusfox (application)
Runtime: PHP 8.3 on Apache
Key environment variables:
| Variable | Value / Notes |
|---|---|
APP_WORKSPACE |
Jeeves Garden |
APP_TIMEZONE |
America/Toronto |
APP_LANG |
en |
APP_DEBUG |
true |
APP_ADMIN_EMAIL |
jeeves5454@gmail.com |
APP_OVERDUE_TASK_HOURS |
10 |
APP_ENABLE_HISTORY |
true |
APP_HISTORY_NAME |
Garden Log |
APP_ENABLE_SYSTEM_MESSAGES |
true |
APP_ENABLE_PHOTO_SHARE |
false |
APP_ENABLE_CHAT |
false |
APP_ENABLE_SCROLLER |
true |
APP_CRON_PW |
REDACTED |
DB_HOST |
hortusfox-db |
DB_PORT |
3306 |
DB_DATABASE |
hortusfox |
DB_USERNAME |
hortusfox |
DB_CHARSET |
utf8mb4 |
Bind mounts:
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/hortusfox/images |
/var/www/html/public/img |
Plant images |
/home/jeeves/docker/hortusfox/themes |
/var/www/html/public/themes |
Custom themes |
/home/jeeves/docker/hortusfox/logs |
/var/www/html/app/logs |
App logs |
/home/jeeves/docker/hortusfox/migrations |
/var/www/html/app/migrations |
DB migration files |
/home/jeeves/docker/hortusfox/backup |
/var/www/html/public/backup |
Export/backup files |
hortusfox-db (MariaDB 11)
Image: mariadb:11 (MariaDB 11.8.6)
| Variable | Value |
|---|---|
MYSQL_DATABASE |
hortusfox |
MYSQL_USER |
hortusfox |
MYSQL_PASSWORD |
REDACTED |
MYSQL_ROOT_PASSWORD |
REDACTED |
Bind mounts:
| Host Path | Container Path |
|---|---|
/home/jeeves/docker/hortusfox/db |
/var/lib/mysql |
hortusfox-cron (Alpine)
Image: alpine:latest
A lightweight Alpine container that runs periodic cron jobs to trigger
HortusFox's scheduled task processing (e.g., overdue task notifications).
It communicates with the main hortusfox container using the APP_CRON_PW
credential. No persistent volumes.
Traefik Labels
traefik.http.routers.hortusfox.rule: Host(`hortusfox.home.local`)
traefik.http.routers.hortusfox.entrypoints: websecure
traefik.http.routers.hortusfox.tls.certresolver: step-ca
traefik.http.services.hortusfox.loadbalancer.server.port: 80
Notes / Gotchas
APP_CRON_PWauthenticates the cron container's requests to the HortusFox web app. This is set as a plaintext environment variable — treat it as a secret. It is REDACTED in documentation.APP_DEBUG: truemeans verbose PHP error output is enabled. Disable this if HortusFox ever becomes accessible externally.- Plant images are stored in the
imagesbind mount. This directory should be included in backups to preserve photo history.
Last Updated: 2026-06-17
11-linkstack.md
kstack: book: Centerpoint Home Lab chapter: Documents & Organization page: LinkStack tags: [linkstack, linktree, about-me, public, website]
Overview
LinkStack is a self-hosted link-in-bio / personal landing page, serving as the
public-facing "about me" page and link hub for jeevesconsults.ca. It responds
to two separate domain names on a single Traefik route: the primary
aboutme.jeevesconsults.ca and the root www.jeevesconsults.ca.
Access
| URL | Auth |
|---|---|
https://aboutme.jeevesconsults.ca |
Public — no auth |
https://www.jeevesconsults.ca |
Public — no auth |
Both hostnames resolve to the same LinkStack instance. No authentication middleware — this is a public-facing page.
Configuration
Image: linkstackorg/linkstack:latest
Environment Variables
| Variable | Value / Notes |
|---|---|
HTTPS_SERVER_NAME |
aboutme.jeevesconsults.ca |
SERVER_ADMIN |
ask@jeevesconsults.ca |
PHP_MEMORY_LIMIT |
512M |
UPLOAD_MAX_FILESIZE |
8M |
TZ |
America/Toronto |
Traefik Labels
traefik.http.routers.linkstack.rule: Host(`aboutme.jeevesconsults.ca`) || Host(`www.jeevesconsults.ca`)
traefik.http.routers.linkstack.entrypoints: websecure
traefik.http.routers.linkstack.tls.certresolver: letsencrypt
traefik.http.services.linkstack.loadbalancer.server.port: 80
The
||in the Traefik host rule matches either domain — both resolve to the same container and present the same LinkStack profile.
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/linkstack/data |
/htdocs |
App files, config, SQLite |
Notes / Gotchas
HTTPS_SERVER_NAMEis set toaboutme.jeevesconsults.ca. Even though both domains are handled by Traefik, LinkStack internally uses this value for canonical URL generation. Links may contain this hostname.- No GeoBlock or CrowdSec on this route — it is intentionally fully public. Monitor access logs if abuse is suspected.
- LinkStack stores its own SQLite database and assets under
/htdocs. The entiredata/bind mount must be backed up to preserve the profile content. - Letsencrypt will issue certificates for both
aboutme.jeevesconsults.caandwww.jeevesconsults.cavia the same Traefik router — both must resolve to Centerpoint's public IP in DNS for certificate issuance to succeed.
Last Updated: 2026-06-17
12-reitti.md
kstack: book: Centerpoint Home Lab chapter: Documents & Organization page: Reitti tags: [reitti, fitness, activity-tracking, postgis, rabbitmq, redis]
Overview
Reitti is a self-hosted activity and fitness tracking platform. It records GPS tracks, check-ins, and location history and displays them on interactive maps. The platform is a 6-container stack: the core Java application, a PostGIS spatial database, Redis cache, RabbitMQ message queue, a tile proxy for map tiles, and Photon for geocoding.
External access is at https://reitti.jeeves5454.ddns.net. Check-in data from
Foursquare/Swarm is synced into Reitti via the separate
swarm-reitti-bridge service (see next page).
Access
| Type | URL | Auth |
|---|---|---|
| External | https://reitti.jeeves5454.ddns.net |
Reitti own auth + letsencrypt |
No internal home.local route — external-only.
Containers
| Container | Image | Role |
|---|---|---|
reitti-app |
dedicatedcode/reitti:latest |
Core Java application |
reitti-postgis |
postgis/postgis:17-3.5-alpine |
Spatial database (PostgreSQL 17 + PostGIS 3.5) |
reitti-redis |
redis:7-alpine |
Cache and session store |
reitti-rabbitmq |
rabbitmq:3-management-alpine |
Message queue for async jobs |
reitti-tiles |
nginx:alpine |
Map tile caching proxy |
reitti-photon |
rtuszik/photon-docker:1.3.0 |
Local geocoding (Nominatim-based) |
reitti-app
The main Spring Boot application serving the Reitti web UI and REST API. Ingests location data via the OwnTracks-compatible endpoint used by swarm-reitti-bridge.
Key environment variables:
| Variable | Value / Notes |
|---|---|
POSTGIS_HOST |
postgis |
POSTGIS_PORT |
5432 |
POSTGIS_DB |
reittidb |
POSTGIS_USER |
reitti |
POSTGIS_PASSWORD |
REDACTED |
REDIS_HOST |
redis |
REDIS_PORT |
6379 |
RABBITMQ_HOST |
rabbitmq |
RABBITMQ_PORT |
5672 |
RABBITMQ_USER |
reitti |
RABBITMQ_PASSWORD |
REDACTED |
PHOTON_BASE_URL |
http://photon:2322 |
Volumes:
| Volume / Mount | Container Path | Purpose |
|---|---|---|
Docker volume reitti_reitti-data |
/data |
Application data and uploads |
Traefik labels:
traefik.http.routers.reitti.rule: Host(`reitti.jeeves5454.ddns.net`)
traefik.http.routers.reitti.entrypoints: websecure
traefik.http.routers.reitti.tls.certresolver: letsencrypt
traefik.http.services.reitti.loadbalancer.server.port: 8080
reitti-postgis
PostgreSQL 17 with PostGIS 3.5 spatial extension. Stores all GPS track data, waypoints, and geospatial indices.
Image: postgis/postgis:17-3.5-alpine
| Variable | Value |
|---|---|
POSTGRES_USER |
reitti |
POSTGRES_DB |
reittidb |
POSTGRES_PASSWORD |
REDACTED |
Volumes:
| Volume | Container Path |
|---|---|
reitti_postgis-data (named) |
/var/lib/postgresql/data |
reitti-redis
Redis 7 (Alpine) — used for caching and session management.
Image: redis:7-alpine
Volumes:
| Volume | Container Path |
|---|---|
reitti_redis-data (named) |
/data |
No authentication configured — network-isolated to the Reitti internal stack network.
reitti-rabbitmq
RabbitMQ 3 with management plugin. Handles async processing of imported tracks and location events.
Image: rabbitmq:3-management-alpine
| Variable | Value |
|---|---|
RABBITMQ_DEFAULT_USER |
reitti |
RABBITMQ_DEFAULT_PASS |
REDACTED |
Volumes:
| Volume | Container Path |
|---|---|
reitti_rabbitmq-data (named) |
/var/lib/rabbitmq |
RabbitMQ management UI is available internally on port 15672 (not exposed via Traefik).
reitti-tiles
NGINX-based map tile proxy/cache. Serves map tiles from upstream tile providers with local caching to reduce external requests.
Image: nginx:alpine (NGINX 1.29.8)
| Variable | Value |
|---|---|
NGINX_CACHE_SIZE |
1g |
No persistent volumes — tile cache is in-memory. No external Traefik route.
reitti-photon
Local geocoding service using the Photon engine (Nominatim/OSM-based).
Configured for the ca (Canada) region with parallel update strategy.
Image: rtuszik/photon-docker:1.3.0
| Variable | Value |
|---|---|
REGION |
ca |
UPDATE_STRATEGY |
PARALLEL |
Volumes:
| Volume | Container Path |
|---|---|
reitti_photon-data (named) |
/photon/data |
The photon data volume holds the downloaded Nominatim extract for the CA region. It can be large (several GB); allow time for the initial data download on first start.
Data Flow
GPS device / OwnTracks → Reitti OwnTracks ingest API
Foursquare/Swarm check-ins → swarm-reitti-bridge → Reitti OwnTracks ingest API
↓
reitti-app (Spring Boot)
↓
RabbitMQ (async processing)
↓
PostGIS (spatial storage)
↓
Redis (cache) + Photon (geocoding)
↓
Web UI / Map tiles
Notes / Gotchas
- All named Docker volumes (
reitti_reitti-data,reitti_postgis-data,reitti_redis-data,reitti_rabbitmq-data,reitti_photon-data) must be included in any backup strategy — none are bind-mounted to host paths. - PostGIS 17 requires spatial extensions to be enabled in the database on first
run —
dedicatedcode/reitti:latesthandles this automatically on startup. - The Photon geocoding data download is region-specific (
REGION: ca). If geocoding stops working after a restart, check that the photon data volume is intact and the photon container started successfully. - RabbitMQ management UI (port 15672) is available inside the Docker network but
is not exposed externally. Access via
docker execor Portainer console if needed. - Check-in data from Foursquare/Swarm flows through swarm-reitti-bridge (see next page). Reitti itself has no Foursquare integration — the bridge translates and delivers data via the OwnTracks ingestion endpoint.
Last Updated: 2026-06-17
Swarm-Reitti Bridge
Overview
Swarm-Reitti Bridge (v1.1.0) is a custom-built Node.js service that automatically syncs Foursquare/Swarm check-ins into two downstream services:
- Reitti — self-hosted location history tracker, receiving check-ins in OwnTracks format
- AdventureLog — self-hosted travel journal, receiving check-ins as Locations, Visits, and World Travel region marks
It was built specifically for this homelab as a bridge between the Foursquare API and these self-hosted services — keeping all location history under local control.
The service runs at https://swarm.jeeves5454.ddns.net and is externally reachable so
the OAuth callback from Foursquare can complete.
This service was built in this homelab environment. Its source code lives at
/home/jeeves/docker/code-server/projects/swarm-reitti-bridge/and the production deployment at/home/jeeves/docker/swarmreitti/swarm-reitti-bridge/. The container image (swarm-reitti-bridge) is built locally viadocker compose buildand is not published to any registry.
Background: Why Polling Mode?
This bridge was originally designed to use Foursquare push notifications —
Foursquare would POST to the bridge's /push endpoint on every check-in,
enabling near-instant syncing. That architecture worked correctly.
In late 2024 / early 2025, Foursquare changed their API pricing and moved push notifications behind a paid credit system. Attempting to subscribe to push notifications returns:
{ "errorType": "credits_exhausted", "code": 402 }
The bridge was updated to polling mode as the solution. Instead of waiting for Foursquare to push, it actively polls the Foursquare API every 15 minutes for new check-ins.
| Aspect | Push (legacy, broken) | Polling (current) |
|---|---|---|
| Trigger | Foursquare → Bridge (instant) | Bridge → Foursquare (timer) |
| Latency | < 1 second | Up to 15 minutes |
| API credits required | Yes (paid) | No (free tier) |
| API calls/day | ~0 idle | 96 (10% of free quota) |
| Cost | Requires payment | $0.00 |
The 15-minute maximum latency is entirely acceptable for personal location history tracking.
How It Works
High-Level Flow
User checks in on Swarm
↓
Foursquare API stores check-in
↓
Bridge polls every 15 minutes (or immediately on container start)
GET /v2/users/self/checkins?oauth_token=...
↓
Bridge identifies new check-ins (deduplication via lastCheckinIds)
↓
┌────────────────────────────────────────────────────────┐
│ For each new check-in (oldest-first): │
│ │
│ 1. Convert to OwnTracks format │
│ POST to Reitti ingest API │
│ Authorization: Bearer <REITTI_API_TOKEN> │
│ ↓ │
│ Check-in on Reitti map │
│ │
│ 2. (if AdventureLog configured) │
│ Find or create AL Category (Foursquare → emoji) │
│ Find or create AL Location (dedup by name + coords) │
│ Create AL Visit (dedup: one per location per day) │
│ Mark AL World Travel region as visited │
│ ↓ │
│ Location + Visit in AdventureLog │
│ Region marked on world map │
└────────────────────────────────────────────────────────┘
↓
State saved to disk (lastCheckinId, lastCheckinTimestamp)
AdventureLog failures are non-fatal — Reitti sync completes regardless.
Deduplication
The bridge tracks lastCheckinIds per user — a map of userId → most recently processed check-in ID. On each poll:
- Quick check — fetches the single most recent check-in from Foursquare. If its ID
matches
lastCheckinIds[userId], the poll exits immediately (already up to date). - Gap fetch — if different, fetches all check-ins within a 2-week window (paginated, 50 per page), sorted oldest-first so Reitti receives events in chronological order.
Startup Behaviour
On container start, the bridge immediately polls for any users already stored in
state.json — it does not wait for the first 15-minute interval. This ensures
check-ins that occurred while the container was down are processed within seconds
of restart.
State Persistence
Three state objects are saved to /app/data/state.json:
| Key | Contents |
|---|---|
userTokens |
OAuth access tokens per userId (from Foursquare) |
lastCheckinTimestamps |
Last seen check-in Unix timestamp per userId |
lastCheckinIds |
Last processed check-in ID per userId |
Writes are atomic (write to .tmp, then rename) to avoid corruption on crash.
Permission note:
state.jsonis owned by root (written inside the container). Usesudowhen editing on the host.
Data Mapping
Reitti (OwnTracks Format)
| OwnTracks Field | Source | Notes |
|---|---|---|
_type |
Hardcoded: "location" |
OwnTracks type identifier |
lat |
checkin.venue.location.lat |
Venue latitude |
lon |
checkin.venue.location.lng |
Venue longitude (lng → lon) |
tst |
checkin.createdAt |
Unix timestamp of check-in |
tid |
First 2 chars of username, uppercased | OwnTracks tracker ID |
acc |
Hardcoded: 10 |
Accuracy in metres |
alt |
Hardcoded: 0 |
Altitude (not available from Foursquare) |
batt |
Hardcoded: 100 |
Battery (not applicable) |
vel |
Hardcoded: 0 |
Velocity (not applicable) |
t |
Hardcoded: "c" |
OwnTracks trigger type: check-in |
desc |
checkin.venue.name (max 200 chars) |
Venue name |
addr |
Address + City + State + Country (max 300 chars) | Formatted address string |
POST <REITTI_API_URL>
Authorization: Bearer <REITTI_API_TOKEN>
Content-Type: application/json
AdventureLog Integration
Category Mapping
- The primary Foursquare category maps to an AdventureLog category (created if absent)
- Category icons are resolved from a static Foursquare → emoji lookup table
(
adventurelog-categories.js); unknown categories fall back to🌍 - Secondary Foursquare categories become AdventureLog
tags
Location Deduplication
Locations are matched by venue name (lowercase) + coordinates rounded to 2 decimal places
(≈1.1 km tolerance). If a match exists, it is reused without modification. If not, a new
location is created. Coordinates sent to AdventureLog are rounded to 6 decimal places
(AdventureLog enforces a 9-total-digit limit on lat/lng fields).
Visit Deduplication
One visit is created per location per local calendar day. The local date is derived using
the venue's timezone (resolved from lat/lng via geo-tz). A check-in shout and any
companion names (with array) are combined into the visit notes field.
World Travel (Region Marking)
After each visit is created, the bridge resolves the check-in's country and state/province
to an ISO 3166-2 region code via AdventureLog's GET /api/regions list, then POSTs to
/api/visitedregion. Already-visited regions are cached per poll cycle and skipped silently.
A small country name map handles known Foursquare → AdventureLog naming mismatches
(e.g. México → Mexico). Unresolvable regions are logged as warnings.
Note: Creating Locations and Visits via the API does not automatically update the World Travel world map — these are separate systems in AdventureLog. The bridge handles both independently.
OAuth Setup
Foursquare OAuth 2.0 is used to grant the bridge permission to read check-in data. This is a one-time setup that persists in the state file.
Initial Setup Steps
-
Visit the auth URL:
https://swarm.jeeves5454.ddns.net/auth -
The bridge redirects to Foursquare with the configured
client_idandredirect_uri. -
Authenticate with your Foursquare account and authorise the app.
-
Foursquare redirects back to
https://swarm.jeeves5454.ddns.net/callbackwith an authorisation code. -
The bridge exchanges the code for an access token, saves it to state, and begins polling immediately.
Important: Re-authenticating via
/authresets the sync baseline to the most recent check-in at that moment — older unsynced check-ins will be skipped. Only re-authenticate if starting fresh or after a token expiry. If you need to reprocess past check-ins, patchstate.jsondirectly (see Operations below).
Foursquare App Configuration
| Field | Value |
|---|---|
| Redirect URI | https://swarm.jeeves5454.ddns.net/callback |
| Push API URL | (not required — polling mode) |
Access and Endpoints
External URL: https://swarm.jeeves5454.ddns.net
Certificate: Let's Encrypt (letsencrypt resolver)
Port: 3000 (internal)
API Endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/ |
Status dashboard — service cards, recent check-ins, recent errors |
GET |
/auth |
Start Foursquare OAuth flow (rate-limited: 10/15 min) |
GET |
/callback |
OAuth callback (Foursquare redirects here) |
GET |
/health |
JSON health — ok, per-service status, last sync times |
POST |
/push |
Legacy webhook endpoint (inactive in polling mode) |
The /health endpoint returns structured JSON including an ok: true/false field
suitable for Uptime Kuma's JSON Query monitor:
{
"ok": true,
"status": "healthy",
"connectedUsers": 1,
"reitti": { "configured": true, "lastSuccessAt": "...", "lastSuccessVenue": "..." },
"adventurelog": { "configured": true, "lastSuccessAt": "...", "lastSuccessVenue": "..." },
"lastPollAt": "..."
}
Configuration
Environment Variables
| Variable | Value / Notes |
|---|---|
FOURSQUARE_CLIENT_ID |
REDACTED (Foursquare developer console) |
FOURSQUARE_CLIENT_SECRET |
REDACTED |
FOURSQUARE_REDIRECT_URI |
https://swarm.jeeves5454.ddns.net/callback |
REITTI_API_URL |
https://reitti.jeeves5454.ddns.net/api/v1/ingest/owntracks |
REITTI_API_TOKEN |
REDACTED (Reitti API bearer token) |
PUSH_SECRET |
REDACTED (session signing secret) |
ADVENTURELOG_API_URL |
https://travel.jeeves5454.ddns.net |
ADVENTURELOG_API_KEY |
REDACTED (AdventureLog API key) |
UPTIME_KUMA_PUSH_URL |
REDACTED (Uptime Kuma push monitor URL, no query params) |
POLLING_INTERVAL_MINUTES |
15 (default) |
PORT |
3000 |
NODE_ENV |
production |
Omitting both ADVENTURELOG_API_URL and ADVENTURELOG_API_KEY disables AdventureLog
sync entirely. Omitting UPTIME_KUMA_PUSH_URL disables push heartbeats.
Traefik Labels
traefik.http.routers.swarm-bridge.rule: Host(`swarm.jeeves5454.ddns.net`)
traefik.http.routers.swarm-bridge.entrypoints: websecure
traefik.http.routers.swarm-bridge.tls.certresolver: letsencrypt
traefik.http.services.swarm-bridge.loadbalancer.server.port: 3000
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/swarmreitti/swarm-reitti-bridge/data |
/app/data |
State file (state.json) persisted across restarts |
Monitoring (Uptime Kuma)
Three complementary monitors are recommended:
| Monitor Type | Target | What it catches |
|---|---|---|
| Docker Container | Container name swarm-reitti-bridge |
Container crash / OOM kill |
| HTTP(s) | https://swarm.jeeves5454.ddns.net/health |
App unresponsive |
| HTTP(s) JSON Query | https://swarm.jeeves5454.ddns.net/health → $.ok |
Reitti/AL sync errors, no users |
| Push | Uptime Kuma push monitor URL | Poll loop stopped, token expired |
The push monitor receives ?status=up after every successful poll and
?status=down&msg=<reason> on failure. Configure the heartbeat interval to
POLLING_INTERVAL_MINUTES + 2 minutes (e.g. 17 minutes for a 15-minute poll interval).
Logging
The bridge uses structured JSON logging to stdout (Dozzle-compatible):
{"ts":"2026-06-23T12:00:00.000Z","level":"INFO","cat":"POLL","msg":"Scheduled poll","userCount":1}
{"ts":"2026-06-23T12:00:01.000Z","level":"INFO","cat":"CHECKIN","msg":"Sent to Reitti","venue":"Tim Hortons"}
{"ts":"2026-06-23T12:00:02.000Z","level":"INFO","cat":"AL","msg":"Location created","venue":"Tim Hortons","id":"..."}
{"ts":"2026-06-23T12:00:02.000Z","level":"INFO","cat":"AL","msg":"Visit created","locationId":"...","date":"2026-06-23"}
{"ts":"2026-06-23T12:00:02.000Z","level":"INFO","cat":"AL","msg":"Region marked as visited","regionId":"CA-ON"}
{"ts":"2026-06-23T12:00:03.000Z","level":"INFO","cat":"STATE","msg":"State saved","usersCount":1}
Log categories: SERVER, POLL, CHECKIN, AL, AUTH, STATE.
WARN and ERROR entries are also captured in an in-memory ring buffer (last 20) and
displayed on the status dashboard at /.
View live logs:
docker logs -f swarm-reitti-bridge
Operations and Management
Verify the Bridge is Running
# Container status
docker ps | grep swarm-reitti-bridge
# Health check (includes ok field and per-service status)
curl -s https://swarm.jeeves5454.ddns.net/health | jq .
# Status dashboard
open https://swarm.jeeves5454.ddns.net/
Re-authenticate with Foursquare
If the OAuth token expires or becomes invalid (bridge logs show Token expired WARN):
- Visit
https://swarm.jeeves5454.ddns.net/auth - Log in and authorise the app
- The bridge saves the new token and polls immediately
Do not re-authenticate to retry past check-ins — it will move the sync baseline forward and those check-ins will be permanently skipped.
Reprocess Past Check-ins
To force the bridge to reprocess a specific check-in (e.g. after fixing an AL sync error):
cd /home/jeeves/docker/swarmreitti/swarm-reitti-bridge
# View current state
sudo cat data/state.json
# Roll back to just before a specific check-in Unix timestamp
sudo python3 -c "
import json
with open('data/state.json') as f:
s = json.load(f)
s['lastCheckinIds']['<userId>'] = None
s['lastCheckinTimestamps']['<userId>'] = <tst - 1>
with open('data/state.json', 'w') as f:
json.dump(s, f, indent=2)
print('Done')
"
docker compose restart swarm-reitti-bridge
The startup poll will immediately pick up the check-in on restart.
Adjust Polling Interval
Edit POLLING_INTERVAL_MINUTES in the compose environment and rebuild:
| Interval | API calls/day | Use case |
|---|---|---|
5 |
288 | More responsive (30% quota) |
15 |
96 | Recommended (10% quota) |
30 |
48 | Most conservative (5% quota) |
Rebuild the Container
cd /home/jeeves/docker/swarmreitti/swarm-reitti-bridge
docker compose up -d --build
docker logs swarm-reitti-bridge --follow
Troubleshooting
Check-ins not appearing in Reitti
- Check the status dashboard at
/— the Reitti card shows last push time and any error message - Check logs:
docker logs swarm-reitti-bridge | grep -E '"cat":"CHECKIN"|"level":"ERROR"' - Verify token is connected:
curl -s https://swarm.jeeves5454.ddns.net/health | jq .connectedUsers— if0, re-authenticate via/auth - Test Reitti API directly:
Expected:curl -s -o /dev/null -w "%{http_code}" -X POST \ https://reitti.jeeves5454.ddns.net/api/v1/ingest/owntracks \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{"_type":"location","lat":43.65,"lon":-79.37,"tst":1782088106}'200. If401, the token in.envis wrong.
Check-ins not appearing in AdventureLog
- Check the status dashboard
/— the AdventureLog card shows last sync and any error - Verify AdventureLog API key is valid:
Expected: JSON withcurl -s "https://travel.jeeves5454.ddns.net/api/locations?limit=1" \ -H "X-API-Key: <key>"countfield. If403, the key is wrong. - Common error — trailing slash causes 308 redirect with lost body: the bridge strips
trailing slashes automatically; if seeing 400s verify the AL URL has no trailing slash
in
ADVENTURELOG_API_URL
World Travel region not updating
- The bridge only marks regions when
venue.location.stateandvenue.location.countryare both present in the Foursquare data. Venues with missing location data are skipped. - If a region resolves but the country/state name doesn't match AdventureLog's English
names, a
WARNlog is emitted:Could not resolve region. Add a mapping toFOURSQUARE_COUNTRY_MAPinindex.jsif needed. GET /api/visitedregioncan confirm which regions are already marked.
OAuth callback fails (redirect URI mismatch)
- Ensure
FOURSQUARE_REDIRECT_URIin env exactly matches the redirect URI registered in the Foursquare developer console:https://swarm.jeeves5454.ddns.net/callback
Bridge container exits immediately
- Check logs:
docker logs swarm-reitti-bridge - Most common cause: missing required environment variable. The app logs all missing vars and exits with code 1.
State file corruption
sudo rm /home/jeeves/docker/swarmreitti/swarm-reitti-bridge/data/state.json
docker restart swarm-reitti-bridge
# Then re-authenticate via /auth
Source Code
/home/jeeves/docker/code-server/projects/swarm-reitti-bridge/ ← development
/home/jeeves/docker/swarmreitti/swarm-reitti-bridge/ ← production deployment
├── index.js ← Main application
├── adventurelog-categories.js ← Foursquare category → emoji icon map
├── Dockerfile
├── docker-compose.yml
├── package.json ← version 1.1.0
├── tests/
│ ├── integration.test.js
│ └── unit.test.js
└── data/
└── state.json ← Runtime state (OAuth tokens, last check-in IDs)
Last Updated: 2026-06-23
AdventureLog - Travel Helper
Overview
Travel Helper is a Python-based sidecar service for AdventureLog. It runs on a weekly schedule and automatically links Locations (based on their Visits) to Collections, populates day-by-day itinerary entries, and adds region-level Locations as Trip Context. It is idempotent — re-running it never creates duplicates.
It also exposes a status web page at https://travel-helper.home.local showing
last run time, run statistics, live log output, and a manual trigger button.
Why It Exists
AdventureLog Collections support itinerary entries and linked Locations, but provides no automatic way to connect existing Visits to a Collection by date range. Without this service, every Collection must be linked manually, location by location.
Travel Helper solves this by scanning all Collections that have a start_date
and end_date, finding every Location that has a Visit falling within that
range, and wiring everything together automatically.
What It Does Each Run
- Loads all regions, locations (with embedded visits), and collections from AdventureLog.
- For each Collection with a date range:
- Finds all Locations whose Visit dates (converted to local timezone) fall within the collection's start–end range.
- Links those Locations to the Collection (
PATCH /api/locations/{id}). - Adds a day-level itinerary entry per visit date
(
POST /api/itineraries,is_global: false). - Resolves the region for each matched Location (from cache → AdventureLog
regionfield → Nominatim reverse geocode fallback). - Finds or creates a region-level Location (centroid of the region).
- Links that region Location to the Collection and adds it as Trip Context
(
POST /api/itineraries,is_global: true).
- Persists the region ↔ location ID cache to
/data/region_cache.jsonso Nominatim is only called for locations not yet resolved.
Access
| Type | URL |
|---|---|
| Internal | https://travel-helper.home.local |
| External | Not exposed |
The status page auto-refreshes every 60 seconds when idle, every 10 seconds while a sync is running.
Configuration
Image: travel-helper:latest (built locally — see Maintenance)
Source: /home/jeeves/docker/adventurelog/travel-helper/
Bind mount: /home/jeeves/docker/adventurelog/travel-helper/data → /data
Key environment variables (set in Portainer stack environment table):
| Variable | Value / Notes |
|---|---|
ADVENTURELOG_API_URLS |
Comma-separated, tried left-to-right. First reachable URL wins. |
http://adventurelog-web:3000,https://travel.home.local,https://<external> |
|
ADVENTURELOG_API_KEY |
Set in Portainer — never hardcode. Rotate here when key changes. |
NOMINATIM_USER_AGENT |
Must include a contact email per Nominatim usage policy. |
NOMINATIM_DELAY |
Seconds between Nominatim requests. Default 3.0. Do not lower. |
CACHE_FILE |
/data/region_cache.json |
CRON_DAY |
Day of week for scheduled run. Default mon. |
CRON_HOUR |
UTC hour for scheduled run. Default 3. |
STATUS_PORT |
Internal port for Flask status page. Default 80. |
Networks:
adventurelog-internal— direct container-to-container access to AdventureLogtraefik-net— exposes the status page through Traefik
Traefik labels:
- "traefik.enable=true"
- "traefik.http.routers.travel-helper-int.rule=Host(`travel-helper.home.local`)"
- "traefik.http.routers.travel-helper-int.entrypoints=websecure"
- "traefik.http.routers.travel-helper-int.tls=true"
- "traefik.http.routers.travel-helper-int.tls.certresolver=step-ca"
- "traefik.http.services.travel-helper.loadbalancer.server.port=80"
Dependencies
| Service | Role |
|---|---|
adventurelog-web |
AdventureLog Next.js frontend (port 3000) — primary API target |
adventurelog-server |
Django backend — reached indirectly via Next.js proxy |
| Nominatim (external) | Reverse geocoding for region resolution. Rate-limited at 1 req/3s |
| Traefik | Routes travel-helper.home.local to the status page |
URL Resolution (Internal-First)
On each run, Travel Helper probes ADVENTURELOG_API_URLS in order and uses
the first URL that returns HTTP 200, 201, 401, or 403. This means:
http://adventurelog-web:3000is tried first (direct, never leaves host)https://travel.home.localis tried second (through Traefik internally)- The external URL is a last resort only
The resolved URL is shown on the status page. If the internal network is
healthy, you should always see http://adventurelog-web:3000 displayed there.
Nominatim Usage
Nominatim is only called when a Location's region cannot be resolved from:
- The in-memory cache (populated from
region_cache.jsonon startup) - The
regionfield already set on the AdventureLog Location record
On first run, most locations are resolved from the existing geocode_cache.json
(generated during the original Swarm import) without hitting Nominatim at all.
Subsequent runs only call Nominatim for newly imported locations.
Requests use accept-language=en and zoom=10 to return English region names
at state/province level. Rate limiting: 3-second base delay with exponential
backoff (3s → 12s → 48s) on HTTP 429.
Gotchas
-
No trailing slashes on AdventureLog API URLs. AdventureLog returns
308 Permanent Redirectfor URLs ending in/. HTTP clients drop the POST body on 308 redirects, causing silent400 Bad Requestfailures. All endpoints in this service omit trailing slashes. -
adventurelog-weblistens on port 3000, not 8000. Port 8000 is exposed byadventurelog-server(Django), but API calls must go through the Next.js frontend which proxies/api/to the backend. -
Collections must have both
start_dateandend_dateto be processed. Collections with only one date or no dates are silently skipped. -
Visit dates are converted to local timezone before matching. A visit at 11 pm in Toronto is June 22 locally but June 23 in UTC. The visit's IANA
timezonefield is used for this conversion. If the field is absent, UTC is used as a fallback. -
loc_collectionsis maintained in-memory across collections in one run. This prevents overwriting a location's collection list when it matches multiple collections in the same run. -
The
itinerarycontent_type for Location is24. This is an internal AdventureLog FK constant. If a future AdventureLog update changes this, the constantLOCATION_CONTENT_TYPE = 24inmain.pymust be updated. -
API key expiry. The AdventureLog API key can be rotated in AdventureLog under User Settings → API Tokens. Update it in Portainer's environment variable table for the AdventureLog stack — never in any file.
Status Page Features
Available at https://travel-helper.home.local:
| Section | Contents |
|---|---|
| Service Status | Running/Idle badge, resolved AdventureLog URL, last/next run times |
| Last Run Results | Collections matched, locations linked, itinerary entries, Trip Context entries, error count |
| Errors — Last Run | Timestamped list of all ERROR-level log entries from the last run |
| Live Log | Last 80 log lines, newest first, auto-refreshes |
| Run Now button | Triggers an immediate sync in a background thread |
GET /health returns a JSON summary suitable for uptime monitoring.
Maintenance
Rebuilding the image after code changes
docker build -t travel-helper:latest /home/jeeves/docker/adventurelog/travel-helper
Then redeploy the AdventureLog stack in Portainer. Portainer uses the
pre-built travel-helper:latest image — it does not build from source.
Rotating the API key
- Generate a new token in AdventureLog → User Settings → API Tokens
- Update
ADVENTURELOG_API_KEYin the AdventureLog stack's environment variable table in Portainer - Redeploy the stack
Clearing the region cache
Delete /home/jeeves/docker/adventurelog/travel-helper/data/region_cache.json.
The next run will rebuild it from AdventureLog region fields and Nominatim
(expect the run to take longer and make Nominatim calls).
Adjusting the schedule
Change CRON_DAY and CRON_HOUR in Portainer environment variables and
redeploy. Times are UTC.
Checking logs outside the status page
docker logs travel-helper --tail 100 -f
How It Was Built
Travel Helper was developed iteratively during a session in June 2026 as a
companion to the Swarm → AdventureLog bulk import (import_swarm.py). The
original import created ~7,000 Locations and ~10,000 Visits but left
Collections unlinked. Key design decisions:
- Python + APScheduler for the weekly cron, matching the import toolchain
- Flask for the status page, styled to match the Swarm-Reitti Bridge dashboard
- Internal-first URL probing to avoid unnecessary external traffic; the
service lives on
adventurelog-internalso direct container access is always preferred - Full re-scan on every run (idempotent) rather than incremental sync, to avoid state management complexity
region_cache.jsonpre-seeded from the Swarm import'sgeocode_cache.jsonso that ~95% of locations resolve without any Nominatim calls on first run
Last Updated
2026-06-24
h07-finance
00-chapter-intro.md
kstack: book: Centerpoint Home Lab chapter: Finance page: Chapter Introduction tags: [finance, subscriptions, expenses, investments, productivity]
Overview
This chapter covers personal finance and expense-tracking services. These tools handle subscription management, shared expense splitting, and investment portfolio tracking.
Services in This Chapter
| Service | Container(s) | Purpose |
|---|---|---|
| Wallos | wallos |
Subscription and recurring expense tracker |
| SplitPro | splitpro, splitpro-db |
Shared expense splitting (Splitwise-alternative) |
| Ghostfolio | ghostfolio, ghostfolio-postgres, ghostfolio-redis |
Open-source wealth management and portfolio tracker |
Last Updated: 2026-06-17
01-wallos.md
kstack: book: Centerpoint Home Lab chapter: Finance page: Wallos tags: [wallos, subscriptions, finance, sqlite]
Overview
Wallos is a self-hosted subscription and recurring payment tracker. It tracks monthly, annual, and custom-interval subscriptions, calculates total spending, and provides a visual dashboard of recurring costs. Data is stored in a SQLite database. Authentication is Wallos' own built-in user system — no Authentik integration. External-only route.
Access
| Type | URL | Auth |
|---|---|---|
| External | https://wallos.jeevesconsults.ca |
GeoBlock + CrowdSec (own auth) |
No internal home.local route configured.
Configuration
Image: bellamy/wallos:latest
Runtime: PHP 8.3
Traefik Labels
traefik.http.routers.wallos.rule: Host(`wallos.jeevesconsults.ca`)
traefik.http.routers.wallos.entrypoints: websecure
traefik.http.routers.wallos.tls.certresolver: letsencrypt
traefik.http.routers.wallos.middlewares: plex-geoblock@file,crowdsec-bouncer@file,wallos-headers
traefik.http.middlewares.wallos-headers.headers.customrequestheaders.X-Forwarded-Proto: https
traefik.http.services.wallos.loadbalancer.server.port: 80
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/wallos/db |
/var/www/html/db |
SQLite database |
/home/jeeves/docker/wallos/logos |
/var/www/html/images/uploads/logos |
Subscription logos |
Wallos stores all data in a SQLite file within the db bind mount. No external
database dependency.
Notes / Gotchas
- Wallos manages its own user accounts — no SSO or Authentik integration. Credentials are stored in the SQLite database.
- The
X-Forwarded-Proto: httpsmiddleware is required for PHP to detect HTTPS correctly when behind a reverse proxy. - Service logos are persisted in the
logosbind mount. Bothdb/andlogos/must be backed up together to fully restore the instance. - No currency exchange rate API key is required by default — Wallos supports manual rate entry.
Last Updated: 2026-06-17
02-splitpro.md
kstack: book: Centerpoint Home Lab chapter: Finance page: SplitPro tags: [splitpro, expenses, splitting, authentik, postgres, step-ca]
Overview
SplitPro is a self-hosted shared expense splitting app — an open-source
alternative to Splitwise. It tracks group expenses, calculates balances, and
supports multi-currency via the Frankfurter exchange rate provider. Authentik
OIDC provides authentication. Email invites are handled via Gmail SMTP. The
Step-CA root certificate is injected so SplitPro can make HTTPS calls to
internal *.home.local services. External-only route.
Access
| Type | URL | Auth |
|---|---|---|
| External | https://splitpro.jeevesconsults.ca |
Authentik OIDC + GeoBlock + CrowdSec |
SplitPro handles its own OIDC redirect to Authentik — Traefik does not apply
authentik-auth@docker ForwardAuth. Authentication is managed natively by the
Next.js next-auth library.
Containers
| Container | Image | Role |
|---|---|---|
splitpro |
ossapps/splitpro:latest |
Web application |
splitpro-db |
ossapps/postgres:17.7-trixie |
PostgreSQL 17 database |
splitpro (application)
Runtime: Node.js 22 (Next.js)
Key environment variables:
| Variable | Value / Notes |
|---|---|
NEXTAUTH_URL |
https://splitpro.jeevesconsults.ca |
NEXTAUTH_SECRET |
REDACTED |
AUTHENTIK_ID |
xA8pjyG7s4LsnmRKp9Wn9H9v8oUpawBAMvmEF9tT |
AUTHENTIK_SECRET |
REDACTED |
AUTHENTIK_ISSUER |
https://auth.jeevesconsults.ca/application/o/splitpro |
OIDC_ALLOW_DANGEROUS_EMAIL_LINKING |
1 |
DATABASE_URL |
REDACTED (includes PostgreSQL password) |
POSTGRES_USER |
splitpro |
POSTGRES_DB |
splitpro |
POSTGRES_PORT |
5432 |
EMAIL_SERVER_HOST |
smtp.gmail.com |
EMAIL_SERVER_PORT |
587 |
EMAIL_SERVER_USER |
jeeves5454@gmail.com |
EMAIL_SERVER_PASSWORD |
REDACTED |
FROM_EMAIL |
splitpro@jeevesconsults.ca |
ENABLE_SENDING_INVITES |
true |
DISABLE_EMAIL_SIGNUP |
false |
CURRENCY_RATE_PROVIDER |
frankfurter |
DEFAULT_HOMEPAGE |
/balances |
CACHE_RETENTION_INTERVAL |
2 days |
CLEAR_CACHE_CRON_RULE |
0 2 * * 0 (Sunday 2am) |
UPLOAD_MAX_FILE_SIZE_MB |
10 |
NODE_EXTRA_CA_CERTS |
/etc/ssl/certs/step-ca-root.crt |
PORT |
3000 |
Bind mounts:
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/splitpro/uploads |
/app/uploads |
Receipt and document uploads |
/home/jeeves/docker/step-ca/config/certs/root_ca.crt |
/etc/ssl/certs/step-ca-root.crt |
Step-CA root trust injection |
splitpro-db (PostgreSQL 17)
Image: ossapps/postgres:17.7-trixie
A PostgreSQL 17 image published by the SplitPro project (based on the
official postgres:17 Debian image).
| Variable | Value |
|---|---|
POSTGRES_USER |
splitpro |
POSTGRES_DB |
splitpro |
POSTGRES_PASSWORD |
REDACTED |
POSTGRES_PORT |
5432 |
Bind mounts:
| Host Path | Container Path |
|---|---|
/home/jeeves/docker/splitpro/db |
/var/lib/postgresql/data |
Traefik Labels
traefik.http.routers.splitpro-external.rule: Host(`splitpro.jeevesconsults.ca`)
traefik.http.routers.splitpro-external.entrypoints: websecure
traefik.http.routers.splitpro-external.tls.certresolver: letsencrypt
traefik.http.routers.splitpro-external.middlewares: plex-geoblock@file,crowdsec-bouncer@file
traefik.http.routers.splitpro-external.service: splitpro-svc
traefik.http.services.splitpro-svc.loadbalancer.server.port: 3000
Notes / Gotchas
OIDC_ALLOW_DANGEROUS_EMAIL_LINKING: 1allows Authentik OIDC logins to link to existing accounts by matching email address. Required if users were created before OIDC was enabled, or if local signup is still active.NODE_EXTRA_CA_CERTSinjects the Step-CA root certificate into Node.js' TLS trust chain. This is required if SplitPro makes any HTTPS calls to internal*.home.localendpoints (e.g., if integrated with other internal services).DATABASE_URLin the container environment contains the PostgreSQL password in plaintext — treat this as sensitive.- Currency rates are fetched from
api.frankfurter.app(public, no API key required).
Last Updated: 2026-06-17
03-ghostfolio.md
kstack: book: Centerpoint Home Lab chapter: Finance page: Ghostfolio tags: [ghostfolio, investments, portfolio, finance, postgres, redis]
Overview
Ghostfolio is an open-source wealth management and investment portfolio tracker. It aggregates holdings across accounts, tracks performance, and visualises asset allocation. Internal-only access via Step-CA TLS — no external route is configured. Ghostfolio manages its own user authentication. The stack is three containers: the application, a PostgreSQL 15 database, and Redis for caching.
Access
| Type | URL | Auth |
|---|---|---|
| Internal | https://ghostfolio.home.local |
Ghostfolio own auth (Step-CA TLS) |
No external route — accessible only on the LAN.
Containers
| Container | Image | Role |
|---|---|---|
ghostfolio |
ghostfolio/ghostfolio:latest |
Web application |
ghostfolio-postgres |
postgres:15-alpine |
PostgreSQL 15 database |
ghostfolio-redis |
redis:alpine |
Cache and session store |
ghostfolio (application)
Runtime: Node.js 22
Key environment variables:
| Variable | Value / Notes |
|---|---|
DATABASE_URL |
REDACTED (PostgreSQL connection string with password) |
REDIS_HOST |
ghostfolio-redis |
REDIS_PORT |
6379 |
ACCESS_TOKEN_SALT |
REDACTED |
JWT_SECRET_KEY |
REDACTED |
NODE_ENV |
production |
TZ |
America/Toronto |
No bind mounts — application state is stored entirely in PostgreSQL.
ghostfolio-postgres (PostgreSQL 15)
Image: postgres:15-alpine
| Variable | Value |
|---|---|
POSTGRES_DB |
ghostfoliodb |
POSTGRES_USER |
ghostfoliouser |
POSTGRES_PASSWORD |
REDACTED |
TZ |
America/Toronto |
Bind mounts:
| Host Path | Container Path |
|---|---|
/home/jeeves/docker/ghostfolio/postgres_data |
/var/lib/postgresql/data |
ghostfolio-redis
Image: redis:alpine
No authentication configured — network-isolated to the Ghostfolio internal stack network.
Bind mounts:
| Host Path | Container Path |
|---|---|
/home/jeeves/docker/ghostfolio/redis_data |
/data |
Traefik Labels
traefik.http.routers.ghostfolio.rule: Host(`ghostfolio.home.local`)
traefik.http.routers.ghostfolio.entrypoints: websecure
traefik.http.routers.ghostfolio.tls.certresolver: step-ca
traefik.http.services.ghostfolio.loadbalancer.server.port: 3333
Notes / Gotchas
ACCESS_TOKEN_SALTandJWT_SECRET_KEYare critical secrets. If lost, all existing sessions and API tokens will be invalidated and cannot be recovered without reconfiguring Ghostfolio.- Ghostfolio fetches market data from various external providers (Yahoo Finance, Coingecko, etc.). Market data API rate limits may apply depending on configured data sources.
- Portfolio data import supports CSV and JSON formats compatible with common brokerages. Check the Ghostfolio docs for the required format.
- Redis stores only ephemeral cache — data is not critical and does not need to
be backed up. The
postgres_databind mount is the primary backup target. - Port 3333 is the default Ghostfolio application port.
Last Updated: 2026-06-17
h08-utilities
00-chapter-intro.md
kstack: book: Centerpoint Home Lab chapter: Utilities page: Chapter Introduction tags: [utilities, tools, remote-access, file-management]
Overview
This chapter covers general-purpose utility services — remote access, file management, offline content, productivity tooling, and infrastructure support containers.
Services in This Chapter
| Service | Container(s) | Purpose |
|---|---|---|
| Guacamole | guacamole, guacd, guacamole_db |
Clientless remote desktop gateway |
| FileBrowser | filebrowser |
Web-based file manager |
| Kiwix | kiwix |
Offline Wikipedia and ZIM content |
| Draw.io | drawio |
Self-hosted diagram editor |
| IT Tools | it-tools |
Developer and IT utility collection |
| PairDrop | pairdrop |
LAN file sharing (AirDrop-like) |
| ConvertX | convertx |
File format converter |
| MinusPod | minuspod |
AI-powered podcast manager and transcriber |
| Docker Proxy | dockerproxy |
Read-only Docker socket proxy |
Last Updated: 2026-06-17
01-guacamole.md
kstack: book: Centerpoint Home Lab chapter: Utilities page: Apache Guacamole tags: [guacamole, remote-desktop, rdp, vnc, ssh, authentik, postgres]
Overview
Apache Guacamole is a clientless remote desktop gateway. It provides browser-based
access to RDP, VNC, SSH, and Telnet sessions with no client software required.
Authentication is via Authentik OpenID Connect (native Guacamole extension — not
Traefik ForwardAuth). The stack is three containers: the web application, the
guacd native protocol daemon, and PostgreSQL 15 for connection and user data.
Access
| Type | URL | Auth |
|---|---|---|
| External | https://guac.jeeves5454.ddns.net |
Authentik OIDC + GeoBlock + CrowdSec |
| Internal | https://guac.home.local |
Authentik OIDC (Step-CA TLS) |
Guacamole uses its built-in openid extension with EXTENSION_PRIORITY=openid
to handle the Authentik OIDC flow directly. Traefik does not apply
authentik-auth@docker ForwardAuth.
Containers
| Container | Image | Role |
|---|---|---|
guacamole |
guacamole/guacamole:latest |
Web application (Tomcat/Java) |
guacd |
guacamole/guacd:latest |
Native protocol daemon (RDP/VNC/SSH) |
guacamole_db |
postgres:15-alpine |
PostgreSQL database |
guacamole (application)
Runtime: Apache Tomcat / Java. WEBAPP_CONTEXT=ROOT serves the app at /
rather than /guacamole.
Key environment variables:
| Variable | Value / Notes |
|---|---|
GUACD_HOSTNAME |
guacd |
POSTGRESQL_HOSTNAME |
postgres (internal alias for guacamole_db) |
POSTGRESQL_DATABASE |
guacamole_db |
POSTGRESQL_USER |
guacamole_user |
POSTGRESQL_PASSWORD |
REDACTED |
WEBAPP_CONTEXT |
ROOT |
EXTENSION_PRIORITY |
openid |
OPENID_ISSUER |
https://auth.jeevesconsults.ca/application/o/guacamole/ |
OPENID_CLIENT_ID |
A1l7KFyrugknbC8jtFjPxm520XkcNVjGMfTFtJMt |
OPENID_CLIENT_SECRET |
REDACTED |
OPENID_REDIRECT_URI |
https://guac.jeeves5454.ddns.net/ |
OPENID_AUTHORIZATION_ENDPOINT |
https://auth.jeevesconsults.ca/application/o/authorize/ |
OPENID_JWKS_ENDPOINT |
https://auth.jeevesconsults.ca/application/o/guacamole/jwks/ |
OPENID_SCOPE |
openid email profile |
OPENID_USERNAME_CLAIM_TYPE |
preferred_username |
No bind mounts — app state is stored in PostgreSQL.
guacd
The native protocol daemon that handles the actual RDP, VNC, SSH, and Telnet protocol sessions. Guacamole app proxies all protocol traffic through guacd.
Bind mounts:
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/guacamole/drive |
/drive |
Virtual drive for file transfer |
/home/jeeves/docker/guacamole/record |
/record |
Session recordings |
guacamole_db (PostgreSQL 15)
Image: postgres:15-alpine
| Variable | Value |
|---|---|
POSTGRES_DB |
guacamole_db |
POSTGRES_USER |
guacamole_user |
POSTGRES_PASSWORD |
REDACTED |
PGDATA |
/var/lib/postgresql/data/guacamole |
Bind mounts:
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/guacamole/db-data |
/var/lib/postgresql/data |
Database files |
/home/jeeves/docker/guacamole/init |
/docker-entrypoint-initdb.d |
Init SQL scripts |
Traefik Labels
traefik.http.routers.guacamole-external.rule: Host(`guac.jeeves5454.ddns.net`)
traefik.http.routers.guacamole-external.entrypoints: websecure
traefik.http.routers.guacamole-external.tls.certresolver: letsencrypt
traefik.http.routers.guacamole-external.middlewares: plex-geoblock@file,crowdsec-bouncer@file
traefik.http.routers.guacamole-external.service: guac-svc
traefik.http.routers.guacamole-internal.rule: Host(`guac.home.local`)
traefik.http.routers.guacamole-internal.entrypoints: websecure
traefik.http.routers.guacamole-internal.tls.certresolver: step-ca
traefik.http.routers.guacamole-internal.service: guac-svc
traefik.http.services.guac-svc.loadbalancer.server.port: 8080
traefik.docker.network: traefik-net
Notes / Gotchas
EXTENSION_PRIORITY=openidmeans Guacamole's OpenID extension takes precedence over any local user database. If Authentik is down, Guacamole login is unavailable — there is no local admin fallback while OpenID is primary.OPENID_REDIRECT_URImust exactly match the redirect URI registered in Authentik. It points to the external domain (guac.jeeves5454.ddns.net) even when accessing internally — Authentik sends the browser there after authentication.WEBAPP_CONTEXT=ROOTremoves the/guacamolepath prefix. The app responds directly at/. Without this, Traefik would need to strip the prefix or all URLs would be/guacamole/....- Session recordings are stored in the
recordbind mount. Manage disk usage if many long sessions are recorded. - The
initbind mount contains the SQL schema initialisation scripts for the database. These run once on first start. Do not remove them if the container has not yet initialised.
Last Updated: 2026-06-17
02-filebrowser.md
kstack: book: Centerpoint Home Lab chapter: Utilities page: FileBrowser tags: [filebrowser, files, storage, authentik]
Overview
FileBrowser is a web-based file manager that provides browsing, uploading, downloading, and editing of files across multiple mounted host paths. The external route is protected by Authentik ForwardAuth. The internal route requires no additional middleware. Several significant host directories are exposed.
Access
| Type | URL | Auth |
|---|---|---|
| External | https://files.jeeves5454.ddns.net |
Authentik ForwardAuth + GeoBlock + CrowdSec |
| Internal | https://files.home.local |
FileBrowser own auth (Step-CA TLS) |
Configuration
Image: filebrowser/filebrowser:latest
Traefik Labels
traefik.http.routers.filebrowser-external.rule: Host(`files.jeeves5454.ddns.net`)
traefik.http.routers.filebrowser-external.entrypoints: websecure
traefik.http.routers.filebrowser-external.tls.certresolver: letsencrypt
traefik.http.routers.filebrowser-external.middlewares: authentik-auth@docker,plex-geoblock@file,crowdsec-bouncer@file
traefik.http.routers.filebrowser-external.service: filebrowser-svc
traefik.http.routers.filebrowser-internal.rule: Host(`files.home.local`)
traefik.http.routers.filebrowser-internal.entrypoints: websecure
traefik.http.routers.filebrowser-internal.tls.certresolver: step-ca
traefik.http.routers.filebrowser-internal.service: filebrowser-svc
traefik.http.services.filebrowser-svc.loadbalancer.server.port: 80
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/docker/filebrowser/config |
/config |
FileBrowser config file |
/home/jeeves/docker/filebrowser/database |
/database |
FileBrowser database (SQLite) |
/home/jeeves |
/srv/home |
Jeeves home directory |
/media/jeeves/1TB_Vol2/downloads |
/srv/downloads |
Download staging area (Ceph OSD) |
/mnt |
/srv/mnt |
NFS mounts (Multimedia, Photos, data) |
FileBrowser provides access to the full home directory, download staging area, and all NFS mounts. Treat external access with appropriate caution.
Notes / Gotchas
- Authentik ForwardAuth is applied on the external route — external access requires a valid Authentik session before FileBrowser's own login is shown. FileBrowser still maintains its own user database internally.
/mntis exposed as/srv/mnt— this includes all NFS shares from UnRAID (/mnt/Multimedia,/mnt/Photos,/mnt/data). Changes made via FileBrowser are live on the NFS shares.- The SQLite database in
/databasestores FileBrowser users, permissions, and settings. Back it up alongside/config. PUID=1000 / GID=1000— FileBrowser runs as thejeevesuser, so file operations respect the same ownership as files on disk.
Last Updated: 2026-06-17
03-kiwix.md
kstack: book: Centerpoint Home Lab chapter: Utilities page: Kiwix tags: [kiwix, offline, wikipedia, zim, authentik]
Overview
Kiwix serves offline ZIM-format content — primarily Wikipedia snapshots and other reference material — accessible without an internet connection. ZIM files are stored on the Ceph OSD volume. The external route is gated behind Authentik ForwardAuth. The internal route has no additional auth middleware.
Access
| Type | URL | Auth |
|---|---|---|
| External | https://kiwix.jeeves5454.ddns.net |
Authentik ForwardAuth + GeoBlock + CrowdSec |
| Internal | https://kiwix.home.local |
No additional auth (Step-CA TLS) |
Configuration
Image: ghcr.io/kiwix/kiwix-serve:latest
Traefik Labels
traefik.http.routers.kiwix-external.rule: Host(`kiwix.jeeves5454.ddns.net`)
traefik.http.routers.kiwix-external.entrypoints: websecure
traefik.http.routers.kiwix-external.tls.certresolver: letsencrypt
traefik.http.routers.kiwix-external.middlewares: plex-geoblock@file,crowdsec-bouncer@file,authentik-auth@docker
traefik.http.routers.kiwix-external.service: kiwix-svc
traefik.http.routers.kiwix-internal.rule: Host(`kiwix.home.local`)
traefik.http.routers.kiwix-internal.entrypoints: websecure
traefik.http.routers.kiwix-internal.tls.certresolver: step-ca
traefik.http.routers.kiwix-internal.service: kiwix-svc
traefik.http.services.kiwix-svc.loadbalancer.server.port: 8080
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/media/jeeves/1TB_Vol1/Kiwix_Images |
/data |
ZIM content files |
ZIM files are stored on the second Ceph OSD volume (nvme2n1,
/media/jeeves/1TB_Vol1). kiwix-serve auto-discovers all .zim files in
/data on startup and presents them in the library UI.
Notes / Gotchas
- ZIM files can be very large (Wikipedia full English is ~100GB). Ensure adequate space on the Ceph volume before downloading new content.
- New ZIM files can be added by placing them in
/media/jeeves/1TB_Vol1/Kiwix_Images. A container restart is required for kiwix-serve to discover them. - Kiwix has no built-in authentication — the internal route is unprotected on the LAN. The external route relies entirely on Authentik ForwardAuth.
- ZIM files can be downloaded from
library.kiwix.organd transferred to the Ceph volume via FileBrowser or SCP.
Last Updated: 2026-06-17
04-drawio.md
kstack: book: Centerpoint Home Lab chapter: Utilities page: Draw.io tags: [drawio, diagrams, productivity]
Overview
Draw.io (now diagrams.net) is a self-hosted diagramming application for creating network diagrams, flowcharts, architecture diagrams, and more. This instance is stateless — diagrams are saved locally by the browser or to connected storage (Google Drive, OneDrive, etc.), not on the server. External-only access with GeoBlock and CrowdSec.
Access
| Type | URL | Auth |
|---|---|---|
| External | https://drawio.jeeves5454.ddns.net |
GeoBlock + CrowdSec (no auth) |
No authentication middleware — Draw.io is accessible to anyone who bypasses GeoBlock. The app itself has no user accounts.
Configuration
Image: jgraph/drawio:latest
Runtime: Java 11 / Apache Tomcat
Key Environment Variables
| Variable | Value / Notes |
|---|---|
DRAWIO_BASE_URL |
https://drawio.jeeves5454.ddns.net |
PUBLIC_DNS |
drawio.jeeves5454.ddns.net |
DRAWIO_GOOGLE_CLIENT_ID |
jeeves5454@gmail.com (Google integration) |
LETS_ENCRYPT_ENABLED |
false (TLS handled by Traefik) |
Traefik Labels
traefik.http.routers.drawio.rule: Host(`drawio.jeeves5454.ddns.net`)
traefik.http.routers.drawio.entrypoints: websecure
traefik.http.routers.drawio.tls.certresolver: letsencrypt
traefik.http.routers.drawio.middlewares: plex-geoblock@file,crowdsec-bouncer@file,drawio-headers
traefik.http.middlewares.drawio-headers.headers.customrequestheaders.X-Forwarded-Proto: https
traefik.http.services.drawio.loadbalancer.server.port: 8080
Volumes / Bind Mounts
None — Draw.io is completely stateless. All diagram data is stored client-side (browser localStorage or connected cloud storage).
Notes / Gotchas
LETS_ENCRYPT_ENABLED=falseis correct — Traefik handles TLS termination. The Draw.io container itself serves plain HTTP on port 8080.- The
X-Forwarded-Proto: httpsheader is required so Draw.io generates correct HTTPS URLs for embedded resources and exports. - Diagrams are not stored on the server. To preserve diagrams, save them to a
local file (
.drawio/.xml) or configure a cloud storage backend in the Draw.io UI. DRAWIO_GOOGLE_CLIENT_IDenables the Google Drive integration in the file picker. Users must authorise their own Google account in the app.
Last Updated: 2026-06-17
05-it-tools.md
kstack: book: Centerpoint Home Lab chapter: Utilities page: IT Tools tags: [it-tools, developer-tools, utilities]
Overview
IT Tools is a collection of handy online developer and IT utilities — including JWT decoders, UUID generators, hash generators, cron expression parsers, base64 encoders, regex testers, and many more. It is a static React app served by NGINX. Internal-only access. No configuration, no authentication, no persistent state.
Access
| Type | URL | Auth |
|---|---|---|
| Internal | https://ittools.home.local |
None (LAN only) |
Configuration
Image: corentinth/it-tools:latest
Runtime: NGINX 1.26 (static SPA)
Traefik Labels
traefik.http.routers.ittools.rule: Host(`ittools.home.local`)
traefik.http.routers.ittools.entrypoints: websecure
traefik.http.routers.ittools.tls.certresolver: step-ca
traefik.http.services.ittools.loadbalancer.server.port: 80
Volumes / Bind Mounts
None — fully stateless static application.
Notes / Gotchas
- No data leaves the browser — all tools run client-side in JavaScript. Safe to use for sensitive values (passwords, tokens, keys) that should not be sent to external services.
- The tool list expands with each release of the
corentinth/it-toolsimage. Pull the latest tag to get new tools.
Last Updated: 2026-06-17
06-pairdrop.md
kstack: book: Centerpoint Home Lab chapter: Utilities page: PairDrop tags: [pairdrop, file-sharing, webrtc, lan]
Overview
PairDrop is a local network file sharing app inspired by Apple AirDrop. It uses WebRTC for peer-to-peer file transfers between devices on the same LAN session. No accounts, no file storage on the server — files transfer directly between browsers. Internal-only access.
Access
| Type | URL | Auth |
|---|---|---|
| Internal | https://pairdrop.home.local |
None (LAN only) |
Configuration
Image: lscr.io/linuxserver/pairdrop:latest
Environment Variables
| Variable | Value | Purpose |
|---|---|---|
TZ |
America/Toronto |
Timezone |
PUID |
1000 |
User ID |
PGID |
1000 |
Group ID |
WS_FALLBACK |
false |
WebSocket fallback (disabled) |
RATE_LIMIT |
false |
Rate limiting (disabled for LAN) |
RTC_CONFIG |
false |
External STUN/TURN (not needed on LAN) |
DEBUG_MODE |
false |
Debug logging |
Traefik Labels
traefik.http.routers.pairdrop.rule: Host(`pairdrop.home.local`)
traefik.http.routers.pairdrop.entrypoints: websecure
traefik.http.routers.pairdrop.tls.certresolver: step-ca
traefik.http.services.pairdrop.loadbalancer.server.port: 3000
Volumes / Bind Mounts
None — fully stateless. Files are transferred peer-to-peer and never stored on the server.
Notes / Gotchas
- PairDrop discovers peers in the same "room" — by default, all users behind the same NAT/gateway appear to each other. On a home LAN this means all devices on the network can see each other.
RTC_CONFIG=falseis correct for LAN use — no external STUN/TURN server is needed when all peers are on the same network.- For Tailscale peers to use PairDrop, they would need a TURN server since Tailscale creates separate subnets. Current config is LAN-only.
Last Updated: 2026-06-17
07-convertx.md
kstack: book: Centerpoint Home Lab chapter: Utilities page: ConvertX tags: [convertx, file-conversion, utilities]
Overview
ConvertX is a self-hosted file format conversion tool. It supports converting between video, audio, image, document, and other file formats using a web interface. Internal-only access. Account registration is enabled, allowing any LAN user to create an account.
Access
| Type | URL | Auth |
|---|---|---|
| Internal | https://convertx.home.local |
ConvertX own auth (Step-CA TLS) |
Configuration
Image: ghcr.io/c4illin/convertx
Environment Variables
| Variable | Value | Purpose |
|---|---|---|
HTTP_ALLOWED |
true |
Allows HTTP connections (Traefik terminates TLS) |
ACCOUNT_REGISTRATION |
true |
Any LAN user can self-register |
QTWEBENGINE_CHROMIUM_FLAGS |
--no-sandbox |
Required for headless Chromium in container |
Traefik Labels
traefik.http.routers.convertx.rule: Host(`convertx.home.local`)
traefik.http.routers.convertx.entrypoints: websecure
traefik.http.routers.convertx.tls.certresolver: step-ca
traefik.http.services.convertx.loadbalancer.server.port: 3000
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/data/compose/43/data |
/app/data |
App data and SQLite |
The data directory is a Portainer-managed compose path (
/data/compose/<stack-id>/data) — this stack is managed entirely via Portainer with no local compose file. The path is determined by the Portainer stack ID (43).
Notes / Gotchas
ACCOUNT_REGISTRATION=trueallows any user on the LAN to self-register. Disable this if access should be restricted to a single admin account.- Converted files are stored temporarily in the app data directory and deleted after download. Do not use this path for long-term storage.
--no-sandboxis a Chromium security flag required when running in a container (no user namespace isolation). This is standard for containerised headless browser setups.- ConvertX uses FFmpeg, ImageMagick, and other converters internally — available formats depend on the image build.
Last Updated: 2026-06-17
08-minuspod.md
kstack: book: Centerpoint Home Lab chapter: Utilities page: MinusPod tags: [minuspod, podcasts, transcription, whisper, ai, cuda]
Overview
MinusPod is an AI-powered podcast manager and transcriber. It manages podcast feeds, downloads episodes, and transcribes audio using a local Whisper model. Summaries and notes are generated via an LLM (Qwen3:14b) through the Ollama API. The container runs with NVIDIA GPU access (RTX 5080) for accelerated Whisper inference. Internal-only access. Episode data is retained for 180 days.
Access
| Type | URL | Auth |
|---|---|---|
| Internal | https://minuspod.home.local |
MinusPod own auth (Step-CA TLS) |
No external route — LAN access only.
Configuration
Image: ttlequals0/minuspod:2.1.9
Runtime: CUDA 12.9 / NVIDIA runtime (RTX 5080)
Key Environment Variables
| Variable | Value / Notes |
|---|---|
BASE_URL |
https://minuspod.home.local |
WHISPER_BACKEND |
local |
WHISPER_DEVICE |
cuda |
WHISPER_MODEL |
medium |
LLM_PROVIDER |
ollama |
OPENAI_BASE_URL |
https://ollama.home.local/v1 |
OPENAI_MODEL |
qwen3:14b |
OPENAI_API_KEY |
REDACTED |
RETENTION_PERIOD |
4320 hours (180 days) |
MINUSPOD_TRUSTED_PROXY_COUNT |
1 |
NVIDIA_VISIBLE_DEVICES |
all |
NVIDIA_DRIVER_CAPABILITIES |
compute,utility |
Traefik Labels
traefik.http.routers.minuspod.rule: Host(`minuspod.home.local`)
traefik.http.routers.minuspod.entrypoints: websecure
traefik.http.routers.minuspod.tls.certresolver: step-ca
traefik.http.services.minuspod.loadbalancer.server.port: 8000
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/home/jeeves/dockers/minuspod/data |
/app/data |
Episode database and downloads |
Note: The data directory is under
/home/jeeves/dockers/(notdocker/) — this is the actual bind mount path as confirmed bydocker inspect.
AI Integration
MinusPod uses two AI components:
| Component | Model | Transport | Purpose |
|---|---|---|---|
| Whisper | medium |
Local CUDA inference (RTX 5080) | Audio transcription |
| Qwen3:14b | qwen3:14b |
Ollama API at ollama.home.local/v1 |
Summaries and notes |
OPENAI_BASE_URL points to the internal Ollama instance (which exposes an
OpenAI-compatible API). The OPENAI_MODEL is qwen3:14b — this model must be
present in Ollama. Verify with docker exec ollama ollama list.
Notes / Gotchas
- The Whisper
mediummodel on CUDA provides a good balance of speed and accuracy. Upgrade tolarge-v3if accuracy is more important than speed (uses more VRAM). The RTX 5080 has 16GB GDDR7 — ample for either. RETENTION_PERIOD=4320hours = 180 days. Episodes older than this are automatically removed. Adjust if long-term episode storage is needed.MINUSPOD_TRUSTED_PROXY_COUNT=1tells MinusPod to trust one level of proxy headers (Traefik) for correct IP and protocol detection.- The bind mount path is
/home/jeeves/dockers/minuspod/data(note:dockers, notdocker). This is different from the convention used by most other stacks. - Ensure
qwen3:14bis loaded in Ollama before enabling LLM summaries:docker exec ollama ollama pull qwen3:14b
Last Updated: 2026-06-17
09-docker-proxy.md
kstack: book: Centerpoint Home Lab chapter: Utilities page: Docker Socket Proxy tags: [dockerproxy, security, docker, infrastructure]
Overview
The Docker Socket Proxy (dockerproxy) provides a read-only, filtered proxy to
the Docker daemon socket. It exposes a limited subset of the Docker API over TCP,
allowing containers like Homepage to query running container data without
requiring direct access to /var/run/docker.sock. No Traefik route — internal
use only.
Configuration
Image: tecnativa/docker-socket-proxy
Environment Variables (API Permission Flags)
| Variable | Value | Permission granted |
|---|---|---|
CONTAINERS |
1 |
Read container list and inspect data |
EVENTS |
1 |
Subscribe to Docker events stream |
POST |
0 |
Disabled — no write operations |
ALLOW_START |
0 |
Cannot start containers |
ALLOW_STOP |
0 |
Cannot stop containers |
ALLOW_RESTARTS |
0 |
Cannot restart containers |
AUTH |
0 |
No auth endpoint access |
BUILD |
0 |
No build operations |
COMMIT |
0 |
No commit operations |
CONFIGS |
0 |
No config access |
DISTRIBUTION |
0 |
No distribution endpoint access |
All write operations are disabled. The proxy grants read-only container metadata access only.
Volumes / Bind Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
/var/run/docker.sock |
/var/run/docker.sock |
Docker socket (read-only proxy source) |
Consumer
The primary consumer is Homepage (homepage.home.local), which connects to
dockerproxy:2375 to discover running containers and display live service
status widgets. This avoids mounting the Docker socket directly into Homepage.
Notes / Gotchas
- The proxy listens on TCP port 2375 (standard Docker API port) inside the Docker network — it is not exposed on any host port or Traefik route.
- Allowing only
CONTAINERS=1andEVENTS=1means Homepage can list containers and receive real-time status events, but cannot perform any management actions. - If Homepage shows containers as offline or fails to discover services, verify
dockerproxyis running and on the same Docker network as Homepage. - The
tecnativa/docker-socket-proxyimage is the community standard for this pattern. It is based on HAProxy 3.x and routes allowed API paths only.
Last Updated: 2026-06-17