Persist settings.json across deploys via a data volume
All checks were successful
Deploy / deploy (push) Successful in 21s

The previous commit made Settings-saved API keys override .env, but the
key still never survived: settings.json was written to the container
filesystem, and every deploy runs `up -d --build`, which recreates the
container and discards it. Saving a rotated SABnzbd key and restarting
therefore still fell back to the stale .env value.

SETTINGS_FILE is now configurable and pointed at /data/settings.json on
a ./data bind mount, so runtime settings live on the host. The deploy
rsync also excludes data/ — it runs --delete and would otherwise wipe
the directory for not being in the repo, reintroducing the same bug.

This affected everything the Settings page writes, not just API keys:
timezone and a UI-changed login password were being reverted on every
deploy too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Barely Removable 2026-08-26 09:34:46 -07:00
parent 20f348eb9e
commit 600a48da93
4 changed files with 31 additions and 5 deletions

View file

@ -17,13 +17,15 @@ jobs:
# at /mnt/user/appdata/greendale on the host, bind-mounted into this # at /mnt/user/appdata/greendale on the host, bind-mounted into this
# container at the same path (see runner container.options) so # container at the same path (see runner container.options) so
# `docker compose`'s relative paths resolve against the real host # `docker compose`'s relative paths resolve against the real host
# directory rather than an ephemeral one. `.env` is server-only and # directory rather than an ephemeral one. `.env` and `data/` are
# must survive every deploy. # server-only state and must survive every deploy — without the
# excludes, --delete removes them because they aren't in the repo.
- name: Sync source to deploy path - name: Sync source to deploy path
run: | run: |
rsync -a --delete \ rsync -a --delete \
--exclude='.git' \ --exclude='.git' \
--exclude='.env' \ --exclude='.env' \
--exclude='data' \
./ /mnt/user/appdata/greendale/ ./ /mnt/user/appdata/greendale/
- name: Rebuild and restart - name: Rebuild and restart

View file

@ -89,5 +89,18 @@ pipeline mechanics. Push to `main` on the `forgejo` remote
and `LOGIN_PASSWORD` — this project was already doing this correctly and `LOGIN_PASSWORD` — this project was already doing this correctly
before the Forgejo migration (compose file uses `${VAR}` substitution from before the Forgejo migration (compose file uses `${VAR}` substitution from
`.env`, no hardcoded secrets), unlike `n64-tracker` which needed fixing. `.env`, no hardcoded secrets), unlike `n64-tracker` which needed fixing.
`settings.json` (timezone, changed login password) also persists Runtime settings written by the Settings page (API-key overrides,
server-side outside git. timezone, changed login password) live in `settings.json` at
`SETTINGS_FILE`, which the compose file points at `/data/settings.json`
on the `./data` bind mount — i.e. `/mnt/user/appdata/greendale/data`.
That mount is load-bearing: without it the file sits on the container
filesystem, and every `up -d --build` recreates the container and
silently discards it. `data/` is likewise excluded from the deploy
rsync, which runs `--delete` and would otherwise remove it for not
being in the repo.
A key saved through the Settings page overrides the corresponding
`.env` value at startup (see the block after the `*_API_KEY` reads in
`app.py`); `.env` remains the source of truth for anything never
overridden there. Both paths need a container restart to take effect,
since the keys are read once at import.

7
app.py
View file

@ -15,7 +15,11 @@ from flask import Flask, render_template, request, jsonify, redirect, url_for, s
# Suppress SSL warnings for Unraid's self-signed certificate # Suppress SSL warnings for Unraid's self-signed certificate
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
SETTINGS_FILE = os.path.join(os.path.dirname(__file__), "settings.json") # Must point at a path that survives `docker compose up --build` — the
# container filesystem does not. See the data volume in docker-compose.yml.
SETTINGS_FILE = os.environ.get(
"SETTINGS_FILE", os.path.join(os.path.dirname(__file__), "settings.json")
)
def load_settings(): def load_settings():
try: try:
@ -27,6 +31,7 @@ def load_settings():
def save_settings(data): def save_settings(data):
existing = load_settings() existing = load_settings()
existing.update(data) existing.update(data)
os.makedirs(os.path.dirname(SETTINGS_FILE) or ".", exist_ok=True)
with open(SETTINGS_FILE, "w") as f: with open(SETTINGS_FILE, "w") as f:
json.dump(existing, f, indent=2) json.dump(existing, f, indent=2)

View file

@ -4,11 +4,17 @@ services:
container_name: arr-summary container_name: arr-summary
ports: ports:
- "${HOST_PORT:-5055}:5000" - "${HOST_PORT:-5055}:5000"
volumes:
# Runtime settings (API keys saved via the Settings page, timezone,
# changed login password) must outlive `up -d --build`, which
# recreates the container and discards its filesystem.
- ./data:/data
environment: environment:
# ── Login / proxy ────────────────────────────────────── # ── Login / proxy ──────────────────────────────────────
- LOGIN_PASSWORD=${LOGIN_PASSWORD} - LOGIN_PASSWORD=${LOGIN_PASSWORD}
- SECRET_KEY=${SECRET_KEY} - SECRET_KEY=${SECRET_KEY}
- SCRIPT_NAME=${SCRIPT_NAME:-} - SCRIPT_NAME=${SCRIPT_NAME:-}
- SETTINGS_FILE=/data/settings.json
# ── Media services ───────────────────────────────────── # ── Media services ─────────────────────────────────────
- ARR_HOST=${ARR_HOST} - ARR_HOST=${ARR_HOST}
- SONARR_API_KEY=${SONARR_API_KEY} - SONARR_API_KEY=${SONARR_API_KEY}