fix(docker): migrate retained SearXNG settings - #6055
Conversation
Retained nonempty SearXNG settings can miss defaults required by newer pinned images while bypassing the entrypoint's narrow regeneration checks. Add an atomic PyYAML-aware migration to all Compose variants. Preserve existing inheritance choices, custom content, secrets, ownership, and mode while inserting only the missing top-level default-inheritance key. Validated with 39 focused and adjacent tests, compile checks, and fresh and retained pinned-image HTTP 200 gates. Full repository CI remains for the PR.
o3LL
left a comment
There was a problem hiding this comment.
Reviewed 3cd6cdb6 against dev at 2c394704. Merge probe is clean.
The approach is right and the bug is real — I reproduced #6054 exactly against the pinned image with a retained settings volume. But the write path can't succeed in a real deployment as written, and the entrypoint turns that into a boot failure.
Findings
P0 · issue — the migration can never write on the upgrade path it targets
- Problem:
fchownruns beforefchmod, and since SearXNG's own entrypoint doeschown -R searxng:searxng /etc/searxng, root no longer owns the temp file by the time the chmod lands — the Compose cap set (cap_drop: ALLplus CHOWN/SETGID/SETUID/DAC_OVERRIDE, noFOWNER) returns EPERM. - Impact: the script exits 1,
set -eukills the wrapper beforeexec /usr/local/searxng/entrypoint.sh, and SearXNG never starts, soodysseusblocks onservice_healthy— strictly worse than the 500 in #6054. - Ask: swap the two calls so the chmod happens while the temp file is still root-owned.
os.fchown(fd, source_stat.st_uid, source_stat.st_gid)
os.fchmod(fd, stat.S_IMODE(source_stat.st_mode))- Location:
scripts/migrate_searxng_settings.py:123
I ran the pinned image with the Compose cap set against a searxng-owned retained settings.yml: as-is it prints SearXNG settings migration failed: [Errno 1] Operation not permitted and the container exits; with the two lines swapped the root endpoint returns 200, the key is inserted, and mode 640 and searxng:searxng ownership survive. It passes for a root-owned file, which is what an isolated hand-seeded volume looks like — a real upgrade is always the chowned case. Happy to paste the matrix if useful.
P1 · issue — any migration failure becomes a boot failure
- Problem:
main()returns 1 for everyOSError/UnicodeError/ValueError, and all three Compose variants invoke it unguarded underset -eu. - Impact: a retained file that is non-UTF-8, a symlink, or has a non-mapping root now prevents SearXNG from starting at all, where today it boots and fails visibly on its own terms.
- Ask: make the migration advisory — append
|| truein the three entrypoints, or return 0 after printing the warning. - Location:
docker-compose.yml:113
P2 · issue — curated engine lists silently gain the full default set
- Problem: adding
use_default_settings: truechanges a standaloneengines:list from "these are the engines" to "merge these into the defaults", which "preserves existing custom settings" in the summary doesn't cover. - Impact: I booted the pinned image with a retained file pinning two engines; after migration
/configreports 244 enabled engines, including ones the operator had removed. - Ask: skip files carrying a top-level
engines:key, or emituse_default_settings: {engines: {keep_only: [...]}}for them — at minimum say so in the log line and the PR body. - Location:
scripts/migrate_searxng_settings.py:90
P2 · issue scope — migrated volumes come up healthy with search still dead
- Problem: SearXNG's default
search.formatsishtmlonly, so a retained file with no explicitformatslist inherits defaults that excludejson. - Impact:
/returns 200 and the healthcheck goes green, butservices/search/providers.py:156requestsformat=jsonand gets a 403 — a quieter failure than the one #6052 reports today. - Ask: also ensure
jsonis present insearch.formats, or note in the PR that those volumes still need a manual edit and keepFixespointed only at #6054. - Location:
scripts/migrate_searxng_settings.py:90
P3 · test — the new tests fail on Windows
- Problem:
os.fchownandos.O_DIRECTORYare Unix-only, so every test that reaches the write path errors there. - Impact: contributors running the suite on Windows get failures unrelated to their change.
- Ask: add the
sys.platform == "win32"skip already used intests/test_app_db_permissions.py:10. - Location:
tests/test_searxng_settings_migration.py:1
P3 · test — nothing covers the ordering that P0 breaks
- Problem: the ownership assertions run as the file's owner, so
fchownis a no-op andfchmodcan never fail. - Impact: the one failure mode that matters inside the container is invisible to the suite.
- Ask: assert
fchmodis called beforefchown— a spy on both is enough. - Location:
tests/test_searxng_settings_migration.py:66
Open Questions
- #6052 is filed as a fresh
git cloneplusdocker compose up -d --build, so the retained-volume diagnosis rests on the reporter reusing an existingsearxng-datavolume. Worth confirming with them before treating this PR as the fix for that report.
Validation
- Ran: reproduced #6054 on
dev2c394704— pinned image, retainedsearxng-ownedsettings.yml, HTTP 500 withKeyError: 'default_doi_resolver'. Ran the migration on that volume under the Compose cap set (fails, P0) and with the calls swapped (200, bytes/secret/mode/owner preserved, second run a no-op), plus the engine-count andformat=jsonchecks above. Full suite on3cd6cdb6: 5341 passed, 2 failed, 4 skipped — both failures are the known macOS ones.compileallclean on both new files. - Not run: full
docker compose upof the whole stack; the two GPU variants; a Linux host; SELinux relabelling of the new bind mount. - Residual risk: I exercised the SearXNG service in isolation on arm64, so anything specific to a full Compose run or to x86 Linux is unverified.
PR Hygiene
- Targets
dev, one focused change, title matches the Conventional Commits check,ready for reviewis on, no rendering changes so the screenshot requirement doesn't apply. Fixes #6054is the right link and the right verb. Merge probe againstdev2c394704is clean, and it also merges cleanly with #5894, which edits the adjacentports:line.- The "I actually ran the app" box is left unticked, which matches what you wrote — that gap is where the P0 sits, so the ask is one
docker composerun against a retained volume once the ordering is swapped.
Only P0 blocks. If you'd rather not respin it yourself, say the word and I'll push the swap plus the ordering test to your branch or open a follow-up — I already have the reproduction set up.
The Compose cap set is `cap_drop: ALL` plus CHOWN/SETGID/SETUID/DAC_OVERRIDE and carries no FOWNER, and searxng's own entrypoint chowns /etc/searxng to searxng:searxng, so every retained settings file belongs to that user by the second boot. Chowning the temporary file first left root unable to chmod it, so the migration exited 1 and `set -eu` killed the container before `exec /usr/local/searxng/entrypoint.sh` — SearXNG never started and odysseus blocked on its healthcheck. Swap the two calls so the chmod lands while the temporary file is still root-owned, and cover the ordering with a test that refuses the chmod once the chown has happened, the way the kernel does.
The migration runs under `set -eu`, so any settings file it cannot parse or rewrite took the container down instead of merely going unmigrated. A symlinked /etc/searxng/settings.yml is enough: the migration refuses a non-regular file and searxng, which reads through the symlink perfectly well, never got to start. Guard the call with `|| true` in all three Compose variants. The failure still prints its reason on stderr, and searxng is left to report anything genuinely wrong with the file.
Summary
Retained nonempty SearXNG settings can omit
use_default_settings, so newer pinned images fail with missing settings instead of inheriting their current defaults. This adds an idempotent PyYAML-aware startup migration to every Compose variant; it inserts only the missing top-level inheritance key and atomically preserves existing custom settings, comments, secrets, ownership, mode, and the settings volume.Target branch
dev, notmain. All PRs land indev;mainis curated by the maintainer at each release. If your PR is onmainby accident, click "Edit" on this PR and change the base.Linked Issue
Fixes #6054
Type of Change
Checklist
devdocker compose uporuvicorn app:app) and verified the change works end-to-end. Type-checks and unit tests are not enough.How to Test
python -m pytest -q tests/test_searxng_settings_migration.py tests/test_searxng_image_pinned.py tests/test_gpu_compose_standalone.py; the focused migration, image pin, and standalone Compose coverage should pass.use_default_settings: trueand must remain byte-identical through the migration.settings.ymlwith a retained nonempty mapping that has a representativeserver.secret_keyand custom search/UI values but no top-leveluse_default_settings, restart SearXNG, and confirm the key is inserted while all original bytes, ownership, mode, custom values, and secret remain intact. A second restart must make no further change, and the health endpoint must return HTTP 200.Author validation: the focused command passed with
39 passedand one unrelated pre-existing SQLAlchemy deprecation warning.python -m py_compile scripts/migrate_searxng_settings.py tests/test_searxng_settings_migration.pypassed. Isolated builds of the pinned SearXNG image returned HTTP 200 for both fresh and retained settings; the fresh file stayed byte-identical, and the retained file preserved its representative secret, custom values, and mode while the second migration was a no-op. Full Odysseusdocker compose upwas not run because Docker is unavailable in the controller and secretless test runner.Visual / UI changes — REQUIRED if you touched anything that renders
Anything that changes what the UI looks like — buttons, icons, padding, colors, fonts, spacing, layout, CSS, HTML, SVG, or any
static/js/module that draws to the DOM — needs all of the following. PRs that change rendering without these WILL be closed.--red,--fg,--bg,--card,--border, etc.) — do not introduce new color values, font sizes, or spacing units.static/index.html) or plain text.Fira Code) for primary UI text. Don't override.Screenshots / clips
N/A — no UI or rendering files changed.