Add modular service configuration with SearXNG and Nginx

- Create modules/ directory with reusable NixOS modules
- Add system module for main user configuration
- Add podman module for rootless container support
- Add nginx module with automatic Let's Encrypt SSL
- Add searxng module with Anubis AI firewall protection
- Configure SearXNG at search.ashisgreat.xyz
- Enable nginx reverse proxy with HTTPS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ashisgreat22 2026-03-17 19:47:43 +01:00
parent 5dcb85e56d
commit 24d01ac630
7 changed files with 469 additions and 1 deletions

9
modules/default.nix Normal file
View file

@ -0,0 +1,9 @@
# Module exports
{
imports = [
./system.nix
./podman.nix
./nginx.nix
./searxng.nix
];
}

93
modules/nginx.nix Normal file
View file

@ -0,0 +1,93 @@
# Nginx Reverse Proxy Module
# Provides: Nginx with automatic Let's Encrypt certificates
#
# Usage:
# myModules.nginx = {
# enable = true;
# email = "your@email.com";
# domains = {
# "search.example.com" = {
# port = 8888;
# };
# };
# };
{
config,
lib,
pkgs,
...
}:
let
cfg = config.myModules.nginx;
in
{
options.myModules.nginx = {
enable = lib.mkEnableOption "Nginx reverse proxy with Let's Encrypt";
email = lib.mkOption {
type = lib.types.str;
example = "admin@example.com";
description = "Email address for Let's Encrypt registration";
};
domains = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule {
options = {
port = lib.mkOption {
type = lib.types.port;
description = "Local port to proxy to";
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Extra Nginx config for this location";
};
};
});
default = { };
description = "Domains to configure with their proxy targets";
};
};
config = lib.mkIf cfg.enable {
# Open HTTP/HTTPS ports
networking.firewall.allowedTCPPorts = [ 80 443 ];
# ACME (Let's Encrypt) configuration
security.acme = {
acceptTerms = true;
defaults.email = cfg.email;
certs = lib.mapAttrs' (domain: opts: {
name = domain;
value = { };
}) cfg.domains;
};
# Nginx configuration
services.nginx = {
enable = true;
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
virtualHosts = lib.mapAttrs' (domain: opts: {
name = domain;
value = {
enableACME = true;
forceSSL = true;
locations."/" = {
proxyPass = "http://127.0.0.1:${toString opts.port}";
extraConfig = opts.extraConfig;
};
};
}) cfg.domains;
};
# Ensure nginx user can access ACME certs
users.users.nginx.extraGroups = [ "acme" ];
};
}

32
modules/podman.nix Normal file
View file

@ -0,0 +1,32 @@
# Podman Module
# Provides: Rootless container runtime configuration
{
config,
lib,
pkgs,
...
}:
let
cfg = config.myModules.podman;
mainUser = config.myModules.system.mainUser;
in
{
options.myModules.podman = {
enable = lib.mkEnableOption "Podman container runtime";
};
config = lib.mkIf cfg.enable {
virtualisation.podman = {
enable = true;
dockerCompat = true;
defaultNetwork.settings.dns_enabled = true;
};
# Enable OCI containers (quadlet/podman containers)
virtualisation.oci-containers.backend = "podman";
# Give main user access to podman
users.users.${mainUser}.extraGroups = [ "podman" ];
};
}

289
modules/searxng.nix Normal file
View file

@ -0,0 +1,289 @@
# SearXNG Module (Rootless Podman)
# Provides: Private meta-search engine running in a rootless container
#
# Usage:
# myModules.searxng = {
# enable = true;
# port = 8888;
# domain = "search.example.com";
# };
{
config,
lib,
pkgs,
...
}:
let
cfg = config.myModules.searxng;
mainUser = config.myModules.system.mainUser;
mainUserUid = toString config.users.users.${mainUser}.uid;
anubisPolicy = pkgs.writeText "anubis-policy.yml" ''
bots:
- name: "Allow OpenSearch"
action: ALLOW
path_regex: ".*opensearch\\.xml.*"
- name: "Catch-All"
user_agent_regex: ".*"
action: CHALLENGE
'';
faviconsConfig = pkgs.writeText "favicons.toml" ''
[favicons]
cfg_schema = 1
[favicons.cache]
db_url = "/var/cache/searxng/faviconcache.db"
LIMIT_TOTAL_BYTES = 2147483648
[favicons.proxy.resolver_map]
google = "searx.favicons.resolver.google_resolver"
duckduckgo = "searx.favicons.resolver.duckduckgo_resolver"
'';
in
{
options.myModules.searxng = {
enable = lib.mkEnableOption "SearXNG meta-search engine";
port = lib.mkOption {
type = lib.types.port;
default = 8888;
description = "Port to expose SearXNG on localhost";
};
domain = lib.mkOption {
type = lib.types.str;
example = "search.example.com";
description = "Public domain name for SearXNG";
};
instanceName = lib.mkOption {
type = lib.types.str;
default = "SearXNG";
description = "Name displayed in the search interface";
};
donations = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
description = "Map of donation platform names to URLs (e.g. { patreon = '...'; })";
};
};
config = lib.mkIf cfg.enable {
# Ensure Podman is enabled
myModules.podman.enable = true;
# 1. Create Bridge Network
systemd.services."create-searxng-network" = {
serviceConfig.Type = "oneshot";
serviceConfig.User = mainUser;
serviceConfig.RemainAfterExit = true;
after = [ "user-runtime-dir@${mainUserUid}.service" ];
requires = [ "user-runtime-dir@${mainUserUid}.service" ];
path = [
pkgs.podman
pkgs.shadow
];
script = ''
export PATH=/run/wrappers/bin:$PATH
export XDG_RUNTIME_DIR="/run/user/${mainUserUid}"
export HOME="/home/${mainUser}"
if ! podman network exists searxng-net; then
echo "Creating searxng-net..."
podman network create searxng-net --subnet 10.89.2.0/24
else
echo "searxng-net already exists."
fi
'';
};
# 2. Valkey Container (Cache/Limiter)
virtualisation.oci-containers.containers."searxng-valkey" = {
image = "docker.io/valkey/valkey:alpine";
labels = { "io.containers.autoupdate" = "registry"; };
cmd = [
"valkey-server"
"--save"
""
"--appendonly"
"no"
];
extraOptions = [
"--network=searxng-net"
"--network-alias=valkey"
];
};
# 3. SearXNG Container
virtualisation.oci-containers.containers."searxng" = {
image = "docker.io/searxng/searxng:latest";
environment = {
"SEARXNG_BASE_URL" = "https://${cfg.domain}";
"SEARXNG_REDIS_URL" = "valkey://valkey:6379";
"SEARXNG_URL_BASE" = "https://${cfg.domain}";
"GRANIAN_HOST" = "0.0.0.0";
};
environmentFiles = [
config.sops.templates."searxng.env".path
];
extraOptions = [
"--network=searxng-net"
"--network-alias=searxng"
"--cap-drop=ALL"
"--cap-add=CHOWN"
"--cap-add=SETGID"
"--cap-add=SETUID"
"--cap-add=DAC_OVERRIDE"
"--dns=9.9.9.9"
"--dns=1.1.1.1"
];
volumes = [
"${config.sops.templates."searxng_settings.yml".path}:/etc/searxng/settings.yml:ro"
"${faviconsConfig}:/etc/searxng/favicons.toml:ro"
"searxng-cache:/var/cache/searxng"
];
dependsOn = [ "searxng-valkey" ];
};
# 4. Anubis Container (AI Firewall)
virtualisation.oci-containers.containers."searxng-anubis" = {
image = "ghcr.io/techarohq/anubis:latest";
labels = { "io.containers.autoupdate" = "registry"; };
ports = [ "127.0.0.1:${toString cfg.port}:8080" ];
environment = {
"TARGET" = "http://searxng:8080";
"BIND" = ":8080";
"POLICY_FNAME" = "/etc/anubis/policy.yml";
};
extraOptions = [
"--network=searxng-net"
"--network-alias=searxng-anubis"
];
volumes = [
"${anubisPolicy}:/etc/anubis/policy.yml:ro"
];
dependsOn = [ "searxng" ];
};
# SOPS templates
sops.templates."searxng.env" = {
owner = mainUser;
content = ''
SEARXNG_SECRET_KEY=${config.sops.placeholder.searxng_secret_key}
'';
};
sops.templates."searxng_settings.yml" = {
owner = mainUser;
content = ''
use_default_settings: true
general:
debug: false
instance_name: "${cfg.instanceName}"
contact_url: false
issue_url: false
donation_url: ${if cfg.donations ? "Monero" then "\"${cfg.donations.Monero}\"" else "false"}
donations:
${lib.concatStringsSep "\n " (
lib.mapAttrsToList (name: url: "${name}: \"${url}\"") cfg.donations
)}
outgoing:
request_timeout: 10.0
connect_timeout: 6.0
max_retry_count: 3
enable_ipv6: false
search:
safe_search: 0
favicon_resolver: "google"
autocomplete: "google"
default_lang: "en-US"
formats:
- html
- json
server:
port: 8080
bind_address: "0.0.0.0"
secret_key: "${config.sops.placeholder.searxng_secret_key}"
limiter: true
image_proxy: true
public_instance: true
default_http_headers:
Content-Security-Policy: "upgrade-insecure-requests; default-src 'none'; script-src 'self'; style-src 'self' 'sha256-/ldGxQqxNIMRftg3AGsPF+F281wiBPECUDcL2RJkxdU='; form-action 'self' https://github.com/searxng/searxng/issues/new; font-src 'self'; frame-ancestors 'self'; img-src 'self' data:; connect-src 'self' https://overpass-api.de; manifest-src 'self'"
ui:
default_theme: simple
default_theme_style: dark
static_use_hash: true
redis:
url: valkey://valkey:6379/0
'';
};
# Secret definitions
sops.secrets.searxng_secret_key = { };
# Rootless Overrides
systemd.services."podman-searxng".serviceConfig.User = lib.mkForce mainUser;
systemd.services."podman-searxng".environment = {
HOME = "/home/${mainUser}";
XDG_RUNTIME_DIR = "/run/user/${mainUserUid}";
};
systemd.services."podman-searxng".serviceConfig.Type = lib.mkForce "simple";
systemd.services."podman-searxng".serviceConfig.Delegate = true;
systemd.services."podman-searxng".after = [
"create-searxng-network.service"
"user-runtime-dir@${mainUserUid}.service"
"network-online.target"
];
systemd.services."podman-searxng".requires = [
"create-searxng-network.service"
"user-runtime-dir@${mainUserUid}.service"
"network-online.target"
];
systemd.services."podman-searxng-valkey".serviceConfig.User = lib.mkForce mainUser;
systemd.services."podman-searxng-valkey".environment = {
HOME = "/home/${mainUser}";
XDG_RUNTIME_DIR = "/run/user/${mainUserUid}";
};
systemd.services."podman-searxng-valkey".serviceConfig.Type = lib.mkForce "simple";
systemd.services."podman-searxng-valkey".serviceConfig.Delegate = true;
systemd.services."podman-searxng-valkey".after = [
"create-searxng-network.service"
"user-runtime-dir@${mainUserUid}.service"
"network-online.target"
];
systemd.services."podman-searxng-valkey".requires = [
"create-searxng-network.service"
"user-runtime-dir@${mainUserUid}.service"
"network-online.target"
];
systemd.services."podman-searxng-anubis".serviceConfig.User = lib.mkForce mainUser;
systemd.services."podman-searxng-anubis".environment = {
HOME = "/home/${mainUser}";
XDG_RUNTIME_DIR = "/run/user/${mainUserUid}";
};
systemd.services."podman-searxng-anubis".serviceConfig.Type = lib.mkForce "simple";
systemd.services."podman-searxng-anubis".serviceConfig.Delegate = true;
systemd.services."podman-searxng-anubis".after = [
"create-searxng-network.service"
"user-runtime-dir@${mainUserUid}.service"
"network-online.target"
];
systemd.services."podman-searxng-anubis".requires = [
"create-searxng-network.service"
"user-runtime-dir@${mainUserUid}.service"
"network-online.target"
];
};
}

25
modules/system.nix Normal file
View file

@ -0,0 +1,25 @@
# System Module
# Provides: Common system configuration options used by other modules
{
config,
lib,
pkgs,
...
}:
let
cfg = config.myModules.system;
in
{
options.myModules.system = {
mainUser = lib.mkOption {
type = lib.types.str;
default = "ashie";
description = "Main user account for running services";
};
};
config = {
# Nothing here by default - just provides the option
};
}