searxng: new chart (1.0.0+up2026.8.22.9fea41204)

SearXNG as a stateless Deployment plus a settings ConfigMap, with the four CI
steps a new chart needs: publish workflow, change-app-version choice list,
ci/searxng/ fixtures and a renovate.json5 block.

Three points where the chart is not a cyberchef with another name:

* The image entrypoint writes settings.yml into /etc/searxng only when the
  file is missing. The chart always mounts one by subPath, so the copy and
  the random-secret sed never run and the container needs no write access
  there - which is what allows readOnlyRootFilesystem: true.
* The signing key is a required OnePasswordItem, not a ConfigMap key. It
  signs the settings cookie and the image-proxy URLs, and the entrypoint's
  self-generating fallback cannot fire here, so without it the instance would
  run on the literal "ultrasecretkey" - which webapp.py refuses with exit 1.
* All three probes use /healthz, the only path the bot detection lets through
  unconditionally. Measured: with the limiter active, "/" answers 429 while
  /healthz still answers 200.

ingress.domain is read a second time outside the Ingress template, for
SEARXNG_BASE_URL, and carries its own required() there - so it stays
mandatory with the Ingress switched off.

The Renovate versioning regex deviates from the plan on the issue and matches
the commit hash WITHOUT capturing it. Renovate parses a "build" group with
Number.parseInt, so a hash becomes the digits leading it; measured on
renovate 41.173.1 and 44.48.0, the planned form reports an image pushed
earlier the same day as an update to a newer one. Reasoning in the block and
in README.
This commit is contained in:
jklein committed 2026-08-27 21:09:59 +02:00
1 parent 71f5d2d450
commit 46942db53a
18 files changed
+1388 -5

No files matched your search

+89
View File
@@ -0,0 +1,89 @@
{{- $replicas := include "searxng.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.searxng.replicas) -}}
{{- $settings := fromYaml (include "searxng.effectiveSettings" .Values.settings) -}}
SearXNG {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
{{- if .Values.ingress.enabled }}
ACCESS
https://{{ .Values.ingress.domain }}
The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}"
cluster issuer. Until it has been issued the browser shows a warning; follow it with
kubectl -n {{ .Release.Namespace }} get certificate tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
{{- else }}
ACCESS — NO INGRESS
ingress.enabled is false, so this release has no Ingress and no certificate.
ingress.domain is still required: it builds SEARXNG_BASE_URL, which SearXNG
puts into its OpenSearch description, into the signed image-proxy URLs and
into every absolute link. Reach the instance inside the cluster with
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80
and open http://localhost:8080. Note that links on the page will still point
at https://{{ .Values.ingress.domain }}/.
{{- end }}
{{ if eq $replicas "0" }}
SCALED TO ZERO
{{ if .Values.scaleDown }}scaleDown: true{{ else }}searxng.replicas: 0{{ end }} — the deployment runs with replicas: 0, so the endpoint
above does not answer until you set it back and upgrade again. Service{{ if .Values.ingress.enabled }} and
Ingress stay in place, so the certificate is not re-issued.{{ else }} stays in
place.{{ end }}
{{ end }}
CONFIGURATION
Instance name: {{ $settings.instanceName }}
settings.yml is rendered from the "settings" values into
{{ .Release.Name }}-{{ .Chart.Name }}-configmap and mounted at /etc/searxng/settings.yml.
It starts with use_default_settings: true and names only the deviations, so
every engine the image ships stays available and upstream engine changes
arrive with the app version instead of with a chart release.
Anything the chart does not model first-class goes into settings.extra in
SearXNG's own key names; it is merged over the generated block. Changing any
of it rolls the pods — the deployment hashes the ConfigMap, because a subPath
mount would otherwise never reach the running container.
WHAT THE GREEN CHECKMARK DOES AND DOES NOT MEAN
All three probes hit /healthz, the only path SearXNG's bot detection lets
through unconditionally. Its handler returns a fixed "OK". So a Ready pod
means the worker answers — not that a search works, not that any engine is
reachable, and not that a login placed in front of the Ingress is up (probes
go straight to the pod and never pass the Ingress).
{{- if not $settings.limiter }}
BOT DETECTION IS OFF
settings.limiter is false and no Valkey is configured. The instance relies on
whatever sits in front of it — put authentication there via
ingress.annotations if it must not be open to everyone.
{{- else if not .Values.secrets.valkeySecretRef }}
BOT DETECTION IS ON WITHOUT A VALKEY
settings.limiter is true but secrets.valkeySecretRef is unset. The limiter
needs a Valkey to keep its counters, so it does nothing at all: SearXNG logs
ERROR:searx.limiter: The limiter requires Valkey, please consult the
documentation: https://docs.searxng.org/admin/searx.limiter.html
once at worker start, the pod becomes Ready, and every request is served
unlimited. This release is therefore NOT rate limited, and the only trace of
that is one line at the top of the log. Set secrets.valkeySecretRef, or set
settings.limiter back to false.
{{- end }}
ROTATING THE SIGNING KEY
secrets.searxngSecretRef carries the key that signs the settings cookie and
the image-proxy URLs. Pointing it at a DIFFERENT 1Password item rolls the
pods; changing the value INSIDE the same item does not — Helm never sees that
content. After such a rotation:
kubectl -n {{ .Release.Namespace }} rollout restart deployment/{{ .Release.Name }}-{{ .Chart.Name }}-deployment
A new key invalidates every user's saved preferences cookie, which is a
logout-shaped event for the people using it.
NOTHING TO BACK UP
The pod holds no state worth keeping: settings.yml comes from the ConfigMap,
the signing key from the Secret, and the SQLite caches under /tmp plus
/var/cache/searxng are emptyDirs that refill on their own. App-version
upgrades and rollbacks therefore carry no data risk.
+349
View File
@@ -0,0 +1,349 @@
{{/*
Render the body of a container "resources:" block from a values dict of the form
resources:
limitFactor: 3 # optional, default 3
requests: { cpu, memory, ephemeral-storage / ephemeralStorage, ... }
limits: { ... } # optional, every entry optional
Rules (variant B, see issue #19):
- requests are rendered as given (camelCase "ephemeralStorage" is normalized
to the Kubernetes key "ephemeral-storage").
- an explicitly set limit always wins.
- a missing memory / ephemeral-storage limit is computed as
limitFactor x the corresponding request.
- a missing cpu limit is NOT set at all (the kubelet then treats CPU as
unlimited; CPU is compressible, so this cannot kill the workload).
Usage (the values path to the resources dict varies per chart):
resources:
{{- include "searxng.resources" .Values.<app>.resources | nindent 12 }}
NOTE: charts in this repo deliberately have no dependencies, so this helper is
duplicated into every chart's templates/_helpers.tpl with the chart's name as
define prefix. template-chart holds the reference implementation.
*/}}
{{- define "searxng.resources" -}}
{{- $resources := . | default dict -}}
{{- $factor := 3.0 -}}
{{- if not (kindIs "invalid" $resources.limitFactor) -}}
{{- $factor = float64 $resources.limitFactor -}}
{{- end -}}
{{- $requests := dict -}}
{{- range $key, $value := ($resources.requests | default dict) -}}
{{- if not (kindIs "invalid" $value) -}}
{{- $_ := set $requests (include "searxng.resourceKey" $key) $value -}}
{{- end -}}
{{- end -}}
{{- $limits := dict -}}
{{- range $key, $value := ($resources.limits | default dict) -}}
{{- if not (kindIs "invalid" $value) -}}
{{- $_ := set $limits (include "searxng.resourceKey" $key) $value -}}
{{- end -}}
{{- end -}}
{{- range $key, $request := $requests -}}
{{- if and (ne $key "cpu") (not (hasKey $limits $key)) -}}
{{- $_ := set $limits $key (include "searxng.multiplyQuantity" (dict "quantity" $request "factor" $factor)) -}}
{{- end -}}
{{- end -}}
{{- $out := dict -}}
{{- with $requests }}{{- $_ := set $out "requests" . }}{{- end -}}
{{- with $limits }}{{- $_ := set $out "limits" . }}{{- end -}}
{{- toYaml $out -}}
{{- end -}}
{{/*
Normalize a resource name from values to its Kubernetes key.
*/}}
{{- define "searxng.resourceKey" -}}
{{- if eq . "ephemeralStorage" -}}ephemeral-storage{{- else -}}{{ . }}{{- end -}}
{{- end -}}
{{/*
Multiply a Kubernetes quantity by a factor, keeping its suffix.
Input: dict "quantity" <string|number> "factor" <float64>.
Handles plain numbers ("2", "0.5", 1073741824), decimal ("1.5Gi"), binary
("512Mi", "1Gi", "10Ki") and milli/SI suffixes ("100m", "500M"). Integer
results are printed without a decimal point.
*/}}
{{- define "searxng.multiplyQuantity" -}}
{{- $quantity := printf "%v" .quantity -}}
{{- $number := regexFind "^[0-9]+(\\.[0-9]+)?" $quantity -}}
{{- if not $number -}}
{{- fail (printf "cannot parse resource quantity %q" $quantity) -}}
{{- end -}}
{{- $suffix := trimPrefix $number $quantity -}}
{{- if not (regexMatch "^(m|k|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$" $suffix) -}}
{{- fail (printf "unsupported resource quantity suffix %q in %q" $suffix $quantity) -}}
{{- end -}}
{{- $product := mulf (float64 $number) .factor -}}
{{- if eq $product (floor $product) -}}
{{- printf "%d%s" (int64 $product) $suffix -}}
{{- else -}}
{{- printf "%v%s" $product $suffix -}}
{{- end -}}
{{- end -}}
{{/*
Render the replica count for a Deployment/StatefulSet.
Input: dict
"scaleDown" the chart-global .Values.scaleDown flag (may be nil)
"replicas" the workload's replicas value (may be nil)
Rules (issue #25):
- scaleDown: true takes precedence and always yields 0.
- otherwise an explicitly set replicas value is used as given, including an
explicit 0 — Helm's "default" would swallow 0, hence the explicit
kindIs "invalid" nil check.
- a missing replicas value defaults to 1.
Usage (the values path to the workload's replicas varies per chart):
replicas: {{ include "searxng.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.<app>.replicas) }}
NOTE: like the resources helper above, this is duplicated into every chart's
templates/_helpers.tpl with the chart's name as define prefix. template-chart
holds the reference implementation.
*/}}
{{- define "searxng.replicas" -}}
{{- if .scaleDown -}}
0
{{- else if not (kindIs "invalid" .replicas) -}}
{{- .replicas -}}
{{- else -}}
1
{{- end -}}
{{- end -}}
{{/*
Merge a user-supplied values sub-tree onto a dict of chart defaults so that a
null / empty override falls back to the defaults instead of erasing them.
Input: dict "defaults" <dict> "given" <dict|nil> "path" <string, optional
values path used in error messages>. Output: YAML of the merged dict (consume
it with fromYaml).
Why this exists (issues #61 and #99): Helm merges values maps recursively, but
an override key written without children — e.g. a leftover
securityContext:
after deleting its last remaining sub-key — is YAML null. Where the chart
defines a default, Helm treats that null as a deletion of the default; the
template then renders "securityContext: null": no readOnlyRootFilesystem, no
dropped capabilities, no allowPrivilegeEscalation: false. The workload starts
and reports healthy while being unhardened, so nothing points at the mistake.
The same shape erases a probe (it disappears) or the resources block (no
requests, no computed limits). This helper defines the opposite semantics: an
empty block means "chart defaults apply".
Rules:
- a nil "given" is treated as an empty dict (all defaults apply).
- a key whose override value is null keeps the default (nulls never delete).
- a key present in both as a map is merged recursively, so a partially
filled block only overrides the keys it actually sets.
- a scalar override for a key the defaults define as a map is rejected with
a message naming the values path, instead of failing later with a template
field error.
- any other set value wins as given, including an explicit false or 0 —
Helm's "default" and sprig's "mergeOverwrite" would both swallow those
zero values, hence the explicit kindIs "invalid" nil check.
- keys only present in the override are passed through unchanged.
NOTE: like the helpers above, this is duplicated into every chart's
templates/_helpers.tpl with the chart's name as define prefix, because charts
in this repo deliberately have no dependencies. template-chart holds the
reference implementation.
*/}}
{{- define "searxng.defaultedDict" -}}
{{- $defaults := .defaults | default dict -}}
{{- $given := .given -}}
{{- $path := .path | default "value" -}}
{{- if kindIs "invalid" $given -}}
{{- $given = dict -}}
{{- end -}}
{{- if not (kindIs "map" $given) -}}
{{- fail (printf "%s must be a mapping or null, got %s (%v)" $path (kindOf $given) $given) -}}
{{- end -}}
{{- $out := deepCopy $defaults -}}
{{- range $key, $value := $given -}}
{{- if not (kindIs "invalid" $value) -}}
{{- $default := index $defaults $key -}}
{{- if and (kindIs "map" $value) (kindIs "map" $default) -}}
{{- $_ := set $out $key (fromYaml (include "searxng.defaultedDict" (dict "defaults" $default "given" $value "path" (printf "%s.%s" $path $key)))) -}}
{{- else if kindIs "map" $default -}}
{{- fail (printf "%s.%s must be a mapping or null, got %s (%v)" $path $key (kindOf $value) $value) -}}
{{- else -}}
{{- $_ := set $out $key $value -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- toYaml $out -}}
{{- end -}}
{{/*
Pick one sub-block out of a values block that may itself be null.
Input: dict "block" <dict|nil> "key" <string> "path" <string>.
Returns YAML of dict "value" <the sub-block, may be nil>, after rejecting a
non-mapping block with a message that names the values path. Needed because
".Values.<app>.securityContext" can itself be erased, and indexing into nil
inside a template is not an error but silently yields nothing.
*/}}
{{- define "searxng.subBlock" -}}
{{- $block := .block -}}
{{- if kindIs "invalid" $block -}}
{{- $block = dict -}}
{{- end -}}
{{- if not (kindIs "map" $block) -}}
{{- fail (printf "%s must be a mapping or null, got %s (%v)" .path (kindOf $block) $block) -}}
{{- end -}}
{{- toYaml (dict "value" (index $block .key)) -}}
{{- end -}}
{{/*
The effective security contexts, probes and resources (issue #99).
The defaults below MUST stay in sync with the corresponding blocks in
values.yaml, which stays the documented, user-facing place for them; this copy
is only the fallback for the case where an override has erased the block.
Two rules that these fallbacks exist to demonstrate (README, "Chart style
guidelines"):
- Reproduce THIS chart's defaults, not the repo standard. Where a chart
deviates on purpose (a root-starting image, a non-default limitFactor, a
pinned uid), the fallback must reproduce the deviation. Restoring the
"clean" standard here is not tidying, it is an outage - in one chart it
would move the runtime uid from 1000 to 911 and make existing volume data
inaccessible.
- Carry the probe's CONTEXT, not just its timings. A probe default without
its port name, Host header or exec command is worse than useless: in one
chart the probe vanished entirely, in another it answered 403 and the pod
never became Ready.
*/}}
{{- define "searxng.podSecurityContext" -}}
{{- $given := (fromYaml (include "searxng.subBlock" (dict "block" . "key" "pod" "path" "searxng.securityContext"))).value -}}
{{/* 977 is the image's searxng user, deliberately not the repo's generic
20000/1000: the image creates /etc/searxng and /var/cache/searxng owned
977:977 and the entrypoint checks that ownership by name. A "cleaned up"
standard uid here would leave the config mount unreadable. */}}
{{- $defaults := dict
"runAsNonRoot" true
"runAsUser" 977
"runAsGroup" 977
"fsGroup" 977
"fsGroupChangePolicy" "Always"
"seccompProfile" (dict "type" "RuntimeDefault") -}}
{{- include "searxng.defaultedDict" (dict "defaults" $defaults "given" $given "path" "searxng.securityContext.pod") -}}
{{- end -}}
{{- define "searxng.containerSecurityContext" -}}
{{- $given := (fromYaml (include "searxng.subBlock" (dict "block" . "key" "container" "path" "searxng.securityContext"))).value -}}
{{- $defaults := dict
"allowPrivilegeEscalation" false
"readOnlyRootFilesystem" true
"runAsNonRoot" true
"capabilities" (dict "drop" (list "ALL") "add" (list)) -}}
{{- include "searxng.defaultedDict" (dict "defaults" $defaults "given" $given "path" "searxng.securityContext.container") -}}
{{- end -}}
{{/* All three probe fallbacks use /healthz, not "/", and that path is part of
the context this fallback has to carry (guideline 5). /healthz is the only
path SearXNG's bot detection lets through unconditionally
(searx/limiter.py, filter_request), and its handler returns a fixed "OK"
without touching an engine or the cache - so liveness hangs on no external
dependency either. The port NAME matters just as much: the container port
is named "http" and a fallback without it would address nothing. */}}
{{- define "searxng.livenessProbe" -}}
{{- $defaults := dict
"httpGet" (dict "path" "/healthz" "port" "http")
"periodSeconds" 10
"timeoutSeconds" 5
"failureThreshold" 3 -}}
{{- include "searxng.defaultedDict" (dict "defaults" $defaults "given" . "path" "searxng.livenessProbe") -}}
{{- end -}}
{{- define "searxng.readinessProbe" -}}
{{- $defaults := dict
"httpGet" (dict "path" "/healthz" "port" "http")
"periodSeconds" 10
"timeoutSeconds" 5
"failureThreshold" 3 -}}
{{- include "searxng.defaultedDict" (dict "defaults" $defaults "given" . "path" "searxng.readinessProbe") -}}
{{- end -}}
{{- define "searxng.startupProbe" -}}
{{- $defaults := dict
"httpGet" (dict "path" "/healthz" "port" "http")
"periodSeconds" 5
"timeoutSeconds" 5
"failureThreshold" 30 -}}
{{- include "searxng.defaultedDict" (dict "defaults" $defaults "given" . "path" "searxng.startupProbe") -}}
{{- end -}}
{{/*
The effective resources block, fed into the resources helper above so that an
erased "resources:" still yields the chart's requests and computed limits
instead of a container without any resource accounting.
*/}}
{{- define "searxng.effectiveResources" -}}
{{- $defaults := dict
"limitFactor" 3
"requests" (dict "cpu" 0.1 "memory" "256Mi" "ephemeral-storage" "64Mi") -}}
{{- $merged := fromYaml (include "searxng.defaultedDict" (dict "defaults" $defaults "given" . "path" "searxng.resources")) -}}
{{- include "searxng.resources" $merged -}}
{{- end -}}
{{/*
The effective "settings" block, i.e. this chart's user-facing configuration
keys with the same null-safe semantics as the blocks above: an erased
"settings:" means "chart defaults apply" rather than an instance whose name,
safe-search level and image proxy silently become empty.
The defaults MUST stay in sync with the settings block in values.yaml.
*/}}
{{- define "searxng.effectiveSettings" -}}
{{- $defaults := dict
"instanceName" "SearXNG"
"safeSearch" 0
"autocomplete" "duckduckgo"
"imageProxy" true
"limiter" false
"publicInstance" false
"extra" dict -}}
{{- include "searxng.defaultedDict" (dict "defaults" $defaults "given" . "path" "settings") -}}
{{- end -}}
{{/*
The body of /etc/searxng/settings.yml.
Two things this is doing, both deliberate:
- It starts from use_default_settings: true and names ONLY the deviations,
so the ~3500-line settings.yml of the image stays the source of truth for
engines and every upstream change to them arrives without a chart release.
- "settings.extra" is merged OVER the block the first-class keys build, with
defaultedDict rather than sprig's mergeOverwrite. That difference is
load-bearing: mergeOverwrite drops zero values from the source, so an
"extra" entry setting some flag to false or 0 would be silently ignored -
a config value that looks set and does nothing, which is exactly the class
of bug the schema work (#68) exists to remove.
server.secret_key is deliberately absent: it arrives as SEARXNG_SECRET from
the 1Password Secret (settings_defaults.py lets the environment override the
file), so the signing key never lands in a ConfigMap.
*/}}
{{- define "searxng.settingsYaml" -}}
{{- $settings := fromYaml (include "searxng.effectiveSettings" .Values.settings) -}}
{{- $base := dict
"use_default_settings" true
"general" (dict "instance_name" $settings.instanceName)
"search" (dict "safe_search" $settings.safeSearch "autocomplete" $settings.autocomplete)
"server" (dict
"limiter" $settings.limiter
"public_instance" $settings.publicInstance
"image_proxy" $settings.imageProxy) -}}
{{- $merged := fromYaml (include "searxng.defaultedDict" (dict "defaults" $base "given" $settings.extra "path" "settings.extra")) -}}
{{- toYaml $merged -}}
{{- end -}}
@@ -0,0 +1,11 @@
{{- if .Values.secrets.valkeySecretRef }}
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-valkey-secret
spec:
# Optional, and only meaningful with settings.limiter: true. The item must
# carry a "valkey-url" field pointing at an EXTERNAL Valkey/Redis; the chart
# deploys none of its own, because charts in this repo have no dependencies.
itemPath: {{ .Values.secrets.valkeySecretRef }}
{{- end }}
+14
View File
@@ -0,0 +1,14 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: "{{ .Release.Name }}-{{ .Chart.Name }}-configmap"
data:
# Mounted at /etc/searxng/settings.yml via subPath. That path is not free
# choice: the image exports __SEARXNG_SETTINGS_PATH=/etc/searxng/settings.yml
# and its entrypoint creates the file from its own template only when it does
# NOT exist. Because this mount is always there, the entrypoint finds the
# file, skips both the copy and the sed that would otherwise invent a random
# secret_key, and never needs to write into /etc/searxng - which is what lets
# the container run with a read-only root filesystem.
settings.yml: |
{{- include "searxng.settingsYaml" . | nindent 4 }}
+169
View File
@@ -0,0 +1,169 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
spec:
replicas: {{ include "searxng.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.searxng.replicas) }}
selector:
matchLabels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
template:
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
annotations:
# Hash of the rendered ConfigMap: a changed settings.yml changes the
# pod template and thus rolls the pods. Without it the change would
# never reach the running container - settings.yml is a subPath mount,
# which the kubelet never refreshes, and SearXNG reads its
# configuration once at start.
checksum/config: {{ include (print $.Template.BasePath "/searxng.configmap.yaml") . | sha256sum }}
# Hashes over the rendered OnePasswordItems. Separately named on
# purpose: they only promise that the config SOURCE changed. Repointing
# a *SecretRef at another vault item rolls the pods; rotating the value
# inside the same item does not, because Helm never sees the content
# (the operator fills the Secret in-cluster) and the values are consumed
# as environment variables, which Kubernetes never refreshes in a
# running container. After such a rotation, restart by hand.
checksum/secret-ref: {{ include (print $.Template.BasePath "/searxng.secret.yaml") . | sha256sum }}
checksum/secret-ref-valkey: {{ include (print $.Template.BasePath "/searxng-valkey.secret.yaml") . | sha256sum }}
spec:
automountServiceAccountToken: false
securityContext:
{{- include "searxng.podSecurityContext" .Values.searxng.securityContext | nindent 8 }}
containers:
- name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
# Registry written out in full on purpose: an unqualified reference
# would put "searxng" in the registry position, which says nothing
# about where the image comes from and is reported as an untrusted
# registry (AVD-KSV-0125). See ci/trivy/data/ksv0125.yaml.
image: "docker.io/searxng/searxng:{{ .Chart.AppVersion }}"
imagePullPolicy: IfNotPresent
env:
# The signing key for the settings cookie and the image-proxy URLs.
# Set by the chart rather than left to searxng.env, because SearXNG
# exits 1 when server.secret_key is still the upstream placeholder
# and the image's self-generating fallback cannot fire here (it
# only runs when settings.yml is missing, and this chart always
# mounts one).
- name: SEARXNG_SECRET
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-{{ .Chart.Name }}-secret"
key: "secret-key"
# server.base_url. This is the SECOND read of ingress.domain, well
# outside the Ingress template, so it carries its own required()
# (README, "Values that must not lie", point 1). Without it, an
# install with ingress.enabled: false would render base_url as
# "https:///" - the pod would go green while the OpenSearch
# description, every signed image-proxy URL and every absolute link
# pointed at a broken address.
- name: SEARXNG_BASE_URL
value: "https://{{ required "A Domain/Host must be provided (ingress.domain), it is used for SEARXNG_BASE_URL even when the Ingress is disabled" .Values.ingress.domain }}/"
{{- if .Values.secrets.valkeySecretRef }}
# Only rendered when an external Valkey/Redis is configured. The
# limiter is what needs it; see settings.limiter in values.yaml.
- name: SEARXNG_VALKEY_URL
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-{{ .Chart.Name }}-valkey-secret"
key: "valkey-url"
{{- end }}
{{- range $value := .Values.searxng.env }}
{{- if $value.value }}
- name: {{ $value.name | quote }}
# quote is required, not cosmetic: a value like "8080" or "true"
# renders unquoted as a YAML number/bool, and the API server then
# rejects the whole manifest with "cannot unmarshal number into
# Go struct field EnvVar.value of type string". Reachable with
# the very first entry someone adds here - SEARXNG_LIMITER and
# GRANIAN_PORT are both of that shape.
value: {{ tpl $value.value $ | quote }}
{{- else if $value.valueFrom }}
{{- if $value.valueFrom.secretKeyRef }}
- name: {{ $value.name | quote }}
valueFrom:
secretKeyRef:
name: {{ tpl $value.valueFrom.secretKeyRef.name $ | quote }}
key: {{ tpl $value.valueFrom.secretKeyRef.key $ | quote }}
{{- else if $value.valueFrom.configMapKeyRef }}
- name: {{ $value.name | quote }}
valueFrom:
configMapKeyRef:
name: {{ tpl $value.valueFrom.configMapKeyRef.name $ | quote }}
key: {{ tpl $value.valueFrom.configMapKeyRef.key $ | quote }}
{{- end }}
{{- end }}
{{- end }}
ports:
# 8080 is the image's own GRANIAN_PORT and its EXPOSE. It is not a
# value, because a value here could lie: changing it would move the
# containerPort and the probes while granian kept listening on
# 8080. The way to move the port is to set both GRANIAN_PORT (or
# its SEARXNG_PORT alias, which the entrypoint maps onto it) in
# searxng.env and this number - which means a chart change, on
# purpose.
#
# The port NAME is load-bearing and must not be renamed: the
# Service selects it by name and so do all three probes.
- containerPort: 8080
protocol: TCP
name: http
# Probes, resources and both security contexts go through the
# null-safe helpers (#99): an erased block means "chart defaults
# apply" instead of a vanished probe or an unhardened container.
# Never wrap these in an "if" - a guard hides the erased case
# completely instead of rendering a visible null.
livenessProbe:
{{- include "searxng.livenessProbe" .Values.searxng.livenessProbe | nindent 12 }}
readinessProbe:
{{- include "searxng.readinessProbe" .Values.searxng.readinessProbe | nindent 12 }}
startupProbe:
{{- include "searxng.startupProbe" .Values.searxng.startupProbe | nindent 12 }}
resources:
{{- include "searxng.effectiveResources" .Values.searxng.resources | nindent 12 }}
securityContext:
{{- include "searxng.containerSecurityContext" .Values.searxng.securityContext | nindent 12 }}
volumeMounts:
- name: settings
mountPath: /etc/searxng/settings.yml
subPath: settings.yml
readOnly: true
# The two writable paths below are what keeps
# readOnlyRootFilesystem: true possible. They are image facts
# rather than user configuration, so they are not values.
- name: tmp
# NOT optional, and not obvious: SearXNG keeps three SQLite
# caches (data, engines, favicons) and searx/cache.py falls back
# to tempfile.gettempdir() when no db_url is configured - i.e.
# /tmp. webapp.py init() connects to two of them before the
# first request, so a read-only /tmp is not a degraded cache, it
# is a container that never starts.
mountPath: /tmp
- name: cache
# The image's __SEARXNG_DATA_PATH. The entrypoint aborts with
# exit 127 if the directory is missing, so it has to exist -
# which it does even unmounted, since the image ships it. It is
# mounted anyway and deliberately: nothing in this version writes
# there, but it is the path the application declares as its data
# directory, and the day a release starts using it a read-only
# root filesystem would turn that into a crash loop.
#
# EXPECTED, not a fault: fsGroup leaves this emptyDir owned
# root:977, so the entrypoint prints a multi-line "!!! WARNING
# ... is not owned by searxng:searxng" and carries on. It cannot
# chown as a non-root process and does not need to - the group
# bit is enough.
mountPath: /var/cache/searxng
volumes:
- name: settings
configMap:
name: "{{ .Release.Name }}-{{ .Chart.Name }}-configmap"
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
restartPolicy: Always
+31
View File
@@ -0,0 +1,31 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-ingress
annotations:
cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided (ingress.clusterIssuer)" .Values.ingress.clusterIssuer }}
# Extra annotations are MERGED with the cert-manager annotation above, not
# substituted for it. This is where forward-auth (Authentik and friends)
# goes; see the example in values.yaml.
{{- with .Values.ingress.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
ingressClassName: {{ .Values.ingress.className }}
rules:
- host: {{ required "A Domain/Host must be provided (ingress.domain)" .Values.ingress.domain }}
http:
paths:
- backend:
service:
name: {{ .Release.Name }}-{{ .Chart.Name }}-service
port:
name: http
path: /
pathType: Prefix
tls:
- hosts:
- {{ required "A Domain/Host must be provided (ingress.domain)" .Values.ingress.domain }}
secretName: tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
{{- end }}
+10
View File
@@ -0,0 +1,10 @@
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-secret
spec:
# The item must carry a "secret-key" field; it becomes SEARXNG_SECRET and
# thereby server.secret_key, which signs the settings cookie and the
# image-proxy URLs. Without it SearXNG exits 1 at start (webapp.py refuses
# to run on the upstream placeholder), so the value is required.
itemPath: {{ required "You must provide a searxng Secret Reference (secrets.searxngSecretRef)" .Values.secrets.searxngSecretRef }}
+13
View File
@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-service
spec:
selector:
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
ports:
- protocol: TCP
port: 80
targetPort: http
name: http
type: ClusterIP