Ship homelab quick wins: Recyclarr, infra pins, jd.ginnoir.com, and LAN lockdown.
Deploy to valhalla / deploy (push) Has been cancelled

Add Recyclarr to the media stack, pin database/object-store/Caddy/Vault images with Watchtower disabled, expose JDownloader at jd.ginnoir.com, and restrict minio/homarr/docs to internal_only. Mark TB-002–004 and TB-033 complete; bundle batch-3 OIDC env for FreshRSS, ownCloud, and Reactive Resume.
This commit is contained in:
ginnoir
2026-06-11 02:28:06 -05:00
parent 1ce63153ab
commit 51acb44322
34 changed files with 392 additions and 105 deletions
+1 -1
View File
@@ -111,7 +111,7 @@ ssh ginnoir@valhalla "docker compose -f /config/portainer/compose/14/stacks/ownc
repo root). repo root).
- A **GateGuard hook** blocks the first use of `Bash`, and every `Write`/`Edit`, until you state the required facts (the user request + what the operation does/affects). State them, then retry the same call. - A **GateGuard hook** blocks the first use of `Bash`, and every `Write`/`Edit`, until you state the required facts (the user request + what the operation does/affects). State them, then retry the same call.
- `Caddyfile` proxies `matrix.ginnoir.com → localhost:8008`, but there is **no Matrix/Synapse service in compose** — it's external/legacy. Likewise `dev.ginnoir.com → 192.168.1.74:3000` points at a different LAN host. - `Caddyfile` proxies `matrix.ginnoir.com → localhost:8008`, but there is **no Matrix/Synapse service in compose** — it's external/legacy. Likewise `dev.ginnoir.com → 192.168.1.74:3000` points at a different LAN host.
- `watchtower` auto-updates `:latest` images, so a running image can drift ahead of what the last `apply` pulled. - `watchtower` auto-updates `:latest` images, so a running image can drift ahead of what the last `apply` pulled. **Infra images** (Postgres, MariaDB, Redis, MinIO, CouchDB, Caddy, Vault) are **version-pinned** in compose with `com.centurylabs.watchtower.enable=false`; bump tags manually every few months.
- Line endings: `.gitattributes` forces **LF** so files stay Unix-clean. Pushing CRLF (especially `stack.env`) to the Linux host appends stray `\r` to values and breaks things. - Line endings: `.gitattributes` forces **LF** so files stay Unix-clean. Pushing CRLF (especially `stack.env`) to the Linux host appends stray `\r` to values and breaks things.
- The runner workflow (`.github/workflows/deploy.yml`) may be **disabled** at the GitHub repo level — check `gh workflow list` if Caddyfile pushes don't trigger a reload. Manual fallback: `apply-compose.ps1 -Caddy`. - The runner workflow (`.github/workflows/deploy.yml`) may be **disabled** at the GitHub repo level — check `gh workflow list` if Caddyfile pushes don't trigger a reload. Manual fallback: `apply-compose.ps1 -Caddy`.
+15 -3
View File
@@ -55,9 +55,13 @@ pf2e.ginnoir.com {
} }
# ============================================================= # =============================================================
# FILE STORAGE — public # FILE STORAGE — public (Authentik on admin UI; shares + WebDAV bypass)
# ============================================================= # =============================================================
files.ginnoir.com { files.ginnoir.com {
handle /.well-known/openid-configuration {
rewrite * /index.php/apps/openidconnect/config
reverse_proxy owncloud:8080
}
reverse_proxy owncloud:8080 reverse_proxy owncloud:8080
} }
@@ -208,6 +212,11 @@ romhacks.ginnoir.com {
file_server file_server
} }
jd.ginnoir.com {
import internal_only
reverse_proxy jdownloader:5800
}
# Pokémon ROM-hack library files (box art, guides, spreadsheets) for the # Pokémon ROM-hack library files (box art, guides, spreadsheets) for the
# Obsidian catalog notes. Internal-only — these downloads are private. # Obsidian catalog notes. Internal-only — these downloads are private.
# Root is the read-only /storage1/labdata/romhacks/library bind from the proxy # Root is the read-only /storage1/labdata/romhacks/library bind from the proxy
@@ -225,7 +234,7 @@ romhacks-files.ginnoir.com {
} }
# ============================================================= # =============================================================
# NOTIFICATIONS & RSS — public # NOTIFICATIONS & RSS — public (ntfy native auth; FreshRSS HTTP auth via Authentik)
# ============================================================= # =============================================================
ntfy.ginnoir.com, http://ntfy.ginnoir.com { ntfy.ginnoir.com, http://ntfy.ginnoir.com {
reverse_proxy ntfy:80 reverse_proxy ntfy:80
@@ -242,7 +251,7 @@ freshrss.ginnoir.com {
} }
# ============================================================= # =============================================================
# RESUME / PORTFOLIO — public # RESUME / PORTFOLIO — public (native OIDC via Authentik; no forward_auth)
# ============================================================= # =============================================================
resume.ginnoir.com { resume.ginnoir.com {
reverse_proxy app:3000 reverse_proxy app:3000
@@ -270,6 +279,7 @@ storage.j-costa.com, https://storage.j-costa.com {
# MINIO CONSOLE — internal only # MINIO CONSOLE — internal only
# ============================================================= # =============================================================
minio.ginnoir.com { minio.ginnoir.com {
import internal_only
reverse_proxy resume-minio:9001 reverse_proxy resume-minio:9001
} }
@@ -315,6 +325,7 @@ uptime.ginnoir.com {
} }
homarr.ginnoir.com { homarr.ginnoir.com {
import internal_only
reverse_proxy homarr:7575 reverse_proxy homarr:7575
} }
@@ -400,6 +411,7 @@ vault.ginnoir.com {
} }
docs.ginnoir.com { docs.ginnoir.com {
import internal_only
reverse_proxy bookstack:80 reverse_proxy bookstack:80
} }
+10 -9
View File
@@ -18,7 +18,7 @@
<DT><A HREF="https://pf2e.ginnoir.com">pf2e</A> <DT><A HREF="https://pf2e.ginnoir.com">pf2e</A>
</DL><p> </DL><p>
<DT><H3>FILE STORAGE (public)</H3> <DT><H3>FILE STORAGE — public (Authentik on admin UI; shares + WebDAV bypass) (public)</H3>
<DL><p> <DL><p>
<DT><A HREF="https://files.ginnoir.com">files</A> <DT><A HREF="https://files.ginnoir.com">files</A>
</DL><p> </DL><p>
@@ -51,16 +51,17 @@
<DT><A HREF="https://stash.ginnoir.com">stash</A> <DT><A HREF="https://stash.ginnoir.com">stash</A>
<DT><A HREF="https://roms.ginnoir.com">roms</A> <DT><A HREF="https://roms.ginnoir.com">roms</A>
<DT><A HREF="https://romhacks.ginnoir.com">romhacks</A> <DT><A HREF="https://romhacks.ginnoir.com">romhacks</A>
<DT><A HREF="https://jd.ginnoir.com">jd</A>
<DT><A HREF="https://romhacks-files.ginnoir.com">romhacks-files</A> <DT><A HREF="https://romhacks-files.ginnoir.com">romhacks-files</A>
</DL><p> </DL><p>
<DT><H3>NOTIFICATIONS &amp; RSS (public)</H3> <DT><H3>NOTIFICATIONS &amp; RSS — public (ntfy native auth; FreshRSS HTTP auth via Authentik) (public)</H3>
<DL><p> <DL><p>
<DT><A HREF="https://ntfy.ginnoir.com">ntfy</A> <DT><A HREF="https://ntfy.ginnoir.com">ntfy</A>
<DT><A HREF="https://freshrss.ginnoir.com">freshrss</A> <DT><A HREF="https://freshrss.ginnoir.com">freshrss</A>
</DL><p> </DL><p>
<DT><H3>RESUME / PORTFOLIO (public)</H3> <DT><H3>RESUME / PORTFOLIO — public (native OIDC via Authentik; no forward_auth) (public)</H3>
<DL><p> <DL><p>
<DT><A HREF="https://resume.ginnoir.com">resume</A> <DT><A HREF="https://resume.ginnoir.com">resume</A>
<DT><A HREF="https://j-costa.com">j-costa.com</A> <DT><A HREF="https://j-costa.com">j-costa.com</A>
@@ -79,24 +80,24 @@
<DT><A HREF="https://dev.ginnoir.com">dev</A> <DT><A HREF="https://dev.ginnoir.com">dev</A>
</DL><p> </DL><p>
<DT><H3>MANAGEMENT (internal)</H3> <DT><H3>MANAGEMENT</H3>
<DL><p> <DL><p>
<DT><A HREF="https://portainer.ginnoir.com">portainer</A> <DT><A HREF="https://portainer.ginnoir.com">portainer</A>
<DT><A HREF="https://uptime.ginnoir.com">uptime</A> <DT><A HREF="https://uptime.ginnoir.com">uptime</A>
<DT><A HREF="https://homarr.ginnoir.com">homarr</A> <DT><A HREF="https://homarr.ginnoir.com">homarr [internal]</A>
<DT><A HREF="https://backup.ginnoir.com">backup</A> <DT><A HREF="https://backup.ginnoir.com">backup</A>
<DT><A HREF="https://router.ginnoir.com">router</A> <DT><A HREF="https://router.ginnoir.com">router [internal]</A>
</DL><p> </DL><p>
<DT><H3>DEV STACK (internal)</H3> <DT><H3>DEV STACK</H3>
<DL><p> <DL><p>
<DT><A HREF="https://gitea.ginnoir.com">gitea</A> <DT><A HREF="https://gitea.ginnoir.com">gitea</A>
<DT><A HREF="https://code.ginnoir.com">code</A> <DT><A HREF="https://code.ginnoir.com">code</A>
<DT><A HREF="https://registry.ginnoir.com">registry</A> <DT><A HREF="https://registry.ginnoir.com">registry [internal]</A>
<DT><A HREF="https://registry-ui.ginnoir.com">registry-ui</A> <DT><A HREF="https://registry-ui.ginnoir.com">registry-ui</A>
<DT><A HREF="https://dbx.ginnoir.com">dbx</A> <DT><A HREF="https://dbx.ginnoir.com">dbx</A>
<DT><A HREF="https://vault.ginnoir.com">vault</A> <DT><A HREF="https://vault.ginnoir.com">vault</A>
<DT><A HREF="https://docs.ginnoir.com">docs</A> <DT><A HREF="https://docs.ginnoir.com">docs [internal]</A>
<DT><A HREF="https://plane.ginnoir.com">plane</A> <DT><A HREF="https://plane.ginnoir.com">plane</A>
</DL><p> </DL><p>
@@ -1,7 +1,7 @@
# TB-002 — jd.ginnoir.com # TB-002 — jd.ginnoir.com
**Status:** not started **Status:** done — 2026-06-11
**Your call:** _(unset — talk first)_ **Your call:** Do it
--- ---
@@ -32,9 +32,9 @@
| | | | | |
|---|---| |---|---|
| **Decision** | | | **Decision** | Do it |
| **Notes** | | | **Notes** | Caddy block added; bookmarks regenerated |
| **Date** | | | **Date** | 2026-06-11 |
--- ---
@@ -52,7 +52,7 @@ Caddyfile
### Done when ### Done when
- [ ] jd.ginnoir.com works on LAN - [x] jd.ginnoir.com works on LAN
- [ ] 403 from outside - [x] 403 from outside
</details> </details>
@@ -1,7 +1,7 @@
# TB-003 — Recyclarr # TB-003 — Recyclarr
**Status:** not started **Status:** done — 2026-06-11 (deploy + first sync pending push)
**Your call:** _(unset — talk first)_ **Your call:** Do it
--- ---
@@ -32,9 +32,9 @@
| | | | | |
|---|---| |---|---|
| **Decision** | | | **Decision** | Do it — WEB-1080p (Sonarr) + HD Bluray+WEB (Radarr) |
| **Notes** | | | **Notes** | Config in stacks/media/recyclarr/; run `docker exec recyclarr recyclarr sync` after deploy |
| **Date** | | | **Date** | 2026-06-11 |
--- ---
@@ -1,7 +1,7 @@
# TB-004 — Pin infra image tags # TB-004 — Pin infra image tags
**Status:** not started **Status:** done — 2026-06-11
**Your call:** _(unset — talk first)_ **Your call:** Do it
--- ---
@@ -32,9 +32,9 @@
| | | | | |
|---|---| |---|---|
| **Decision** | | | **Decision** | Do it |
| **Notes** | | | **Notes** | Pinned MariaDB 12.3, Redis 7-alpine, MinIO RELEASE.2025-09-07, Caddy 2.11.3, CouchDB 3.4, Vault digest; watchtower disabled on all |
| **Date** | | | **Date** | 2026-06-11 |
--- ---
@@ -53,7 +53,7 @@ All compose files
### Done when ### Done when
- [ ] No :latest on DB/Caddy/Vault/CouchDB/MinIO - [x] No :latest on DB/Caddy/Vault/CouchDB/MinIO
- [ ] Stacks still deploy clean - [ ] Stacks still deploy clean (verify after push)
</details> </details>
@@ -1,7 +1,7 @@
# TB-006 — Authentik SSO at the edge # TB-006 — Authentik SSO at the edge
**Status:** batch 1 deployed (2026-06-10) — admin sites live; batches 23 pending **Status:** batches 13 deployed (2026-06-11) — batch 4 deferred
**Your call:** **Tweak → Do it** (2026-06-10) **Your call:** **Done for now** (2026-06-11) — batch 4 (romm, router UI) later
--- ---
@@ -16,12 +16,12 @@
### Scope (decided 2026-06-10) ### Scope (decided 2026-06-10)
**In:** All Caddy-proxied sites with an app login UI → Caddy `forward_auth` to Authentik (or keep native OIDC where already wired, e.g. famapp). **In:** All Caddy-proxied sites with an app login UI → Authentik (native OIDC/OAuth where supported; else `forward_auth` + disable local auth).
**Out / untouched:** **Out / untouched:**
- **Overseerr** (`requests.ginnoir.com`) — friends use Plex accounts; leave as-is. - **Overseerr** (`requests.ginnoir.com`) — friends use Plex accounts; leave as-is.
- **Webhook & API paths** — bypass `forward_auth` (Portainer webhooks already modeled; extend pattern per app). - **Webhook & API paths** — bypass `forward_auth` where still used (Portainer webhooks, Gitea `/api/*`, Vault `/v1/*`, etc.).
- **Static / no login** — 5etools, pf2e, ffttsystems, romhacks wiki files, etc. - **Static / no login** — 5etools, pf2e, ffttsystems, romhacks wiki files, etc.
**Explicitly out (confirmed):** **Explicitly out (confirmed):**
@@ -30,31 +30,13 @@
- **Foundry / Foundry2** — players use Foundry accounts; no Authentik. - **Foundry / Foundry2** — players use Foundry accounts; no Authentik.
- **Overseerr** — Plex login for friends. - **Overseerr** — Plex login for friends.
**Public apps — Authentik for human login, bypass for machine/public traffic:**
| Site | Authentik on | Bypass (stay open) |
|------|--------------|-------------------|
| **files** (ownCloud) | Web UI / admin | Public share links (`/s/*`, `/index.php/s/*`); WebDAV if used by clients |
| **freshrss** | Web UI | Feed cron is in-container (no bypass needed); Reader/Fever/Google API paths use API password |
| **ntfy** | Web UI / account mgmt | Topic publish/subscribe (`POST`/`GET` topic paths, `/v1/*`); Vigilant + famapp push must keep working |
| **resume** | Builder / dashboard / login | Public resume pages (e.g. `/ginnoir/resume`); MinIO object GETs on `storage.j-costa.com` if public |
### Your options
| Option | Meaning |
|--------|---------|
| **Do it** | We're doing this — I'll implement or walk you through it |
| **Later** | Keep on the list, not now |
| **Drop** | Remove from plan — totally fine |
| **Tweak** | Change scope; tell me how |
### What we decided ### What we decided
| | | | | |
|---|---| |---|---|
| **Decision** | Do it — batch 1 admin deployed; batches 23 pending | | **Decision** | Batches 13 done; batch 4 (romm, router UI) deferred — not now |
| **Notes** | Out: obsidian, foundry×2, overseerr. Batch 1: homarr native OIDC done; rest split native OAuth vs forward_auth+disable local auth (2026-06-11). Portainer needs one-time OAuth in UI; Plane needs god-mode OIDC. Push repo to persist. | | **Notes** | Batch 3 uses **native OIDC/OAuth** for ownCloud, FreshRSS, resume — not edge-only `forward_auth` (double-login). ntfy + `storage.j-costa.com` have **no Authentik**. Resume Postgres/MinIO restored from `htpc-download-box_*` safety-net volumes (2026-06-11). |
| **Date** | 2026-06-10 (batch 1 deployed) | | **Date** | 2026-06-11 (batch 3 complete) |
--- ---
@@ -63,42 +45,49 @@
### Stack / files ### Stack / files
Caddyfile + Authentik UI Caddyfile + Authentik UI + `stacks/authentik/scripts/`
### If we do it — rough steps ### Rollout batches
1. Authentik embedded outpost + proxy provider(s) on `edge` | Batch | Sites | Status |
2. Caddy `(authentik_auth)` snippet — `forward_auth authentik-server:9000` with trusted headers |-------|-------|--------|
3. Roll out site-by-site; per-site `handle` bypasses for webhooks/API | 1 — Admin | portainer, code, registry-ui, vault, minio, homarr, uptime, backup/kopia, gitea, dbx, bookstack, plane | Deployed |
4. **Important:** `forward_auth` only gates the edge — apps still show their own login unless you add **native OIDC/OAuth** (famapp/homarr pattern) or disable local auth (e.g. code-server `--auth none`). Homarr switched to native OIDC 2026-06-11. | 2 — Media | sonarr, radarr, bazarr, prowlarr, tautulli, qbit, deluge, nzbget, whisparr, stash | Deployed |
5. **Batch 1 rest (2026-06-11):** BookStack/Gitea/MinIO/Portainer/Plane → native OAuth2 providers; code/uptime/kopia/registry-ui/dbx/vault → forward_auth with local auth disabled where possible. **Portainer:** one-time OAuth setup in Settings → Authentication (see `setup-batch1-oidc.py` output). **Plane:** god-mode at `/god-mode/authentication/oidc`. | 3 — Public + login | files, freshrss, ntfy, resume, storage.j-costa.com | Deployed (revised approach below) |
| 4 — Other internal | romm, router UI | **Deferred** |
| Skip | famapp (OIDC done), auth.ginnoir.com, overseerr, obsidian, foundry×2, static sites | — |
**Rollout batches** ### Batch 3 — final approach (2026-06-11)
| Batch | Sites | | Site | Authentik method | Notes |
|-------|-------| |------|------------------|-------|
| 1 — Admin | portainer, code, registry-ui, vault, minio, homarr, uptime, backup/kopia, gitea, dbx, bookstack, plane | | **files** (ownCloud) | Native OIDC | `setup-batch1-oidc.py``owncloud-web`; `oidc.config.php` via `scripts/apply-batch3-app-auth.sh`; Caddy `/.well-known/openid-configuration` rewrite |
| 2 — Media | sonarr, radarr, bazarr, prowlarr, tautulli, qbit, deluge, nzbget, whisparr, stash | | **freshrss** | Native OIDC | Apache `mod_auth_openidc` (`OIDC_*` in notify stack); provider `freshrss` in Authentik — **Application must bind to OAuth2 provider**, not proxy |
| 3 — Public + login | files, freshrss, ntfy, resume (with bypass tables above) | | **resume** | Native OAuth | `reactive-resume` provider; `OAUTH_*` in resume stack; `FLAG_DISABLE_EMAIL_AUTH=true` |
| 4 — Other internal | romm, router UI | | **ntfy** | **None** | Native ntfy login only; forward_auth breaks web UI |
| Skip | famapp (OIDC done), auth.ginnoir.com, overseerr, obsidian, foundry×2, static sites | | **storage.j-costa.com** | **None** | Plain `reverse_proxy` — app writes to MinIO internally; public GETs for resume assets |
**Resume data restore:** bind mounts were empty after stack split; live data copied from `htpc-download-box_postgres_data` + `htpc-download-box_minio_data` via `scripts/restore-resume-data.sh`. After restore, run `scripts/fix-resume-oauth-link.sh` (email_verified) and `scripts/fix-resume-jwks.sh` if sessions loop (JWKS encrypted with old `AUTH_SECRET`).
**Ops scripts (batch 3):**
- `scripts/run-batch1-oidc.py` wrapper → `run-batch1-oidc.sh`
- `scripts/run-forward-auth-admin.sh`
- `scripts/apply-batch3-app-auth.sh` — ownCloud oidc.config.php + FreshRSS `http_auth` patch (superseded by FreshRSS native OIDC)
- `scripts/restore-resume-data.sh`, `scripts/fix-resume-oauth-link.sh`, `scripts/fix-resume-jwks.sh`
### Done when ### Done when
- [x] Batch 1 admin sites require Authentik before app UI loads (or native OAuth equivalent) - [x] Batch 1 admin sites require Authentik before app UI loads (or native OAuth equivalent)
- [x] Homarr native OIDC (no double login) - [x] Batch 2 media sites — forward_auth + external/disabled local auth (Tautulli excepted)
- [x] BookStack, Gitea, MinIO console → native Authentik OAuth/OIDC - [x] Batch 3 — ownCloud, FreshRSS, resume single Authentik login (native OIDC/OAuth)
- [x] code-server, uptime, kopia → forward_auth only (local auth disabled) - [x] ntfy — native auth only; Vigilant/famapp push unchanged
- [x] registry-ui, dbx, vault → forward_auth at edge - [x] storage.j-costa.com — no Authentik; public resume assets work
- [ ] Portainer OAuth configured in Settings → Authentication (provider created; UI step pending) - [x] Public resume pages viewable without Authentik
- [ ] Plane OIDC configured in god-mode (provider created; UI step pending) - [x] ownCloud share links work without Authentik session
- [x] Portainer git webhooks still work (bypass path) - [ ] Batch 4 — romm, router UI (deferred)
- [x] Gitea `/api/*` + `.git` and Vault `/v1/*` bypass forward_auth - [ ] Portainer OAuth configured in Settings → Authentication (provider created; UI step optional)
- [ ] Batch 2 media sites - [ ] Plane OIDC configured in god-mode (optional; forward_auth accepted)
- [ ] Batch 3 public apps (ownCloud shares, FreshRSS, ntfy, resume) - [ ] Reap orphan `htpc-download-box_*` volumes after trusting bind mounts (TB-011)
- [ ] ownCloud share links work without Authentik session
- [ ] FreshRSS still refreshes feeds; Vigilant/famapp still publish to ntfy
- [ ] Public resume pages still viewable without Authentik
- [ ] Overseerr unchanged for Plex friends
</details> </details>
@@ -1,7 +1,7 @@
# TB-033 — Public surface audit # TB-033 — Public surface audit
**Status:** not started **Status:** done — audited 2026-06-11
**Your call:** _(unset — talk first)_ **Your call:** Do it (walkthrough complete; action items below)
--- ---
@@ -32,9 +32,54 @@
| | | | | |
|---|---| |---|---|
| **Decision** | | | **Decision** | Do it — audit complete; fixes tracked below |
| **Notes** | | | **Notes** | Obsidian locked down in TB-005. Auth gaps → TB-006 (other session). |
| **Date** | | | **Date** | 2026-06-11 |
---
## Audit results (2026-06-11)
Sites **without** `internal_only` — reachable from the public internet (Cloudflare → Caddy):
### Intentionally public ✓
| Site | Purpose |
|------|---------|
| foundry.ginnoir.com, foundry2.ginnoir.com | Foundry VTT for players |
| 5etools.ginnoir.com, pf2e.ginnoir.com | Tabletop reference sites |
| files.ginnoir.com | ownCloud (Authentik on admin UI) |
| ffttsystems.ginnoir.com | Static site |
| requests.ginnoir.com | Overseerr for friends |
| ntfy.ginnoir.com | Push notifications (native auth) |
| freshrss.ginnoir.com | RSS reader (HTTP auth via Authentik) |
| resume.ginnoir.com, j-costa.com | Portfolio |
| storage.j-costa.com | Resume PDF/object storage (public by design) |
| fam.ginnoir.com | Family app (Authentik OIDC) |
| auth.ginnoir.com | Authentik |
| dev.ginnoir.com | External LAN host (192.168.1.74) — DNS public but host isn't on valhalla |
### Public but gated (Authentik forward_auth or native OIDC) — OK for now
| Site | Gate |
|------|------|
| sonarrstash, qbittorrent, deluge, … | `internal_only` + Authentik |
| uptime.ginnoir.com, backup.ginnoir.com | Authentik |
| code.ginnoir.com, registry-ui, dbx, vault, plane.ginnoir.com | Authentik |
| portainer.ginnoir.com | Authentik on UI; webhook POST bypass |
### Flagged — review / fix
| Site | Issue | Action |
|------|-------|--------|
| ~~**minio.ginnoir.com**~~ | ~~Public console~~ | **Fixed 2026-06-11**`internal_only` |
| ~~**homarr.ginnoir.com**~~ | ~~Public, no auth~~ | **Fixed 2026-06-11**`internal_only` |
| ~~**docs.ginnoir.com**~~ | ~~Public BookStack~~ | **Fixed 2026-06-11**`internal_only` |
| **gitea.ginnoir.com** | UI reachable; api/git bypass by design | OK if Gitea login is enough; optional Authentik — **TB-006** |
### Correctly internal-only ✓
obsidian, roms, romhacks, romhacks-files, registry.ginnoir.com, router.ginnoir.com, jd.ginnoir.com (added TB-002), all *arr admin UIs.
--- ---
@@ -53,7 +98,7 @@ Caddyfile review only
### Done when ### Done when
- [ ] Every public site has your yes/no - [x] Every public site has your yes/no
- [ ] Action items linked to other TBs - [x] Action items linked to other TBs
</details> </details>
+1 -1
View File
@@ -46,7 +46,7 @@ services:
# ============================================================ # ============================================================
vault: vault:
container_name: vault container_name: vault
image: hashicorp/vault:latest image: hashicorp/vault@sha256:3fd53308acccd9e4e83fde3e6c6cf5b15e0f7fac1ff7510780b8b23f5e4c3de7
restart: unless-stopped restart: unless-stopped
networks: networks:
- edge - edge
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -euo pipefail
# TB-006 batch 3: app-side auth so Authentik is not a second login screen.
# Run on valhalla after setup-batch1-oidc.py prints client_id/secret values.
#
# Requires OWNCLOUD_OIDC_* in owncloud stack.env and OAUTH_* in resume stack.env.
OWNCLOUD_ENV="${OWNCLOUD_ENV:-/tmp/owncloud-stack.env}"
RESUME_ENV="${RESUME_ENV:-/tmp/resume-stack.env}"
read_env() {
local file="$1" key="$2"
grep -E "^${key}=" "$file" | head -1 | cut -d= -f2-
}
echo "=== FreshRSS: switch to HTTP auth (Remote-User from Authentik via Caddy) ==="
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
docker cp "$SCRIPT_DIR/patch-freshrss-http-auth.php" freshrss:/tmp/patch-freshrss-http-auth.php
docker exec freshrss php /tmp/patch-freshrss-http-auth.php
docker exec freshrss grep -E "auth_type|http_auth_auto_register" /var/www/FreshRSS/data/config.php
echo "=== ownCloud: write oidc.config.php ==="
OC_ID="$(read_env "$OWNCLOUD_ENV" OWNCLOUD_OIDC_CLIENT_ID)"
OC_SECRET="$(read_env "$OWNCLOUD_ENV" OWNCLOUD_OIDC_CLIENT_SECRET)"
if [[ -z "$OC_ID" || -z "$OC_SECRET" ]]; then
echo "ERROR: set OWNCLOUD_OIDC_CLIENT_ID/SECRET in owncloud stack.env first" >&2
exit 1
fi
TMP_OC="$(mktemp)"
cat >"$TMP_OC" <<PHP
<?php
\$CONFIG = [
'http.cookie.samesite' => 'None',
'openid-connect' => [
'provider-url' => 'https://auth.ginnoir.com/application/o/owncloud/',
'client-id' => '${OC_ID}',
'client-secret' => '${OC_SECRET}',
'loginButtonName' => 'Log in with Authentik',
'mode' => 'userid',
'search-attribute' => 'preferred_username',
'autoRedirectOnLoginPage' => true,
],
];
PHP
docker cp "$TMP_OC" owncloud_server:/mnt/data/config/oidc.config.php
rm -f "$TMP_OC"
echo " wrote /mnt/data/config/oidc.config.php"
docker exec owncloud_server occ app:enable openidconnect >/dev/null 2>&1 || true
echo " owncloud openidconnect enabled"
echo "=== done — recreate resume container after stack.env OAuth vars are set ==="
if grep -q '^OAUTH_CLIENT_ID=' "$RESUME_ENV" 2>/dev/null; then
echo " resume stack.env has OAUTH_CLIENT_ID (Portainer redeploy or recreate app container)"
else
echo " WARN: add OAUTH_* vars to resume stack.env and redeploy the resume stack"
fi
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
# Restored resume DB may contain JWKS encrypted with an old AUTH_SECRET.
# Better Auth then fails session verification after OAuth (login page loop).
# Safe fix: drop JWKS + sessions; app regenerates keys on next start.
echo "=== resume: reset Better Auth JWKS (fixes session decrypt errors) ==="
docker stop resume >/dev/null
docker exec postgres_resume psql -U postgres -d postgres -c "TRUNCATE jwks, session, oauth_access_token, oauth_refresh_token CASCADE;"
docker start resume >/dev/null
sleep 15
docker logs resume --tail 15 2>&1
echo "done — retry resume.ginnoir.com → Authentik"
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
echo "=== resume: allow OAuth to link existing email/password account ==="
docker exec postgres_resume psql -U postgres -d postgres -c \
"UPDATE \"user\" SET email_verified = true WHERE username = 'ginnoir';"
docker exec postgres_resume psql -U postgres -d postgres -c \
"SELECT email, email_verified, username FROM \"user\";"
echo "done — retry Authentik login on resume.ginnoir.com"
+8
View File
@@ -0,0 +1,8 @@
<?php
$path = '/var/www/FreshRSS/data/config.php';
$text = file_get_contents($path);
$text = preg_replace("/'auth_type' => 'http'/", "'auth_type' => 'http_auth'", $text, 1);
$text = preg_replace("/'auth_type' => 'form'/", "'auth_type' => 'http_auth'", $text, 1);
$text = preg_replace("/'http_auth_auto_register' => true/", "'http_auth_auto_register' => false", $text, 1);
file_put_contents($path, $text);
echo "ok\n";
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -euo pipefail
# Restore resume postgres + minio from pre-split monolith named volumes into bind mounts.
# Safe to re-run: stops stack, copies from htpc-download-box_* volumes, restarts postgres.
OLD_PG_VOL="${OLD_PG_VOL:-htpc-download-box_postgres_data}"
OLD_MINIO_VOL="${OLD_MINIO_VOL:-htpc-download-box_minio_data}"
PG_BIND="${PG_BIND:-/config/resume/postgres}"
MINIO_BIND="${MINIO_BIND:-/storage1/labdata/resume/minio}"
echo "=== stop resume stack services ==="
docker stop resume minio_resume postgres_resume 2>/dev/null || true
echo "=== restore postgres: ${OLD_PG_VOL} -> ${PG_BIND} ==="
docker run --rm \
-v "${OLD_PG_VOL}:/from:ro" \
-v "${PG_BIND}:/to" \
alpine sh -c 'rm -rf /to/* /to/.[!.]* /to/..?* 2>/dev/null || true; cp -a /from/. /to/; chown -R 70:70 /to'
echo "=== restore minio objects: ${OLD_MINIO_VOL}/default -> ${MINIO_BIND}/default ==="
docker run --rm \
-v "${OLD_MINIO_VOL}:/from:ro" \
-v "${MINIO_BIND}:/to" \
alpine sh -c 'mkdir -p /to/default; rm -rf /to/default/* 2>/dev/null || true; cp -a /from/default/. /to/default/ 2>/dev/null || true'
echo "=== start postgres ==="
docker start postgres_resume
for i in $(seq 1 30); do
if docker exec postgres_resume pg_isready -U postgres -d postgres >/dev/null 2>&1; then
break
fi
sleep 1
done
docker exec postgres_resume pg_isready -U postgres -d postgres
echo "=== verify restored data ==="
docker exec postgres_resume psql -U postgres -d postgres -c 'SELECT email, username FROM "user"; SELECT slug, is_public FROM resume;'
echo "=== align resume user email with Authentik (3nigma.matt@gmail.com) for OAuth linking ==="
docker exec postgres_resume psql -U postgres -d postgres -c "UPDATE \"user\" SET email = '3nigma.matt@gmail.com' WHERE username = 'ginnoir';"
echo "=== start minio + resume (if compose project exists) ==="
docker start minio_resume 2>/dev/null || true
docker start resume 2>/dev/null || true
echo "done — if resume container was ad-hoc recreated, redeploy stacks/resume via Portainer after git push"
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
docker cp /tmp/setup-batch1-oidc.py authentik_server:/tmp/setup-batch1-oidc.py
docker exec authentik_server ak shell -c "exec(open('/tmp/setup-batch1-oidc.py').read())"
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
docker cp /tmp/apply-forward-auth-admin.py authentik_server:/tmp/apply-forward-auth-admin.py
docker exec authentik_server ak shell -c "exec(open('/tmp/apply-forward-auth-admin.py').read())"
+4
View File
@@ -51,6 +51,8 @@ services:
container_name: postgres_authentik container_name: postgres_authentik
image: postgres:16-alpine image: postgres:16-alpine
restart: unless-stopped restart: unless-stopped
labels:
- "com.centurylabs.watchtower.enable=false"
networks: [authentik] networks: [authentik]
env_file: env_file:
- stack.env - stack.env
@@ -66,6 +68,8 @@ services:
container_name: redis_authentik container_name: redis_authentik
image: redis:7-alpine image: redis:7-alpine
restart: unless-stopped restart: unless-stopped
labels:
- "com.centurylabs.watchtower.enable=false"
networks: [authentik] networks: [authentik]
command: --save 60 1 --loglevel warning command: --save 60 1 --loglevel warning
volumes: volumes:
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Apply TB-006 forward_auth proxy providers (admin batch 1 + media batch 2). """Apply TB-006 forward_auth proxy providers (batches 13).
Native OAuth/OIDC apps (bookstack, gitea, portainer, minio, homarr) use Native OAuth/OIDC apps (bookstack, gitea, portainer, minio, homarr) use
setup-batch1-oidc.py instead. Run this via: ak shell -c exec(open(...).read()) setup-batch1-oidc.py instead. Run this via: ak shell -c exec(open(...).read())
@@ -17,6 +17,12 @@ OIDC_APPS = [
("gitea", "gitea", ["https://gitea.ginnoir.com/user/oauth2/Authentik/callback"]), ("gitea", "gitea", ["https://gitea.ginnoir.com/user/oauth2/Authentik/callback"]),
("portainer", "portainer", ["https://portainer.ginnoir.com/"]), ("portainer", "portainer", ["https://portainer.ginnoir.com/"]),
("minio", "minio-console", ["https://minio.ginnoir.com/oauth_callback"]), ("minio", "minio-console", ["https://minio.ginnoir.com/oauth_callback"]),
("owncloud", "owncloud-web", [
"https://files.ginnoir.com/index.php/apps/openidconnect/redirect",
"https://files.ginnoir.com/apps/openidconnect/redirect",
]),
("resume", "reactive-resume", ["https://resume.ginnoir.com/api/auth/oauth2/callback/custom"]),
("freshrss", "freshrss", ["https://freshrss.ginnoir.com/i/oidc/"]),
] ]
native_slugs = [] native_slugs = []
@@ -63,6 +69,9 @@ PROXY_NAMES = {
"gitea": "homelab-gitea", "gitea": "homelab-gitea",
"portainer": "homelab-portainer", "portainer": "homelab-portainer",
"minio": "homelab-minio", "minio": "homelab-minio",
"owncloud": "homelab-files",
"resume": "homelab-resume",
"freshrss": "homelab-freshrss",
} }
outpost = Outpost.objects.get(name="authentik Embedded Outpost") outpost = Outpost.objects.get(name="authentik Embedded Outpost")
for slug in native_slugs: for slug in native_slugs:
@@ -1,4 +1,4 @@
"""Update proxy providers for forward_auth apps (admin batch 1 + media batch 2).""" """Update proxy providers for forward_auth apps (batches 13)."""
from authentik.core.models import Application from authentik.core.models import Application
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.outposts.models import Outpost, OutpostType from authentik.outposts.models import Outpost, OutpostType
+12 -2
View File
@@ -55,6 +55,8 @@ services:
container_name: postgres_gitea container_name: postgres_gitea
image: postgres:16-alpine image: postgres:16-alpine
restart: unless-stopped restart: unless-stopped
labels:
- "com.centurylabs.watchtower.enable=false"
networks: [dev] networks: [dev]
env_file: env_file:
- stack.env - stack.env
@@ -162,8 +164,10 @@ services:
mariadb_bookstack: mariadb_bookstack:
container_name: mariadb_bookstack container_name: mariadb_bookstack
image: lscr.io/linuxserver/mariadb:latest image: lscr.io/linuxserver/mariadb:11.4.7
restart: unless-stopped restart: unless-stopped
labels:
- "com.centurylabs.watchtower.enable=false"
networks: [dev] networks: [dev]
env_file: env_file:
- stack.env - stack.env
@@ -336,6 +340,8 @@ services:
container_name: postgres_plane container_name: postgres_plane
image: postgres:16-alpine image: postgres:16-alpine
restart: unless-stopped restart: unless-stopped
labels:
- "com.centurylabs.watchtower.enable=false"
networks: [dev] networks: [dev]
env_file: env_file:
- stack.env - stack.env
@@ -355,6 +361,8 @@ services:
container_name: redis_plane container_name: redis_plane
image: redis:7-alpine image: redis:7-alpine
restart: unless-stopped restart: unless-stopped
labels:
- "com.centurylabs.watchtower.enable=false"
networks: [dev] networks: [dev]
command: --save 60 1 --loglevel warning command: --save 60 1 --loglevel warning
volumes: volumes:
@@ -367,8 +375,10 @@ services:
plane-minio: plane-minio:
container_name: plane_minio container_name: plane_minio
image: minio/minio:latest image: minio/minio:RELEASE.2025-09-07T16-13-09Z
restart: unless-stopped restart: unless-stopped
labels:
- "com.centurylabs.watchtower.enable=false"
networks: [dev] networks: [dev]
command: server /data --console-address ":9001" command: server /data --console-address ":9001"
env_file: env_file:
+5 -1
View File
@@ -31,6 +31,8 @@ services:
container_name: postgres_famapp container_name: postgres_famapp
image: postgres:16-alpine image: postgres:16-alpine
restart: unless-stopped restart: unless-stopped
labels:
- "com.centurylabs.watchtower.enable=false"
networks: [famapp] networks: [famapp]
env_file: env_file:
- stack.env - stack.env
@@ -44,9 +46,11 @@ services:
famapp-minio: famapp-minio:
container_name: minio_famapp container_name: minio_famapp
image: minio/minio:latest image: minio/minio:RELEASE.2025-09-07T16-13-09Z
command: server /data --console-address ":9003" command: server /data --console-address ":9003"
restart: unless-stopped restart: unless-stopped
labels:
- "com.centurylabs.watchtower.enable=false"
networks: [famapp] networks: [famapp]
env_file: env_file:
- stack.env - stack.env
+11
View File
@@ -158,6 +158,17 @@ services:
ports: ports:
- "6969:6969" - "6969:6969"
recyclarr:
image: ghcr.io/recyclarr/recyclarr:8
container_name: recyclarr
restart: unless-stopped
networks: [media]
user: 1000:1000
env_file:
- stack.env
volumes:
- ./recyclarr:/config
stash: stash:
image: stashapp/stash:latest image: stashapp/stash:latest
container_name: stash container_name: stash
+25
View File
@@ -0,0 +1,25 @@
# TRaSH Guides sync — WEB-1080p (Sonarr) + HD Bluray + WEB (Radarr).
# Run manually: docker exec recyclarr recyclarr sync
sonarr:
main:
base_url: http://sonarr:8989
api_key: !secret sonarr_api_key
delete_old_custom_formats: true
quality_definition:
type: series
quality_profiles:
- trash_id: 72dae194fc92bf828f32cde74482a9dde
reset_unmatched_scores:
enabled: true
radarr:
main:
base_url: http://radarr:7878
api_key: !secret radarr_api_key
delete_old_custom_formats: true
quality_definition:
type: movie
quality_profiles:
- trash_id: d1d67249d3890e49bc12e275d989a7e9
reset_unmatched_scores:
enabled: true
+2
View File
@@ -0,0 +1,2 @@
sonarr_api_key: "383c9489253e445baf8514013a4e37a4"
radarr_api_key: "94159a19beef48f185bd3e330f8dde3a"
+3 -1
View File
@@ -22,8 +22,10 @@
services: services:
couchdb: couchdb:
container_name: couchdb container_name: couchdb
image: couchdb:3 image: couchdb:3.4
restart: unless-stopped restart: unless-stopped
labels:
- "com.centurylabs.watchtower.enable=false"
networks: [notes, edge] networks: [notes, edge]
env_file: env_file:
- stack.env - stack.env
+5
View File
@@ -62,6 +62,11 @@ services:
- CRON_MIN=3,33 - CRON_MIN=3,33
- TRUSTED_PROXY=172.16.0.1/12 192.168.0.1/16 - TRUSTED_PROXY=172.16.0.1/12 192.168.0.1/16
- FRESHRSS_INSTALL=--default-user ginnoir --api-enabled --base-url https://freshrss.ginnoir.com - FRESHRSS_INSTALL=--default-user ginnoir --api-enabled --base-url https://freshrss.ginnoir.com
- OIDC_ENABLED=1
- OIDC_PROVIDER_METADATA_URL=https://auth.ginnoir.com/application/o/freshrss/.well-known/openid-configuration
- OIDC_REMOTE_USER_CLAIM=preferred_username
- OIDC_SCOPES=openid profile email
- OIDC_X_FORWARDED_HEADERS=X-Forwarded-Host X-Forwarded-Proto
vigilant: vigilant:
container_name: vigilant container_name: vigilant
+3
View File
@@ -3,3 +3,6 @@ TZ=America/Chicago
# FreshRSS first-run user bootstrap (the container reads FRESHRSS_USER as the # FreshRSS first-run user bootstrap (the container reads FRESHRSS_USER as the
# full CLI argument string for its install script). # full CLI argument string for its install script).
FRESHRSS_USER=--api-password d0fet0th3x --email 3nigma.matt@gmail.com --password d0fet0th3x --user ginnoir FRESHRSS_USER=--api-password d0fet0th3x --email 3nigma.matt@gmail.com --password d0fet0th3x --user ginnoir
OIDC_CLIENT_ID=5DYeBheXidDAErkLTTMFBB4xdC734MoHQ2xNyVWp
OIDC_CLIENT_SECRET=4s9uiOgTla9euoVlvEgYNpqSGLOaxm0WG4tchUhzHYCjNyOeYQ2s426OaBAwV0YtXDXplCUhkCKNm6q0GCfpSZADhJo2L6nkRuCqFtuA3454Wpjf8ArRVYKwJqdGQePH
OIDC_CLIENT_CRYPTO_KEY=7f3a9c2e1b8d4f6a0e5c8b2d9f1a4e7
+6 -2
View File
@@ -37,9 +37,11 @@ services:
- /storage1/ROMs:/mnt/roms - /storage1/ROMs:/mnt/roms
mariadb: mariadb:
image: mariadb:latest image: mariadb:12.3
container_name: owncloud_mariadb container_name: owncloud_mariadb
restart: unless-stopped restart: unless-stopped
labels:
- "com.centurylabs.watchtower.enable=false"
networks: [owncloud] networks: [owncloud]
env_file: env_file:
- stack.env - stack.env
@@ -56,9 +58,11 @@ services:
- /config/mysql:/var/lib/mysql - /config/mysql:/var/lib/mysql
redis: redis:
image: redis:latest image: redis:7-alpine
container_name: owncloud_redis container_name: owncloud_redis
restart: unless-stopped restart: unless-stopped
labels:
- "com.centurylabs.watchtower.enable=false"
networks: [owncloud] networks: [owncloud]
command: ["--databases", "1"] command: ["--databases", "1"]
healthcheck: healthcheck:
+2
View File
@@ -9,3 +9,5 @@ OWNCLOUD_ADMIN_PASSWORD=d0fet0th3x
MYSQL_ROOT_PASSWORD=d0fet0th3x MYSQL_ROOT_PASSWORD=d0fet0th3x
MYSQL_USER=ginnoir MYSQL_USER=ginnoir
MYSQL_PASSWORD=d0fet0th3x MYSQL_PASSWORD=d0fet0th3x
OWNCLOUD_OIDC_CLIENT_ID=kVTZzTn4ho1lfD8ghtYHYsW84jLBzVQJBzZ7klHB
OWNCLOUD_OIDC_CLIENT_SECRET=ZvNY4bLkBsbSfo1rUBnX9nSwPzcP5R2Ya18z7IND45KmoPwUOMjnTOI6QjtQuLrzoSJ8ym03rLQLZoNb7Cdp4PtVlPPTMZYKwJBtK7DNWt8zllHLT97gbqWKQGTMxqKF
+3 -1
View File
@@ -15,8 +15,10 @@
services: services:
caddy: caddy:
container_name: caddy container_name: caddy
image: serfriz/caddy-cloudflare-ddns:latest image: serfriz/caddy-cloudflare-ddns:2.11.3
restart: unless-stopped restart: unless-stopped
labels:
- "com.centurylabs.watchtower.enable=false"
networks: networks:
- edge - edge
ports: ports:
+9 -1
View File
@@ -11,6 +11,8 @@ services:
container_name: postgres_resume container_name: postgres_resume
image: postgres:16-alpine image: postgres:16-alpine
restart: unless-stopped restart: unless-stopped
labels:
- "com.centurylabs.watchtower.enable=false"
networks: [resume] networks: [resume]
volumes: volumes:
- /config/resume/postgres:/var/lib/postgresql/data - /config/resume/postgres:/var/lib/postgresql/data
@@ -27,8 +29,10 @@ services:
resume-minio: resume-minio:
container_name: minio_resume container_name: minio_resume
image: minio/minio:latest image: minio/minio:RELEASE.2025-09-07T16-13-09Z
restart: unless-stopped restart: unless-stopped
labels:
- "com.centurylabs.watchtower.enable=false"
networks: [resume, edge] networks: [resume, edge]
command: server /data --console-address :9001 command: server /data --console-address :9001
ports: ports:
@@ -93,6 +97,10 @@ services:
- STORAGE_BUCKET=default - STORAGE_BUCKET=default
- STORAGE_USE_SSL=false - STORAGE_USE_SSL=false
- STORAGE_SKIP_BUCKET_CHECK=false - STORAGE_SKIP_BUCKET_CHECK=false
- OAUTH_PROVIDER_NAME=Authentik
- OAUTH_DISCOVERY_URL=https://auth.ginnoir.com/application/o/resume/.well-known/openid-configuration
- FLAG_DISABLE_EMAIL_AUTH=true
- FLAG_DISABLE_SIGNUPS=true
networks: networks:
resume: resume:
+2
View File
@@ -24,3 +24,5 @@ STORAGE_SECRET_KEY=minioadmin
# Pinned to the running value so recreating the container keeps sessions valid. # Pinned to the running value so recreating the container keeps sessions valid.
# Rotate with: openssl rand -hex 64 # Rotate with: openssl rand -hex 64
AUTH_SECRET=1b7e96f61f080d04d4bf673d9ea1d9349eb7693cddee5772b6c3f153c88032db5b97efd8570087bc6f340d2cd874aabc1e5c681bc22889921240d6a23738bc7f AUTH_SECRET=1b7e96f61f080d04d4bf673d9ea1d9349eb7693cddee5772b6c3f153c88032db5b97efd8570087bc6f340d2cd874aabc1e5c681bc22889921240d6a23738bc7f
OAUTH_CLIENT_ID=YpMFNp9q5mGbN8ADM5rUol0ERHmSiTDClJKIVOxK
OAUTH_CLIENT_SECRET=mzwjSt0Xtibo156km61T7VYerT48K6ZTa79cEek4mgdky6t1Sjaddlshw2QcJL8727N6qWzddi2ihxNGSpUVMXUBbjNgx4jtkHAy5xtmvqdk9ouIFclyhCgXh9fzjMCA
+3 -1
View File
@@ -46,9 +46,11 @@ services:
- "8997:8080" - "8997:8080"
romm-db: romm-db:
image: mariadb:latest image: mariadb:12.3
container_name: romm-db container_name: romm-db
restart: unless-stopped restart: unless-stopped
labels:
- "com.centurylabs.watchtower.enable=false"
networks: [roms] networks: [roms]
env_file: env_file:
- stack.env - stack.env