- Add brain router with Authentik forward-auth pointing to LXC nginx on 8082 - Update gharbeia-site-internal from production-1 Docker nginx to LXC nginx on 8083 - Add brain-internal service (10.10.10.29:8082)
55 KiB
- AudioMuse-AI — Sonic Playlist Generator
- [2026-05-17 Sun 17:00] Tube Archivist: gluetun routing, CSRF fix, download path
1|#+TITLE: Infrastructure Documentation — gharbeia.net
2|#+AUTHOR: Amr Gharbeia
3|#+DATE: 2026-05-15
4|
5|* Architecture
6|
7|** Hosts
8|- production-1 (10.10.10.201) :: Docker host, runs all services
9|- Hermes Agent :: Management/automation host
10|
11|** Network
12|- Docker network networking (172.28.10.0/24)
13|- Proxmox VLANs: 1/10/20/30/40/50
14|- Services VLAN: 10.10.10.0/24
15|- Domain: gharbeia.net via Cloudflare (orange cloud/proxied)
16|
17|** External Access Architecture
18|
19|#+BEGIN_EXAMPLE
20|Cloudflare (edge, orange cloud)
21| └─ Cloudflare Tunnel "home" (cloudflared on production-1)
22| └─ Traefik (entrypoint=tunnel, port 8081)
23| ├─ Authentik Forward Auth (external routers)
24| ├─ gharbeia-site (nginx)
25| ├─ jellyfin (SSO via plugin + OIDC)
26| ├─ gitea (native OIDC)
27| └─ *.gharbeia.net services
28|#+END_EXAMPLE
29|
30|** Internal Access Architecture
31|
32|#+BEGIN_EXAMPLE
33|LAN client (browser)
34| └─ Traefik (entrypoint=secureweb, port 443)
35| ├─ Authentik Forward Auth (internal.yaml routers)
36| ├─ gharbeia-site (public, no auth)
37| ├─ jellyfin (SSO via plugin)
38| └─ *.gharbeia.net services
39|
40|Service-to-service / automation / cross-VLAN
41| └─ Traefik (entrypoint=internal, port 8083 — NO auth)
42| └─ Same routing as secureweb, from traefik-internal-noauth.yaml
43|#+END_EXAMPLE
44|
45|Key distinction: :443 = browsers/humans with Authentik auth.
46|=:8083= = runners, automated tooling, services on other VLANs.
47|
48|** Tangle & Deploy Pipeline
49|
50|Changes are made to this org file, tangled into config files by the
51|=tangle-deploy= script on production-1, then deployed via docker compose.
52|
53|#+BEGIN_SRC bash :tangle /docker/compose/infrastructure/tangle-deploy.sh
54|#!/usr/bin/env bash
55|# tangle-deploy — Tangle infrastructure.org and restart affected services
56|GITEA_URL='ssh://git@git.gharbeia.net:2222/amr/infrastructure.git'
57|REPO_DIR="${1:-/docker/compose/infrastructure}"
58|ORG_FILE="${REPO_DIR}/infrastructure.org"
59|if [ -z "${1:-}" ]; then
60| if [ ! -d "$REPO_DIR" ]; then
61| git clone "$GITEA_URL" "$REPO_DIR"
62| else
63| cd "$REPO_DIR" && git pull
64| fi
65|fi
66|if [ ! -f "$ORG_FILE" ]; then
67| echo "ERROR: $ORG_FILE not found in $REPO_DIR"
68| exit 1
69|fi
70|echo "= Tangling $ORG_FILE ==="
71|emacs --batch -Q --load /usr/share/emacs/28.2/lisp/org/org-loaddefs.el \
72| --eval "(require 'org)" \
73| --eval "(org-babel-tangle-file \"$ORG_FILE\")" 2>&1
74|echo "= Restarting services ="
75|cd /docker/compose
76|if [ -f /docker/compose/traefik-static.yaml ] || \
77| [ -f /docker/compose/traefik-internal.yaml ] || \
78| [ -f /docker/compose/traefik-internal-noauth.yaml ] || \
79| [ -f /docker/compose/traefik-dynamic.yaml ]; then
80| echo 'Traefik config changed – restarting…'
81| docker compose up -d traefik
82|fi
83|if [ -f /docker/compose/docker-compose.yaml ]; then
84| echo 'Docker compose changed – restarting all services'
85| docker compose up -d 2>&1 | tail -5
86|fi
87|echo '= Deploy complete ='
88|#+END_SRC
89|
90|The infra-tangle.timer polls the Gitea repo every 5 minutes and runs this
91|script. Pushing to Gitea triggers the pipeline within 5 minutes.
92|
93|* Traefik — Reverse Proxy
94|
95|Traefik is the edge router for all HTTP traffic. It handles TLS termination via
96|Let's Encrypt (DNS-01 challenge through Cloudflare), routes traffic to the right
97|container, and applies middleware chains for auth, security, and rate limiting.
98|
99|Three entrypoints:
100|
101|- tunnel (:8081) :: Receives traffic from the Cloudflare tunnel. All routers
102| here have Authentik Forward Auth.
103|- secureweb (:443) :: Internal LAN traffic with TLS. Also has Authentik
104| Forward Auth for browser access.
105|- internal (:8083) :: Service-to-service and cross-VLAN traffic. No auth.
106| HTTP only. For runners, automation, and API calls that shouldn't hit Authentik.
107|
108|** Static Configuration
109|
110|The static config sets entrypoints, TLS resolvers, providers, and plugins.
111|It is the foundation everything else builds on.
112|
113|#+BEGIN_SRC yaml :tangle /docker/compose/traefik-static.yaml
114|global:
115| checkNewVersion: true
116| sendAnonymousUsage: true
117|
118|log:
119| level: INFO
120|
121|accessLog:
122| filePath: /var/log/access.log
123| format: json
124|
125|api:
126| dashboard: true
127| insecure: true
128|
129|entryPoints:
130| web:
131| address: :80
132| http:
133| redirections:
134| entryPoint:
135| to: secureweb
136| scheme: https
137| permanent: true
138| tunnel:
139| address: :8081
140| secureweb:
141| address: :443
142| http:
143| tls:
144| options: default
145| certResolver: letsencrypt
146| domains:
147| - main: gharbeia.net
148| sans:
149| - "*.gharbeia.net"
150| internal:
151| address: :8083
152| metrics:
153| address: :8082
154|
155|metrics:
156| prometheus:
157| entryPoint: metrics
158| manualRouting: true
159| headerLabels:
160| useragent: User-Agent
161| buckets:
162| - 0.1
163| - 0.3
164| - 1.2
165| - 5.0
166|
167|providers:
168| docker:
169| exposedByDefault: false
170| file:
171| directory: /etc/traefik
172| watch: true
173|
174|certificatesResolvers:
175| letsencrypt:
176| acme:
177| storage: /letsencrypt/acme.json
178| email: gharbeia@riseup.net
179| keyType: EC384
180| caServer: https://acme-v02.api.letsencrypt.org/directory
181| dnsChallenge:
182| provider: cloudflare
183| resolvers:
184| - 1.1.1.1:53
185| - 1.0.0.1:53
186| propagation:
187| delayBeforeChecks: 60s
188|
189|experimental:
190| plugins:
191| crowdsec-bouncer-traefik-plugin:
192| moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
193| version: v1.4.2
194|#+END_SRC
195|
196|Why each piece:
197|- web (:80) exists only to redirect to HTTPS. No TLS.
198|- tunnel (:8081) is inbound-only from cloudflared, never exposed to LAN.
199| Cloudflare handles TLS at the edge, so this can be plain HTTP inside Docker.
200|- secureweb (:443) is the LAN-facing entrypoint with Let's Encrypt certs
201| covering both gharbeia.net and *.gharbeia.net.
202|- internal (:8083) is plain HTTP for service-to-service traffic. TLS overhead
203| is unnecessary on the internal bridge network.
204|- metrics (:8082) exposes Prometheus metrics, manually routed.
205|- dnsChallenge with Cloudflare provider issues wildcard certs. The 60s
206| propagation delay avoids rate-limit issues with Cloudflare's API.
207|
208|** Dynamic Configuration — Middleware
209|
210|Shared middleware used by all routers. Defined once here, referenced by name
211|in every router block.
212|
213|#+BEGIN_SRC yaml :tangle /docker/compose/traefik-dynamic.yaml
214|http:
215| middlewares:
216|
217| authentik-forwardauth:
218| forwardAuth:
219| address: http://authentik-server:9000/outpost.goauthentik.io/auth/traefik
220| trustForwardHeader: true
221| authResponseHeaders:
222| - X-authentik-username
223| - X-authentik-groups
224| - X-authentik-email
225| - X-authentik-name
226| - X-authentik-uid
227|
228| security-headers:
229| headers:
230| customFrameOptionsValue: SAMEORIGIN
231| contentTypeNosniff: true
232| browserXssFilter: true
233| referrerPolicy: no-referrer
234| permissionsPolicy: ""
235| customResponseHeaders:
236| X-Robots-Tag: "noindex, nofollow"
237| Server: ""
238|
239| traefik-bouncer:
240| plugin:
241| crowdsec-bouncer-traefik-plugin:
242| enabled: "true"
243| crowdsecMode: live
244| crowdsecLapiKey: CROWDSEC_LAPI_KEY
245| crowdsecLapiHost: crowdsec:8080
246| crowdsecLapiScheme: http
247| updateFrequencySec: 5
248| defaultDecisionLifetimeSec: 60
249|
250| compress:
251| compress:
252| excludedContentTypes:
253| - text/event-stream
254|
255| ratelimit:
256| rateLimit:
257| average: 100
258| burst: 50
259|#+END_SRC
260|
261|The auth flow: Authentik's outpost runs as a sidecar inside the authentik
262|container that validates session cookies. When a request lacks a valid session,
263|Traefik redirects to the Authentik login page. After login, Authentik redirects
264|back to the original URL with a session cookie.
265|
266|=security-headers= locks down XSS, clickjacking, and fingerprinting. The empty
267|=permissionsPolicy= disables all browser APIs by default.
268|
269|=traefik-bouncer= runs CrowdSec's LAPI bouncer as a Traefik plugin. IPs flagged
270|by CrowdSec get blocked. The LAPI key is a placeholder – fill from vault.
271|
272|** Internal Routers — Authenticated (secureweb :443)
273|
274|These routers serve LAN browser traffic. All have Authentik Forward Auth.
275|Backend services are referenced by Docker DNS name on the networking bridge.
276|
277|#+BEGIN_SRC yaml :tangle /docker/compose/traefik-internal.yaml
278|http:
279| routers:
280|
281| # – Media & Streaming —————————————–
282|
283| jellyfin:
284| rule: "Host(`jellyfin.gharbeia.net`)"
285| service: jellyfin-internal
286| entryPoints:
287| - secureweb
288| tls:
289| certResolver: letsencrypt
290| middlewares:
291| - authentik-forwardauth@file
292| - security-headers@file
293| - traefik-bouncer@file
294|
295| jellyseerr:
296| rule: "Host(`jellyseerr.gharbeia.net`)"
297| service: jellyseerr-internal
298| entryPoints:
299| - secureweb
300| tls:
301| certResolver: letsencrypt
302| middlewares:
303| - authentik-forwardauth@file
304| - security-headers@file
305| - traefik-bouncer@file
306|
307| # – *arr Suite ————————————————-
308|
309| radarr:
310| rule: "Host(`radarr.gharbeia.net`)"
311| service: radarr-internal
312| entryPoints:
313| - secureweb
314| tls:
315| certResolver: letsencrypt
316| middlewares:
317| - authentik-forwardauth@file
318| - security-headers@file
319| - traefik-bouncer@file
320|
321| sonarr:
322| rule: "Host(`sonarr.gharbeia.net`)"
323| service: sonarr-internal
324| entryPoints:
325| - secureweb
326| tls:
327| certResolver: letsencrypt
328| middlewares:
329| - authentik-forwardauth@file
330| - security-headers@file
331| - traefik-bouncer@file
332|
333| lidarr:
334| rule: "Host(`lidarr.gharbeia.net`)"
335| service: lidarr-internal
336| entryPoints:
337| - secureweb
338| tls:
339| certResolver: letsencrypt
340| middlewares:
341| - authentik-forwardauth@file
342| - security-headers@file
343| - traefik-bouncer@file
344|
345| prowlarr:
346| rule: "Host(`prowlarr.gharbeia.net`)"
347| service: prowlarr-internal
348| entryPoints:
349| - secureweb
350| tls:
351| certResolver: letsencrypt
352| middlewares:
353| - authentik-forwardauth@file
354| - security-headers@file
355| - traefik-bouncer@file
356|
357| whisparr:
358| rule: "Host(`whisparr.gharbeia.net`)"
359| service: whisparr-internal
360| entryPoints:
361| - secureweb
362| tls:
363| certResolver: letsencrypt
364| middlewares:
365| - authentik-forwardauth@file
366| - security-headers@file
367| - traefik-bouncer@file
368|
369| mylar:
370| rule: "Host(`mylar.gharbeia.net`)"
371| service: mylar-internal
372| entryPoints:
373| - secureweb
374| tls:
375| certResolver: letsencrypt
376| middlewares:
377| - authentik-forwardauth@file
378| - security-headers@file
379| - traefik-bouncer@file
380|
381| lazylibrarian:
382| rule: "Host(`lazylibrarian.gharbeia.net`)"
383| service: lazylibrarian-internal
384| entryPoints:
385| - secureweb
386| tls:
387| certResolver: letsencrypt
388| middlewares:
389| - authentik-forwardauth@file
390| - security-headers@file
391| - traefik-bouncer@file
392|
393| # – Downloaders ————————————————
394|
395| sabnzbd:
396| rule: "Host(`sabnzbd.gharbeia.net`)"
397| service: sabnzbd-internal
398| entryPoints:
399| - secureweb
400| tls:
401| certResolver: letsencrypt
402| middlewares:
403| - authentik-forwardauth@file
404| - security-headers@file
405| - traefik-bouncer@file
406|
407| qbittorrent:
408| rule: "Host(`qbittorrent.gharbeia.net`)"
409| service: qbittorrent-internal
410| entryPoints:
411| - secureweb
412| tls:
413| certResolver: letsencrypt
414| middlewares:
415| - authentik-forwardauth@file
416| - security-headers@file
417| - traefik-bouncer@file
418|
419| flaresolverr:
420| rule: "Host(`flaresolverr.gharbeia.net`)"
421| service: flaresolverr-internal
422| entryPoints:
423| - secureweb
424| tls:
425| certResolver: letsencrypt
426| middlewares:
427| - authentik-forwardauth@file
428| - security-headers@file
429| - traefik-bouncer@file
430|
431| # – Homepage / Dashboards ————————————–
432|
433| homepage:
434| rule: "Host(`homepage.gharbeia.net`)"
435| service: homepage-internal
436| entryPoints:
437| - secureweb
438| tls:
439| certResolver: letsencrypt
440| middlewares:
441| - authentik-forwardauth@file
442| - security-headers@file
443| - traefik-bouncer@file
444|
445| homarr:
446| rule: "Host(`homarr.gharbeia.net`)"
447| service: homarr-internal
448| entryPoints:
449| - secureweb
450| tls:
451| certResolver: letsencrypt
452| middlewares:
453| - authentik-forwardauth@file
454| - security-headers@file
455| - traefik-bouncer@file
456|
457| heimdall:
458| rule: "Host(`heimdall.gharbeia.net`)"
459| service: heimdall-internal
460| entryPoints:
461| - secureweb
462| tls:
463| certResolver: letsencrypt
464| middlewares:
465| - authentik-forwardauth@file
466| - security-headers@file
467| - traefik-bouncer@file
468|
469| # – Monitoring ————————————————
470|
471| grafana:
472| rule: "Host(`grafana.gharbeia.net`)"
473| service: grafana-internal
474| entryPoints:
475| - secureweb
476| tls:
477| certResolver: letsencrypt
478| middlewares:
479| - authentik-forwardauth@file
480| - security-headers@file
481| - traefik-bouncer@file
482|
483| prometheus:
484| rule: "Host(`prometheus.gharbeia.net`)"
485| service: prometheus-internal
486| entryPoints:
487| - secureweb
488| tls:
489| certResolver: letsencrypt
490| middlewares:
491| - authentik-forwardauth@file
492| - security-headers@file
493| - traefik-bouncer@file
494|
495| # – Website (public, no auth) ———————————-
496|
497| gharbeia-site:
498| rule: "Host(`gharbeia.net`) || Host(`www.gharbeia.net`)"
499| service: gharbeia-site-internal
500| entryPoints:
501| - secureweb
502| tls:
503| certResolver: letsencrypt
504|
505| # – Brain Knowledge Base (private, behind Authentik) ————
506|
507| brain:
508| rule: "Host(`brain.gharbeia.net`)"
509| service: brain-internal
510| entryPoints:
511| - secureweb
512| tls:
513| certResolver: letsencrypt
514| middlewares:
515| - authentik-forwardauth@file
516| - security-headers@file
517|
518| # – Management ————————————————
506|
507| gitea:
508| rule: "Host(`git.gharbeia.net`)"
509| service: gitea-internal
510| entryPoints:
511| - secureweb
512| tls:
513| certResolver: letsencrypt
514| middlewares:
515| - security-headers@file
516| - traefik-bouncer@file
517| # No authentik-forwardauth – Gitea has native OIDC
518|
519| portainer:
520| rule: "Host(`portainer.gharbeia.net`)"
521| service: portainer-internal
522| entryPoints:
523| - secureweb
524| tls:
525| certResolver: letsencrypt
526| middlewares:
527| - authentik-forwardauth@file
528| - security-headers@file
529| - traefik-bouncer@file
530|
531| authentik:
532| rule: "Host(`auth.gharbeia.net`)"
533| service: authentik-internal
534| entryPoints:
535| - secureweb
536| tls:
537| certResolver: letsencrypt
538| middlewares:
539| - security-headers@file
540| - traefik-bouncer@file
541| # No authentik-forwardauth – otherwise auth loops
542|
543| headscale:
544| rule: "Host(`headscale.gharbeia.net`)"
545| service: headscale-internal
546| entryPoints:
547| - secureweb
548| tls:
549| certResolver: letsencrypt
550| middlewares:
551| - security-headers@file
552| - traefik-bouncer@file
553| # No authentik-forwardauth – Tailscale clients need direct access
554|
555| headplane:
556| rule: "Host(`headplane.gharbeia.net`) && PathPrefix(`/admin/`)"
557| service: headplane-internal
558| entryPoints:
559| - secureweb
560| tls:
561| certResolver: letsencrypt
562| middlewares:
563| - authentik-forwardauth@file
564| - security-headers@file
565| - traefik-bouncer@file
566|
567| ddns-updater:
568| rule: "Host(`ddns-updater.gharbeia.net`)"
569| service: ddns-updater-internal
570| entryPoints:
571| - secureweb
572| tls:
573| certResolver: letsencrypt
574| middlewares:
575| - authentik-forwardauth@file
576| - security-headers@file
577| - traefik-bouncer@file
578|
579| audiobookshelf:
580| rule: "Host(`audiobookshelf.gharbeia.net`)"
581| service: audiobookshelf-internal
582| entryPoints:
583| - secureweb
584| tls:
585| certResolver: letsencrypt
586| middlewares:
587| - authentik-forwardauth@file
588| - security-headers@file
589| - traefik-bouncer@file
590|
591| guacamole:
592| rule: "Host(`guacamole.gharbeia.net`)"
593| service: guacamole-internal
594| entryPoints:
595| - secureweb
596| tls:
597| certResolver: letsencrypt
598| middlewares:
599| - authentik-forwardauth@file
600| - security-headers@file
601| - traefik-bouncer@file
602|
603| tubearchivist:
604| rule: "Host(`tubearchivist.gharbeia.net`)"
605| service: tubearchivist-internal
606| entryPoints:
607| - secureweb
608| tls:
609| certResolver: letsencrypt
610| middlewares:
611| - authentik-forwardauth@file
612| - security-headers@file
613| - traefik-bouncer@file
614|
615| traefik-dashboard:
616| rule: "Host(`traefik.gharbeia.net`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
617| service: traefik-dashboard-internal
618| entryPoints:
619| - secureweb
620| tls:
621| certResolver: letsencrypt
622| middlewares:
623| - authentik-forwardauth@file
624| - security-headers@file
625| - traefik-bouncer@file
626|
627| services:
628| jellyfin-internal:
629| loadBalancer:
630| servers:
631| - url: http://gluetun:8096
632| jellyseerr-internal:
633| loadBalancer:
634| servers:
635| - url: http://gluetun:5055
636| radarr-internal:
637| loadBalancer:
638| servers:
639| - url: http://gluetun:7878
640| sonarr-internal:
641| loadBalancer:
642| servers:
643| - url: http://gluetun:8989
644| lidarr-internal:
645| loadBalancer:
646| servers:
647| - url: http://gluetun:8686
648| prowlarr-internal:
649| loadBalancer:
650| servers:
651| - url: http://gluetun:9696
652| whisparr-internal:
653| loadBalancer:
654| servers:
655| - url: http://gluetun:6969
656| mylar-internal:
657| loadBalancer:
658| servers:
659| - url: http://gluetun:8090
660| lazylibrarian-internal:
661| loadBalancer:
662| servers:
663| - url: http://gluetun:5299
664| sabnzbd-internal:
665| loadBalancer:
666| servers:
667| - url: http://gluetun:8080
668| qbittorrent-internal:
669| loadBalancer:
670| servers:
671| - url: http://gluetun:8200
672| flaresolverr-internal:
673| loadBalancer:
674| servers:
675| - url: http://gluetun:8191
676| homepage-internal:
677| loadBalancer:
678| servers:
679| - url: http://homepage:3000
680| homarr-internal:
681| loadBalancer:
682| servers:
683| - url: http://homarr:7575
684| heimdall-internal:
685| loadBalancer:
686| servers:
687| - url: http://heimdall:80
688| grafana-internal:
689| loadBalancer:
690| servers:
691| - url: http://grafana:3000
692| prometheus-internal:
693| loadBalancer:
694| servers:
695| - url: http://prometheus:9090
696| gharbeia-site-internal:
697| loadBalancer:
698| servers:
699| - url: http://10.10.10.29:8083
700| brain-internal:
701| loadBalancer:
702| servers:
703| - url: "http://10.10.10.29:8082"
704| gitea-internal:
701| loadBalancer:
702| servers:
703| - url: http://gitea:3000
704| portainer-internal:
705| loadBalancer:
706| servers:
707| - url: http://portainer:9000
708| authentik-internal:
709| loadBalancer:
710| servers:
711| - url: http://authentik:9000
712| headscale-internal:
713| loadBalancer:
714| servers:
715| - url: http://headscale:8080
716| headplane-internal:
717| loadBalancer:
718| servers:
719| - url: http://headplane:3000
720| ddns-updater-internal:
721| loadBalancer:
722| servers:
723| - url: http://ddns-updater:8310
724| audiobookshelf-internal:
725| loadBalancer:
726| servers:
727| - url: http://gluetun:80 # audiobookshelf on port 80 inside gluetun
728| guacamole-internal:
729| loadBalancer:
730| servers:
731| - url: http://guacamole:8080
732| traefik-dashboard-internal:
733| loadBalancer:
734| servers:
735| - url: http://traefik:8080
736| tubearchivist-internal:
737| loadBalancer:
738| servers:
739| - url: http://gluetun:8000 # tubearchivist on port 8000 inside gluetun
740|#+END_SRC
741|
742|All 28 routers follow the same pattern. The service URLs point to Docker DNS
743|names on the networking bridge. Services behind Gluetun VPN aren't on
744|the bridge network — they use network_mode: service:gluetun and are
745|reached via http://gluetun:<port> instead of http://servicename:<port>.
746|
747|** Internal Routers — No Auth (internal :8083)
748|
749|An identical set of routers without the authentik-forwardauth middleware.
750|Used by service-to-service traffic, Gitea runner, and cross-VLAN automation.
751|Generated by stripping the auth middleware from traefik-internal.yaml.
752|
753|#+BEGIN_SRC yaml :tangle /docker/compose/traefik-internal-noauth.yaml
754|# This file is maintained manually as a copy of traefik-internal.yaml
755|# with all authentik-forwardauth middleware references removed.
756|# See: docker/appdata/traefik/internal-noauth.yaml for the production copy.
757|#+END_SRC
758|
759|** Authentication Architecture
760|
761|Three authentication mechanisms depending on the service type:
762|
763|*** Forward Auth (default for web-only services)
764|
765|Traefik middleware intercepts every request and redirects unauthenticated
766|users to the Authentik login page. After login, Authentik sets a session
767|cookie that passes subsequent checks transparently.
768|
769|Used by: all *arr, dashboards, monitoring, Portainer, Guacamole, etc.
770|Limitation: only works in browsers — native/TV apps can't use Forward Auth.
771|
772|*** Native OIDC / SSO (for services with apps)
773|
774|Services that have native mobile or TV apps need real OIDC integration so
775|the app can authenticate directly via a browser-based login flow.
776|
777|- Gitea: configured with Authentik OIDC provider in Gitea's admin panel.
778| Users log in via "Sign in with Authentik" button on the Gitea login page.
779| Existing user accounts are linked by username match.
780|
781|- Jellyfin: uses the SSO-Auth plugin (v4.0.0.4) with an Authentik OIDC
782| provider (client_id = jellyfin-sso). The plugin does a two-step flow:
783| 1. OIDC callback returns an HTML page with JavaScript + authorization state
784| 2. JavaScript POSTs to /sso/OID/Auth/Authentik to complete the login
785|
786| Critical detail: Jellyfin must trust Traefik's X-Forwarded-Proto header
787| or the JavaScript will construct URLs with http:// instead of https://.
788| This is configured via KnownProxies in /config/network.xml:
789|
790| #+BEGIN_SRC xml
791| <KnownProxies>
792| <string>172.28.10.0/24</string>
793| <string>172.28.10.4</string>
794| </KnownProxies>
795| #+END_SRC
796|
797| Without this, GetRequestBase() returns http://jellyfin.gharbeia.net and
798| the iframe, auth POST, and final redirect all use the wrong scheme.
799|
800|*** Local users (TV apps, fallback)
801|
802|TV apps (Android TV, webOS, Tizen) often can't complete the OIDC JavaScript
803|two-step flow inside their embedded browser. For these, create a dedicated
804|Jellyfin local user (e.g. tv) with a simple password. The app logs in
805|directly with password — no SSO involved. The user keeps library access
806|without losing admin history/settings.
807|
808|This pattern applies to any service where native app SSO doesn't work:
809|create a local service account, use it for app access, keep SSO for browsers.
810|
811|The entire stack runs on production-1 using Docker Compose. Services are
812|split into individual YAML fragment files under /docker/compose/services/,
813|referenced by the master compose via include: directives.
814|
815|This splitting has three benefits:
816|1. Each service is self-contained with its own prose documentation
817|2. Adding or removing a service is a single line in the master compose
818|3. Differences between deployments (e.g. test vs production) are just different
819| include lists
820|
821|** Master Compose
822|
823|The master compose defines the shared network and includes all service fragments.
824|It is the single entry point for docker compose commands.
825|
826|#+BEGIN_SRC yaml :tangle /docker/compose/docker-compose.yaml
827|networks:
828| networking:
829| name: networking
830| driver: bridge
831| ipam:
832| driver: default
833| config:
834| - subnet: ${DOCKER_SUBNET:?err}
835| gateway: ${DOCKER_GATEWAY:?err}
836|
837|include:
838| - services/gluetun.yaml
839| - services/postgresql.yaml
840| - services/valkey.yaml
841| - services/authentik.yaml
842| - services/authentic-worker.yaml
843| - services/traefik.yaml
844| - services/traefik-certs-dumper.yaml
845| - services/crowdsec.yaml
846| - services/gitea.yaml
847| - services/runner.yaml
848| - services/cloudflared.yaml
849| - services/gharbeia-site.yaml
850| - services/unbound.yaml
851| - services/homepage.yaml
852| - services/homarr.yaml
853| - services/heimdall.yaml
854| - services/grafana.yaml
855| - services/prometheus.yaml
856| - services/headscale.yaml
857| - services/tailscale.yaml
858| - services/headplane.yaml
859| - services/ddns-updater.yaml
860| - services/portainer.yaml
861| - services/guacamole.yaml
862| - services/guacd.yaml
863| - services/unpackerr.yaml
864| - services/bazarr.yaml
865| - services/flaresolverr.yaml
866| - services/jellyfin.yaml
867| - services/jellyseerr.yaml
868| - services/lazylibrarian.yaml
869| - services/lidarr.yaml
870| - services/mylar.yaml
871| - services/prowlarr.yaml
872| - services/qbittorrent.yaml
873| - services/radarr.yaml
874| - services/sabnzbd.yaml
875| - services/sonarr.yaml
876| - services/stash.yaml
877| - services/tdarr.yaml
878| - services/tdarr-node.yaml
879| - services/tubearchivist.yaml
- services/audiobookshelf.yaml
- services/audiomuse.yaml
- services/whisparr.yaml
882|#+END_SRC
883|
884|All 44 services are organized alphabetically by category in the include list.
885|The order matters for startup dependencies: infrastructure services (gluetun,
886|postgresql, valkey, authentik, traefik) come first.
887|
888|** Jellyfin — Media Server
889|
890|Jellyfin serves media libraries through the browser and native apps. It runs
891|in Gluetun's network namespace (VPN-routed), uses Authentik SSO for browser
892|login, and supports local user accounts for TV apps.
893|
894|*** KnownProxies (Critical for SSO)
895|
896|Jellyfin sits behind Traefik reverse proxy. Without KnownProxies, Jellyfin
897|doesn't trust X-Forwarded-Proto: https, so the SSO plugin's JavaScript
898|flow constructs URLs with HTTP instead of HTTPS. This file must match the
899|runtime config at /docker/appdata/jellyfin/network.xml.
900|
901|#+BEGIN_SRC xml :tangle /docker/appdata/jellyfin/network.xml
902|<?xml version="1.0" encoding="utf-8"?>
903|<NetworkConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
904| <BaseUrl />
905| <EnableHttps>false</EnableHttps>
906| <RequireHttps>false</RequireHttps>
907| <CertificatePath />
908| <CertificatePassword />
909| <InternalHttpPort>8096</InternalHttpPort>
910| <InternalHttpsPort>8920</InternalHttpsPort>
911| <PublicHttpPort>8096</PublicHttpPort>
912| <PublicHttpsPort>8920</PublicHttpsPort>
913| <AutoDiscovery>true</AutoDiscovery>
914| <EnableUPnP>true</EnableUPnP>
915| <EnableIPv4>true</EnableIPv4>
916| <EnableIPv6>false</EnableIPv6>
917| <EnableRemoteAccess>true</EnableRemoteAccess>
918| <LocalNetworkSubnets />
919| <LocalNetworkAddresses />
920| <KnownProxies>
921| <string>172.28.10.0/24</string>
922| <string>172.28.10.4</string>
923| </KnownProxies>
924| <IgnoreVirtualInterfaces>true</IgnoreVirtualInterfaces>
925| <VirtualInterfaceNames>
926| <string>veth</string>
927| </VirtualInterfaceNames>
928| <EnablePublishedServerUriByRequest>false</EnablePublishedServerUriByRequest>
929| <PublishedServerUriBySubnet />
930| <RemoteIPFilter />
931| <IsRemoteIPFilterBlacklist>false</IsRemoteIPFilterBlacklist>
932|</NetworkConfiguration>
933|#+END_SRC
934|
935|*** SSO-Auth Plugin Configuration
936|
937|The SSO plugin config lives at /docker/appdata/jellyfin/plugins/configurations/SSO-Auth.xml.
938|Key settings:
939|- OIDC provider pointing to https://auth.gharbeia.net/application/o/jellyfin-sso
940|- EnableAuthorization=false (bypasses group-based role checking)
941|- EnableAllFolders=true (all libraries accessible to SSO users)
942|- Scopes: openid profile email groups
943|
944|With EnableAuthorization=false, any Authentik user can log in to Jellyfin
945|via SSO. Admin rights are managed within Jellyfin itself.
946|
947|** Gluetun — VPN Client
948|
949|Gluetun is the VPN gateway for all media-related traffic. Services that need
950|VPN routing use network_mode: service:gluetun to share its network namespace.
951|This means their traffic exits through the VPN tunnel, not the host's public IP.
952|
953|Key architectural decisions:
954|- All VPN-routed services share Gluetun's port mappings (configured on Gluetun)
955|- extra_hosts resolves *.gharbeia.net to 10.10.10.201 so VPN-routed services
956| can reach Traefik without leaking DNS
957|- The FIREWALL_OUTBOUND_SUBNETS allows LAN access through the VPN
958|
959|#+BEGIN_SRC yaml :tangle /docker/compose/services/gluetun.yaml
960|services:
961| gluetun:
962| image: qmcgaw/gluetun:latest
963| container_name: gluetun
964| restart: always
965| cap_add:
966| - NET_ADMIN
967| devices:
968| - /dev/net/tun:/dev/net/tun
969| ports:
970| - 8888:8888/tcp
971| - 8388:8388/tcp
972| - 8388:8388/udp
973| - ${GLUETUN_CONTROL_PORT:?err}:${GLUETUN_CONTROL_PORT:?err}
974| - ${WEBUI_PORT_AUDIOBOOKSHELF:?err}:80
975| - ${WEBUI_PORT_BAZARR:?err}:6767
976| - ${WEBUI_PORT_FILEBOT:?err}:5454
977| - ${WEBUI_PORT_HUNTARR:?err}:9705
978| - ${WEBUI_PORT_JELLYFIN:?err}:8096
979| - ${WEBUI_PORT_JELLYSEERR:?err}:5055
980| - ${WEBUI_PORT_LAZYLIBRARIAN:?err}:5299
981| - ${WEBUI_PORT_LIDARR:?err}:8686
982| - ${WEBUI_PORT_MYLAR:?err}:8090
983| - ${WEBUI_PORT_PROWLARR:?err}:9696
984| - ${WEBUI_PORT_RADARR:?err}:7878
985| - ${WEBUI_PORT_READARR:?err}:8787
986| - ${WEBUI_PORT_SABNZBD:?err}:8080
987| - ${WEBUI_PORT_SONARR:?err}:8989
988| - ${WEBUI_PORT_STASH:?err}:7777
989| - ${WEBUI_PORT_WHISPARR:?err}:6969
990| - ${WEBUI_PORT_QBITTORRENT:?err}:${WEBUI_PORT_QBITTORRENT:?err}
991| - ${QBIT_PORT:?err}:6881
992| - ${FLARESOLVERR_PORT:?err}:8191
993| - ${TDARR_SERVER_PORT:?err}:${TDARR_SERVER_PORT:?err}
994| - ${WEBUI_PORT_TDARR:?err}:${WEBUI_PORT_TDARR:?err}
995| - ${WEBUI_PORT_PLEX:?err}:32400
996| - ${WEBUI_PORT_TUBEARCHIVIST:-8000}:8000
997| - 8324:8324
998| - 32410:32410/udp
999| - 32412:32412/udp
1000| - 32413:32413/udp
1001| - 32414:32414/udp
1002| - 32469:32469
1003| extra_hosts:
1004| - ${CLOUDFLARE_DNS_ZONE}:${LOCAL_DOCKER_IP}
1005| - "*.${CLOUDFLARE_DNS_ZONE}:${LOCAL_DOCKER_IP}"
1006| volumes:
1007| - ${FOLDER_FOR_DATA:?err}/gluetun:/gluetun
1008| environment:
1009| - PUID=${PUID:?err}
1010| - PGID=${PGID:?err}
1011| - UMASK=${UMASK:?err}
1012| - TZ=${TIMEZONE:?err}
1013| - VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER:?err}
1014| - OPENVPN_USER=${VPN_USERNAME:?err}
1015| - OPENVPN_PASSWORD=${VPN_PASSWORD:?err}
1016| - SERVER_COUNTRIES=${SERVER_COUNTRIES}
1017| - SERVER_REGIONS=${SERVER_REGIONS}
1018| - SERVER_CITIES=${SERVER_CITIES}
1019| - SERVER_HOSTNAMES=${SERVER_HOSTNAMES}
1020| - SERVER_CATEGORIES=${SERVER_CATEGORIES}
1021| - FIREWALL_OUTBOUND_SUBNETS=${LOCAL_SUBNET:?err}
1022| - OPENVPN_CUSTOM_CONFIG=${OPENVPN_CUSTOM_CONFIG}
1023| - HTTP_CONTROL_SERVER_ADDRESS=:${GLUETUN_CONTROL_PORT:?err}
1024| - VPN_TYPE=${VPN_TYPE}
1025| - VPN_ENDPOINT_IP=${VPN_ENDPOINT_IP}
1026| - VPN_ENDPOINT_PORT=${VPN_ENDPOINT_PORT}
1027| - WIREGUARD_PUBLIC_KEY=${WIREGUARD_PUBLIC_KEY}
1028| - WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
1029| - WIREGUARD_PRESHARED_KEY=${WIREGUARD_PRESHARED_KEY}
1030| - WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
1031| - HTTPPROXY=on
1032| - SHADOWSOCKS=on
1033| networks:
1034| - networking
1035|#+END_SRC
1036|
1037|** Authentik — Identity Provider
1038|
1039|Authentik provides universal authentication for all web services. It acts as
1040|both the SSO login page (via Traefik Forward Auth) and the OIDC provider for
1041|services that support it natively (Gitea, Jellyfin via plugin).
1042|
1043|The stack has two containers:
1044|- =authentik= (server) — handles login flows, session management, policies
1045|- =authentic-worker= — background tasks, outpost management
1046|
1047|Both connect to the same Postgres and Valkey databases.
1048|
1049|#+BEGIN_SRC yaml :tangle /docker/compose/services/authentik.yaml
1050|services:
1051| authentik:
1052| image: ghcr.io/goauthentik/server:${AUTHENTIK_VERSION:?err}
1053| container_name: authentik
1054| restart: unless-stopped
1055| networks:
1056| - networking
1057| user: ${PUID:?err}:${PGID:?err}
1058| command: server
1059| environment:
1060| - TZ=${TIMEZONE:?err}
1061| - AUTHENTIK_LOG_LEVEL=info
1062| - AUTHENTIK_SECRET_KEY=${AUTHENTIK_SECRET_KEY:?err}
1063| - AUTHENTIK_REDIS__HOST=valkey
1064| - AUTHENTIK_POSTGRESQL__HOST=postgresql
1065| - AUTHENTIK_POSTGRESQL__NAME=${AUTHENTIK_DATABASE:?err}
1066| - AUTHENTIK_POSTGRESQL__USER=${POSTGRESQL_USERNAME:?err}
1067| - AUTHENTIK_POSTGRESQL__PASSWORD=${POSTGRESQL_PASSWORD:?err}
1068| - AUTHENTIK_ERROR_REPORTING__ENABLED=false
1069| - AUTHENTIK_EMAIL__HOST=${EMAIL_SERVER_HOST}
1070| - AUTHENTIK_EMAIL__PORT=${EMAIL_SERVER_PORT}
1071| - AUTHENTIK_EMAIL__USERNAME=${EMAIL_ADDRESS}
1072| - AUTHENTIK_EMAIL__PASSWORD=${EMAIL_PASSWORD}
1073| - AUTHENTIK_EMAIL__USE_TLS=true
1074| - AUTHENTIK_EMAIL__USE_SSL=false
1075| - AUTHENTIK_EMAIL__FROM=${EMAIL_SENDER}
1076| - AUTHENTIK_EMAIL__TIMEOUT=15
1077| volumes:
1078| - ${FOLDER_FOR_DATA:?err}/authentik/media:/media
1079| - ${FOLDER_FOR_DATA:?err}/authentik/templates:/templates
1080| ports:
1081| - ${WEBUI_PORT_AUTHENTIK:?err}:9000
1082| depends_on:
1083| postgresql:
1084| condition: service_healthy
1085| restart: true
1086| valkey:
1087| condition: service_healthy
1088| restart: true
1089| labels:
1090| - traefik.enable=true
1091| - traefik.http.routers.authentik.service=authentik
1092| - traefik.http.routers.authentik.rule=Host(`auth.${CLOUDFLARE_DNS_ZONE:?err}`)
1093| - traefik.http.routers.authentik.entrypoints=secureweb
1094| - traefik.http.routers.authentik.middlewares=security-headers@file,traefik-bouncer@file
1095| - traefik.http.services.authentik.loadbalancer.server.scheme=http
1096| - traefik.http.services.authentik.loadbalancer.server.port=9000
1097|#+END_SRC
1098|
1099|** Gitea — Git Hosting
1100|
1101|Gitea hosts the infrastructure repo and triggers the tangle-deploy pipeline.
1102|The runner connects via the authless internal entrypoint (:8083) so it can
1103|check out repos without SSO interference.
1104|
1105|#+BEGIN_SRC yaml :tangle /docker/compose/services/gitea.yaml
1106|services:
1107| gitea:
1108| image: docker.gitea.com/gitea:1.25.5
1109| container_name: gitea
1110| restart: always
1111| networks:
1112| - networking
1113| environment:
1114| - USER_UID=1000
1115| - USER_GID=1000
1116| volumes:
1117| - /docker/appdata/gitea:/data
1118| - /memex:/memex
1119| - /etc/timezone:/etc/timezone:ro
1120| - /etc/localtime:/etc/localtime:ro
1121| ports:
1122| - "3001:3000"
1123| - "2222:22"
1124| labels:
1125| - traefik.enable=true
1126| - traefik.http.routers.gitea.service=gitea
1127| - traefik.http.routers.gitea.rule=Host(`git.${CLOUDFLARE_DNS_ZONE:?err}`)
1128| - traefik.http.routers.gitea.entrypoints=tunnel
1129| - traefik.http.routers.gitea.middlewares=security-headers@file,traefik-bouncer@file
1130| - traefik.http.services.gitea.loadbalancer.server.scheme=http
1131| - traefik.http.services.gitea.loadbalancer.server.port=3000
1132|#+END_SRC
1133|
1134|** Infrastructure Services
1135|
1136|Core data and networking services that everything depends on.
1137|
1138|*** Postgresql
1139|#+BEGIN_SRC yaml :tangle /docker/compose/services/postgresql.yaml
1140|services:
1141| postgresql:
1142| image: docker.io/library/postgres:17
1143| container_name: postgresql
1144| restart: unless-stopped
1145| networks:
1146| - networking
1147| user: ${PUID:?err}:${PGID:?err}
1148| ports:
1149| - ${POSTGRESQL_PORT:?err}:5432
1150| healthcheck:
1151| test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
1152| start_period: 20s
1153| interval: 30s
1154| retries: 5
1155| timeout: 5s
1156| volumes:
1157| - ${FOLDER_FOR_DATA:?err}/postgresql:/var/lib/postgresql/data
1158| environment:
1159| - TZ=${TIMEZONE:?err}
1160| - POSTGRES_DB=${AUTHENTIK_DATABASE:?err}
1161| - POSTGRES_USER=${POSTGRESQL_USERNAME:?err}
1162| - POSTGRES_PASSWORD=${POSTGRESQL_PASSWORD:?err}
1163|#+END_SRC
1164|
1165|*** Valkey (Redis Alternative)
1166|#+BEGIN_SRC yaml :tangle /docker/compose/services/valkey.yaml
1167|services:
1168| valkey:
1169| image: valkey/valkey:alpine
1170| container_name: valkey
1171| restart: unless-stopped
1172| networks:
1173| - networking
1174| command: --save 60 1 --loglevel warning
1175| user: ${PUID:?err}:${PGID:?err}
1176| ports:
1177| - ${VALKEY_PORT:?err}:6379
1178| healthcheck:
1179| test: ["CMD-SHELL", "valkey-cli ping | grep PONG"]
1180| start_period: 20s
1181| interval: 30s
1182| retries: 5
1183| timeout: 3s
1184| volumes:
1185| - ${FOLDER_FOR_DATA:?err}/valkey:/data
1186|#+END_SRC
1187|
1188|*** Unbound — DNS Resolver
1189|#+BEGIN_SRC yaml :tangle /docker/compose/services/unbound.yaml
1190|services:
1191| unbound:
1192| image: mvance/unbound:latest
1193| container_name: unbound
1194| restart: unless-stopped
1195| networks:
1196| - networking
1197| ports:
1198| - 53:53/tcp
1199| - 53:53/udp
1200| volumes:
1201| - /docker/appdata/unbound/unbound.conf:/opt/unbound/etc/unbound/unbound.conf:ro
1202|#+END_SRC
1203|
1204|** Tube Archivist — YouTube Archiving
1205|
1206|Tube Archivist downloads and indexes YouTube channels, playlists, and
1207|videos. Full-text search, metadata browsing, subscription management.
1208|
1209|The stack has three containers:
1210|- tubearchivist (main app) — Django web UI on port 8000
1211|- tubearchivist-es — Elasticsearch 8.17 for metadata + search
1212|- tubearchivist-redis — Redis for Celery task queue
1213|
1214|Tube Archivist routes through Gluetun VPN to avoid YouTube geo-blocking.
1215|
1216|#+BEGIN_SRC yaml :tangle /docker/compose/services/tubearchivist.yaml
services:
tubearchivist:
image: bbilly1/tubearchivist:latest
container_name: tubearchivist
restart: unless-stopped
depends_on:
gluetun:
condition: service_healthy
restart: true
network_mode: service:gluetun
volumes:
- ${FOLDER_FOR_MORE:?err}/media/youtube:/youtube
- ${FOLDER_FOR_DATA:?err}/tubearchivist/cache:/cache
environment:
- TZ=${TIMEZONE:?err}
- TA_USERNAME=${TA_USERNAME:?err}
- TA_PASSWORD=${TA_PASSWORD:?err}
- ES_URL=http://tubearchivist-es:9200
- REDIS_CON=redis://tubearchivist-redis:6379
- TA_HOST=https://tubearchivist.gharbeia.net
- ELASTIC_PASSWORD=tubearchivist
- HOST_UID=${PUID:?err}
- HOST_GID=${PGID:?err}
labels:
- traefik.enable=true
- traefik.http.routers.tubearchivist.service=tubearchivist
- traefik.http.routers.tubearchivist.rule=Host(`tubearchivist.${CLOUDFLARE_DNS_ZONE:?err}`)
- traefik.http.routers.tubearchivist.entrypoints=tunnel
- traefik.http.routers.tubearchivist.middlewares=authentik-forwardauth@file,security-headers@file,traefik-bouncer@file
- traefik.http.services.tubearchivist.loadbalancer.server.scheme=http
- traefik.http.services.tubearchivist.loadbalancer.server.port=8000
tubearchivist-es: image: docker.elastic.co/elasticsearch/elasticsearch:8.17.0 container_name: tubearchivist-es restart: unless-stopped networks:
- networking
environment:
- discovery.type=single-node
- ES_JAVA_OPTS=-Xms512m -Xmx512m
- xpack.security.enabled=false
- path.repo=/usr/share/elasticsearch/data/snapshot
volumes:
- ${FOLDER_FOR_DATA:?err}/tubearchivist/es:/usr/share/elasticsearch/data
healthcheck: test: curl -s http://localhost:9200/_cluster/health | grep -vq '"status":"red"' interval: 30s timeout: 10s retries: 3
tubearchivist-redis: image: redis:7-alpine container_name: tubearchivist-redis restart: unless-stopped networks:
- networking
command: –save 60 1 –loglevel warning volumes:
- ${FOLDER_FOR_DATA:?err}/tubearchivist/redis:/data
healthcheck: test: redis-cli ping | grep PONG interval: 30s timeout: 10s retries: 3
#+END_SRC
1293|
1294|** Remaining Services
1295|
1296|The following services follow the same pattern as those documented above.
1297|Each is a YAML fragment in /docker/compose/services/ with its container
1298|definition, environment, volumes, and Traefik labels.
1299|
1300|- traefik.yaml — Reverse proxy (documented above)
1301|- traefik-certs-dumper.yaml — Export Let's Encrypt certs for other services
1302|- crowdsec.yaml — Intrusion prevention (blocks malicious IPs via Traefik bouncer)
1303|- cloudflared.yaml — Cloudflare Tunnel client
1304|- gharbeia-site.yaml — Static website via nginx
1305|- homepage.yaml, homarr.yaml, heimdall.yaml — Dashboard UIs
1306|- grafana.yaml, prometheus.yaml — Monitoring stack
1307|- headscale.yaml, tailscale.yaml, headplane.yaml — Wireguard mesh VPN
1308|- ddns-updater.yaml — Dynamic DNS
1309|- portainer.yaml — Docker GUI
1310|- guacamole.yaml, guacd.yaml — Remote desktop gateway
1311|- unpackerr.yaml — Archive extraction for *arr downloads
1312|- runner.yaml — Gitea Actions runner
1313|- bazarr.yaml, flaresolverr.yaml — Subtitle downloader, Cloudflare bypass
1314|- jellyfin.yaml, jellyseerr.yaml — Media server + request manager
1315|- lazylibrarian.yaml, lidarr.yaml, mylar.yaml — Ebook, music, comic managers
1316|- prowlarr.yaml, radarr.yaml, sonarr.yaml, whisparr.yaml — *arr indexer + library managers
1317|- qbittorrent.yaml, sabnzbd.yaml — Torrent and usenet clients
1318|- stash.yaml — Adult content library manager
1319|- tdarr.yaml, tdarr-node.yaml — Media transcoding automation
1320|- tubearchivist.yaml — YouTube archiving (Tube Archivist)
1321|- audiobookshelf.yaml — Audiobook and podcast server
AudioMuse-AI — Sonic Playlist Generator
AudioMuse-AI performs sonic analysis on music files to auto-generate playlists for Jellyfin. Runs as Flask app + RQ worker, connects to existing PostgreSQL and Valkey. On the bridge network (not gluetun).
services:
audiomuse-ai:
image: ghcr.io/neptunehub/audiomuse-ai:latest
container_name: audiomuse-ai
restart: unless-stopped
networks:
- networking
ports:
- ${WEBUI_PORT_AUDIOMUSE:-8005}:8000
environment:
SERVICE_TYPE: "flask"
TZ: ${TIMEZONE:?err}
POSTGRES_USER: ${POSTGRESQL_USERNAME:?err}
POSTGRES_PASSWORD: ${POSTGRESQL_PASSWORD:?err}
POSTGRES_DB: audiomusedb
POSTGRES_HOST: postgresql
POSTGRES_PORT: "5432"
REDIS_URL: redis://valkey:6379/0
TEMP_DIR: /app/temp_audio
FRONTEND_PORT: "8000"
volumes:
- ${FOLDER_FOR_MEDIA:?err}:/library
- ${FOLDER_FOR_MORE:?err}:/more
- temp-audio-flask:/app/temp_audio
labels:
- traefik.enable=true
- traefik.http.routers.audiomuse.service=audiomuse
- traefik.http.routers.audiomuse.rule=Host(`audiomuse.${CLOUDFLARE_DNS_ZONE:?err}`)
- traefik.http.routers.audiomuse.entrypoints=tunnel,web,secureweb
- traefik.http.routers.audiomuse.middlewares=authentik-forwardauth@file,security-headers@file
- traefik.http.services.audiomuse.loadbalancer.server.scheme=http
- traefik.http.services.audiomuse.loadbalancer.server.port=8000
audiomuse-worker:
image: ghcr.io/neptunehub/audiomuse-ai:latest
container_name: audiomuse-worker
restart: unless-stopped
networks:
- networking
environment:
SERVICE_TYPE: "worker"
TZ: ${TIMEZONE:?err}
POSTGRES_USER: ${POSTGRESQL_USERNAME:?err}
POSTGRES_PASSWORD: ${POSTGRESQL_PASSWORD:?err}
POSTGRES_DB: audiomusedb
POSTGRES_HOST: postgresql
POSTGRES_PORT: "5432"
REDIS_URL: redis://valkey:6379/0
TEMP_DIR: /app/temp_audio
volumes:
- ${FOLDER_FOR_MEDIA:?err}:/library
- ${FOLDER_FOR_MORE:?err}:/more
- temp-audio-worker:/app/temp_audio
volumes:
temp-audio-flask:
temp-audio-worker:
[2026-05-17 Sun 17:00] Tube Archivist: gluetun routing, CSRF fix, download path
- TA moved to network_mode: service:gluetun (port 8000 mapped on gluetun)
- TA_HOST changed to https:// prefix to fix CSRF on POST api/playlist
- REDIS_CON fixed (was corrupted with embedded Traefik labels)
- Download folder changed to ${FOLDER_FOR_MORE}/media/youtube
- Stash got ${FOLDER_FOR_MORE}:/more volume mount
- Jellyfin TubeArchivist plugin configured (http://localhost:8000, user amr)
- Backup files moved to docker/compose/off
- Critical: recreating gluetun orphans all network_mode: service:gluetun containers
(jellyfin, *arr, sabnzbd, etc.) — ALL must be recreated after any gluetun change
1344|
1345|** [2026-05-15 Thu 09:30] Jellyfin SSO fixed — KnownProxies and Two-Step Flow
1346|- Root cause: Jellyfin's empty KnownProxies caused SSO plugin to use HTTP
1347| base URL, breaking the JavaScript two-step auth flow (iframe/POST/redirect)
1348|- Fix: Added 172.28.10.0/24 and 172.28.10.4 to Jellyfin's KnownProxies
1349|- Created jellyfin_admin Authentik group + linked user amr to it
1350|- Set EnableAuthorization=false in SSO-Auth plugin config
1351|- Documented three authentication mechanisms: Forward Auth, OIDC/SSO, local users
1352|- Fixed ASCII tree diagrams by wrapping in #+BEGIN_EXAMPLE blocks
1353|- Fixed trees rendering issue: Unicode box-drawing chars must be inside
1354| example blocks in org-mode, otherwise font rendering mangles them
1355|
1356|** [2026-05-15 Thu 06:40] Pipeline fixed — Emacs path and auth
1357|- Fixed Emacs org-loaddefs.el path in tangle-deploy
1358|- Created Gitea access token for git operations
1359|- Replaced Gitea Action workflow with systemd timer
1360|- tangle-deploy now pulls, tangles, and restarts on a 5-minute timer
1361|
1362|** [2026-05-15 Thu 06:50] Monolith split into modular compose
1363|- 42 service fragments created under docker/compose/services
1364|- Master docker-compose.yaml uses include: directives (43 services total)
1365|- All service labels and env vars preserved from original monolith
1366|- Compose validated with –env-file .env, all 43 services resolve
1367|- Deployment verified: all containers running
1368|- Orphaned unbound container absorbed into compose (was started manually)
1369|
1370|** [2026-05-15 Thu 03:47] Literate infrastructure established
1371|- infrastructure.org becomes the source of truth – all config files are
1372| tangle targets embedded as
#+BEGIN_SRCblocks with absolute paths 1373|-tangle-deployscript installed at/usr/local/bin/tangle-deployon 1374| production-1; run after git push to regenerate configs and restart services 1375|- Gitea repo:git@git.gharbeia.net:amr/infrastructure.git1376| 1377|** [2026-05-15 Thu 03:07] Internal entrypoint and Gitea runner 1378|- Created internal entrypoint on port 8083 for service-to-service traffic 1379|- Updated Gitea runner URL to use internal entrypoint 1380|- Documented three-path architecture 1381| 1382|** [2026-05-15 Thu 02:56] Static site and Error 1033 fix 1383|- Added gharbeia-site nginx container for root domain 1384|- Fixed CNAME record for bare domain pointing to correct tunnel 1385| 1386|** [2026-05-15 Thu 02:40] Jellyfin SSO and infrastructure.org 1387|- Configured Jellyfin SSO-Auth plugin with Authentik OIDC 1388|- Removed Forward Auth from Jellyfin Traefik labels 1389|- Created infrastructure.org as source of truth 1390|- Added Forward Auth to internal LAN routers 1391|