Skip to content

feat(services): add SearXNG Quadlet sidecar for self-hosted search - #2

Open
spicectl wants to merge 2 commits into
mainfrom
homelab/t_ae928be1-searxng-sidecar---self-hosted-search-eng
Open

feat(services): add SearXNG Quadlet sidecar for self-hosted search#2
spicectl wants to merge 2 commits into
mainfrom
homelab/t_ae928be1-searxng-sidecar---self-hosted-search-eng

Conversation

@spicectl

@spicectl spicectl commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

What

Deploys SearXNG as a Quadlet sidecar container managed through Den, giving Hermes a self-hosted search backend that isn't CAPTCHA-blocked.

Changes

  • New module: modules/den/aspects/services/searxng.nix — Den aspect with configurable port, base URL, instance name, and agenix-backed SEARXNG_SECRET_KEY. Gracefully handles missing secrets with a build warning.
  • Host wiring: Added den.aspects.services.searxng to hvn-hyp1 includes and enabled the service with a secret path.

Usage

  1. Create the age secret:
    echo "your-random-secret-here" | agenix -e .secrets/hosts/hvn-hyp1/searxng-secret-key.age
    agenix rekey
    git add .secrets/ && git commit
  2. Deploy
  3. Test: curl http://localhost:8080/search?q=test&format=json
  4. Configure Hermes to use it as the search backend

Summary by CodeRabbit

  • New Features
    • Added support for deploying a SearXNG search service with configurable instance name, port, and public URL.
    • The service runs in a persistent container and securely uses a configured secret key.
    • Enabled SearXNG on the HVN-HYP1 host.
    • Added helpful warnings when the required secret is not available.

Deploys searxng/searxng:latest as a Quadlet container managed through Den.
SearXNG aggregates search results from multiple upstream engines,
bypassing the CAPTCHA blocks we keep hitting from the home IP.

- New Den aspect: den.aspects.services.searxng
- Configurable port, base URL, instance name, and agenix-backed secret
- Graceful missing-secret handling with warning message
- Quadlet container with persistent data volume for /etc/searxng
- Wired into hvn-hyp1 host config

Usage:
  1. Create the age secret:
     agenix -e .secrets/hosts/hvn-hyp1/searxng-secret-key.age
  2. agenix rekey
  3. Deploy
  4. Query at http://hvn-hyp1:8080/search?q=test&format=json
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@spicectl, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: aa946be3-720f-4dd3-a402-8356fef33fb8

📥 Commits

Reviewing files that changed from the base of the PR and between 5639742 and 30a6f06.

📒 Files selected for processing (1)
  • modules/den/aspects/services/searxng.nix
📝 Walkthrough

Walkthrough

Adds a configurable SearXNG NixOS module with agenix-backed secret handling and Quadlet container deployment, then enables the service for the hvn-hyp1 host using a host-specific secret file.

Changes

SearXNG service deployment

Layer / File(s) Summary
SearXNG module and container configuration
modules/den/aspects/services/searxng.nix
Defines service settings, conditionally requests the agenix secret, warns when it is missing, configures the SearXNG Quadlet container, and installs curl.
hvn-hyp1 service integration
modules/den/hosts/hvn-hyp1/default.nix
Enables SearXNG with the host-specific secret path and includes the service aspect in the host configuration.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HostConfig
  participant SearXNGModule
  participant Agenix
  participant Quadlet
  participant SearXNG
  HostConfig->>SearXNGModule: enable service and provide secretKeyFile
  SearXNGModule->>Agenix: request secret when age file exists
  SearXNGModule->>Quadlet: configure container and published port
  Agenix->>SearXNG: provide secret at /run/secrets/<secretName>
  Quadlet->>SearXNG: start searxng/searxng:latest
Loading

Suggested reviewers: dvicory

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding a SearXNG Quadlet service for self-hosted search.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch homelab/t_ae928be1-searxng-sidecar---self-hosted-search-eng

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add SearXNG Quadlet sidecar for self-hosted search on hvn-hyp1

✨ Enhancement ⚙️ Configuration changes 🕐 10-20 Minutes

Grey Divider

AI Description

• Add a Den aspect that deploys SearXNG via Quadlet with configurable port/base URL.
• Integrate agenix-backed SEARXNG_SECRET_KEY with a warning when the secret is missing.
• Enable the SearXNG aspect and host settings for hvn-hyp1.
Diagram

graph TD
  A["hvn-hyp1 host config"] --> B["Den SearXNG aspect"] --> C[("agenix secret file")]
  B --> D["Quadlet (systemd)"] --> E["SearXNG container"]
  F["Hermes service"] --> E
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use a native NixOS SearXNG module (if available)
  • ➕ Avoids pulling a mutable container image at deploy time
  • ➕ Leverages NixOS module defaults for config files, users, and hardening
  • ➕ Typically simplifies secret injection and persistence semantics
  • ➖ May require more Nix packaging work/upstream overrides to match desired settings
  • ➖ Less aligned with an existing Quadlet/container-first homelab pattern
2. Pin the container image (tag/digest) instead of :latest
  • ➕ Reproducible deployments and safer rollbacks
  • ➕ Avoids surprise breakages from upstream image updates
  • ➖ Requires periodic manual upgrades to bump the pinned version/digest

Recommendation: The Quadlet sidecar approach is reasonable given the repo’s Den/Quadlet pattern, but consider pinning the searxng image (version tag or digest) to improve reproducibility. If long-term maintainability or hardening becomes a priority, evaluate a native NixOS module to eliminate runtime image drift and better integrate config/secret management.

Files changed (2) +77 / -0

Enhancement (1) +72 / -0
searxng.nixIntroduce Den SearXNG aspect with Quadlet + agenix secret support +72/-0

Introduce Den SearXNG aspect with Quadlet + agenix secret support

• Adds a new Den aspect exposing enable/instance/port/baseUrl/secretKeyFile options. When enabled, it conditionally requests an agenix secret and starts a Quadlet container only if the secret file exists, otherwise emitting a build-time warning. The container publishes the configured host port, sets SEARXNG_BASE_URL and SEARXNG_SECRET_KEY (via /run/secrets), and persists /etc/searxng via a named volume.

modules/den/aspects/services/searxng.nix

Other (1) +5 / -0
default.nixEnable SearXNG service and include its aspect on hvn-hyp1 +5/-0

Enable SearXNG service and include its aspect on hvn-hyp1

• Wires host settings to enable SearXNG on hvn-hyp1 and points secretKeyFile at the host-specific age file. Adds the new den.aspects.services.searxng to the hvn-hyp1 includes list so the module is applied during evaluation/deploy.

modules/den/hosts/hvn-hyp1/default.nix

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@modules/den/aspects/services/searxng.nix`:
- Around line 67-69: Move the environment.systemPackages assignment into the
lib.mkIf (cfg.enable or false) block so pkgs.curl is installed only when the
SearXNG service is enabled. Keep the existing package assignment unchanged
otherwise and remove it from the unconditional lib.mkMerge argument.
- Around line 56-59: Update the environment construction in the SearXNG service
configuration to read the contents of the secret file and assign those contents
to SEARXNG_SECRET_KEY, rather than exporting the literal file:/run/secrets path.
Preserve SEARXNG_BASE_URL and the existing secretName-based file location.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1e5ce24f-31fc-42e8-a47e-12980f5de8c7

📥 Commits

Reviewing files that changed from the base of the PR and between 5d7b9b7 and 5639742.

📒 Files selected for processing (2)
  • modules/den/aspects/services/searxng.nix
  • modules/den/hosts/hvn-hyp1/default.nix

Comment thread modules/den/aspects/services/searxng.nix Outdated
Comment thread modules/den/aspects/services/searxng.nix Outdated
@qodo-code-review

qodo-code-review Bot commented Jul 14, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. secretKeyFile wrong type 🐞 Bug ≡ Correctness
Description
settings.secretKeyFile is declared as lib.types.str, but hvn-hyp1 sets it to a Nix path
(inputs.self + "/..."), which will fail option type checking and prevent evaluation/build.
Code

modules/den/aspects/services/searxng.nix[R24-27]

+      secretKeyFile = lib.mkOption {
+        type = lib.types.str;
+        description = "Path to agenix secret containing SEARXNG_SECRET_KEY.";
+      };
Evidence
The option is defined as a string but is used as a path later (for builtins.pathExists and
secretRequests.ageFile), and the host assigns it a path value; this combination will cause Nix
evaluation/type-checking failures.

modules/den/aspects/services/searxng.nix[24-36]
modules/den/hosts/hvn-hyp1/default.nix[19-22]
modules/den/aspects/secrets/agenix.nix[41-53]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`den.aspects.services.searxng.settings.secretKeyFile` is typed as a string, but it is used as a filesystem path (and the host config sets it as a path). This causes an evaluation-time type error.

## Issue Context
Other secret-related options in this repo (e.g., `secretRequests.<name>.ageFile`) are typed as `path`/`nullOr path`.

## Fix Focus Areas
- modules/den/aspects/services/searxng.nix[24-27]

## Suggested fix
- Change `secretKeyFile` option type to `lib.types.path` (or `lib.types.nullOr lib.types.path` with a default).
- If you intentionally want to accept strings, use `lib.types.either lib.types.str lib.types.path` and normalize with `toString`/`/. +` before calling `builtins.pathExists` and passing to `secretRequests`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Secret not mounted ✓ Resolved 🐞 Bug ≡ Correctness
Description
The container is configured to read the secret from file:/run/secrets/searxng-secret-key, but the
Quadlet config never mounts the decrypted agenix secret (config.age.secrets.<name>.path) into the
container, so that file won’t exist inside the container at runtime.
Code

modules/den/aspects/services/searxng.nix[R56-63]

+            environments = {
+              SEARXNG_BASE_URL = cfg.baseUrl;
+              SEARXNG_SECRET_KEY = "file:/run/secrets/${secretName}";
+            };
+            volumes = [
+              "${dataVolume}:/etc/searxng:rw"
+            ];
+          };
Evidence
The SearXNG container references /run/secrets/... but only mounts ${dataVolume}:/etc/searxng; in
contrast, other Quadlet configs in this repo explicitly mount age.secrets.*.path into
/run/secrets/... when using the file:/run/secrets/... convention.

modules/den/aspects/services/searxng.nix[51-63]
modules/den/aspects/workloads/hermes.nix[219-236]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The SearXNG Quadlet sets `SEARXNG_SECRET_KEY = "file:/run/secrets/<name>"` but does not mount any host secret file into `/run/secrets/<name>`.

## Issue Context
This repo’s existing Quadlet usage mounts `osConfig.age.secrets.<name>.path` into `/run/secrets/...` when using `file:/run/secrets/...`.

## Fix Focus Areas
- modules/den/aspects/services/searxng.nix[51-65]

## Suggested fix
- Add a read-only volume mount, e.g.:
 - `"${config.age.secrets.${secretName}.path}:/run/secrets/${secretName}:ro"`
- Alternatively, adjust the env var to point at the mounted location you choose (but ensure the file exists inside the container).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Quadlet module not imported 🐞 Bug ≡ Correctness
Description
hvn-hyp1 includes den.aspects.services.searxng, which sets virtualisation.quadlet.containers...,
but this host does not include den.aspects.virtualization.podman (the only aspect in this repo
that imports/enables Quadlet), so evaluation is likely to fail with an unknown
virtualisation.quadlet option.
Code

modules/den/hosts/hvn-hyp1/default.nix[R76-80]

      den.aspects.core."remote-unlock"
      den.aspects.services.mergerfs
      den.aspects.secrets.agenix
+      den.aspects.services.searxng
      den.aspects.core.network.tailscale
Evidence
The SearXNG aspect writes to virtualisation.quadlet.containers, but the only place Quadlet is
imported/enabled in this repo is den.aspects.virtualization.podman; hvn-hyp1’s includes list does
not include it.

modules/den/aspects/services/searxng.nix[51-65]
modules/den/aspects/virtualization/podman.nix[12-23]
modules/den/hosts/hvn-hyp1/default.nix[66-83]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The SearXNG aspect configures `virtualisation.quadlet.containers`, but hvn-hyp1 does not include the virtualization aspect that imports `quadlet-nix` and enables `virtualisation.quadlet`.

## Issue Context
Repo search shows only `den.aspects.virtualization.podman` imports `inputs.quadlet-nix.nixosModules.quadlet` and sets `virtualisation.quadlet.enable = true`.

## Fix Focus Areas
- modules/den/hosts/hvn-hyp1/default.nix[66-83]
- modules/den/aspects/services/searxng.nix[51-65]
- modules/den/aspects/virtualization/podman.nix[12-23]

## Suggested fix
- Add `den.aspects.virtualization.podman` to hvn-hyp1 `includes`, or
- Add `includes = [ den.aspects.virtualization.podman ];` inside the `den.aspects.services.searxng` aspect so it brings its runtime dependency automatically.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Secret config not optional ✓ Resolved 🐞 Bug ☼ Reliability
Description
The aspect reads cfg.secretKeyFile and calls builtins.pathExists unconditionally in the let;
combined with secretKeyFile having no default, any host that includes the aspect but doesn’t fully
configure services.searxng.secretKeyFile will fail evaluation (even if SearXNG is meant to be
disabled).
Code

modules/den/aspects/services/searxng.nix[R30-37]

+    nixos = { host, config, pkgs, ... }: let
+      cfg = host.settings.services.searxng or { };
+      svcName = cfg.instance or "searxng";
+      dataVolume = "${svcName}-data";
+      secretName = "searxng-secret-key";
+      ageFile = cfg.secretKeyFile;
+      provisioned = builtins.pathExists ageFile;
+    in lib.mkMerge [
Evidence
cfg.secretKeyFile is dereferenced and passed into builtins.pathExists before any mkIf gating,
so a missing value will throw during evaluation rather than producing the intended warning-only
behavior.

modules/den/aspects/services/searxng.nix[24-37]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ageFile = cfg.secretKeyFile; provisioned = builtins.pathExists ageFile;` is evaluated unconditionally, but `secretKeyFile` has no default and is not safely optional. This can break evaluation for hosts that include the aspect without setting the secret.

## Issue Context
The PR description claims missing secrets are handled via warnings; current code instead can error before warnings are produced.

## Fix Focus Areas
- modules/den/aspects/services/searxng.nix[24-49]

## Suggested fix
- Make `secretKeyFile` optional: `type = lib.types.nullOr lib.types.path; default = null;`.
- Compute `provisioned` safely: `provisioned = (ageFile != null) && builtins.pathExists ageFile;`.
- Only reference `cfg.secretKeyFile` inside the `mkIf (cfg.enable or false)` branch (or otherwise guard it) so disabled services don’t require secret config.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +24 to +27
secretKeyFile = lib.mkOption {
type = lib.types.str;
description = "Path to agenix secret containing SEARXNG_SECRET_KEY.";
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Secretkeyfile wrong type 🐞 Bug ≡ Correctness

settings.secretKeyFile is declared as lib.types.str, but hvn-hyp1 sets it to a Nix path
(inputs.self + "/..."), which will fail option type checking and prevent evaluation/build.
Agent Prompt
## Issue description
`den.aspects.services.searxng.settings.secretKeyFile` is typed as a string, but it is used as a filesystem path (and the host config sets it as a path). This causes an evaluation-time type error.

## Issue Context
Other secret-related options in this repo (e.g., `secretRequests.<name>.ageFile`) are typed as `path`/`nullOr path`.

## Fix Focus Areas
- modules/den/aspects/services/searxng.nix[24-27]

## Suggested fix
- Change `secretKeyFile` option type to `lib.types.path` (or `lib.types.nullOr lib.types.path` with a default).
- If you intentionally want to accept strings, use `lib.types.either lib.types.str lib.types.path` and normalize with `toString`/`/. +` before calling `builtins.pathExists` and passing to `secretRequests`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread modules/den/aspects/services/searxng.nix Outdated
Comment thread modules/den/aspects/services/searxng.nix Outdated
Comment on lines 76 to 80
den.aspects.core."remote-unlock"
den.aspects.services.mergerfs
den.aspects.secrets.agenix
den.aspects.services.searxng
den.aspects.core.network.tailscale

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Quadlet module not imported 🐞 Bug ≡ Correctness

hvn-hyp1 includes den.aspects.services.searxng, which sets virtualisation.quadlet.containers...,
but this host does not include den.aspects.virtualization.podman (the only aspect in this repo
that imports/enables Quadlet), so evaluation is likely to fail with an unknown
virtualisation.quadlet option.
Agent Prompt
## Issue description
The SearXNG aspect configures `virtualisation.quadlet.containers`, but hvn-hyp1 does not include the virtualization aspect that imports `quadlet-nix` and enables `virtualisation.quadlet`.

## Issue Context
Repo search shows only `den.aspects.virtualization.podman` imports `inputs.quadlet-nix.nixosModules.quadlet` and sets `virtualisation.quadlet.enable = true`.

## Fix Focus Areas
- modules/den/hosts/hvn-hyp1/default.nix[66-83]
- modules/den/aspects/services/searxng.nix[51-65]
- modules/den/aspects/virtualization/podman.nix[12-23]

## Suggested fix
- Add `den.aspects.virtualization.podman` to hvn-hyp1 `includes`, or
- Add `includes = [ den.aspects.virtualization.podman ];` inside the `den.aspects.services.searxng` aspect so it brings its runtime dependency automatically.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

- secretKeyFile is now nullOr str with default null (fixes Nix path type issue)
- Mount agenix secret via environmentFiles (SearXNG doesn't support file: prefix in env vars)
- Secret file must contain SEARXNG_SECRET_KEY=value format
- provisioned guards with null check (avoids pathExists on null)
- curl only installed when enabled + provisioned
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants