Runs Questarr on Kubernetes from the official
ghcr.io/doezer/questarr image.
The chart deploys a single-replica Deployment, a Service, an optional Ingress, and a
PersistentVolumeClaim for /app/data (SQLite database, config.yaml, and any other
state Questarr persists).
- Kubernetes >= 1.23
- Helm >= 3.8
- A StorageClass able to provision a ReadWriteOnce volume (unless you bring your own PVC)
The chart is not published to a Helm repository yet — install it from a clone of the repository:
git clone https://github.com/Doezer/questarr.git
cd questarr
helm install questarr charts/questarr --namespace questarr --create-namespaceThen reach the UI (the release notes print the exact command for your Service type):
kubectl port-forward -n questarr svc/questarr 5000:5000The first account created in the UI becomes the administrator.
Questarr keeps all of its state in one SQLite database on a ReadWriteOnce volume. A
second replica would corrupt it, so the chart refuses to render with replicaCount
above 1, and uses the Recreate strategy so two pods never hold the volume at once.
| Key | Default | Description |
|---|---|---|
image.repository |
ghcr.io/doezer/questarr |
Image repository |
image.tag |
"" |
Image tag; defaults to v + the chart's appVersion (e.g. v1.5.0) |
image.pullPolicy |
IfNotPresent |
Image pull policy |
imagePullSecrets |
[] |
Pull secrets for private registries |
replicaCount |
1 |
Must stay 1 (see above) |
strategy |
{type: Recreate} |
Deployment update strategy |
nameOverride |
"" |
Overrides the chart name |
fullnameOverride |
"" |
Overrides the generated resource names |
| Key | Default | Description |
|---|---|---|
questarr.port |
5000 |
HTTP port the server listens on (PORT) |
questarr.puid |
1000 |
UID the app runs as, applied by the container entrypoint |
questarr.pgid |
1000 |
GID the app runs as |
questarr.umask |
"022" |
umask for files the app creates |
questarr.basePath |
"" |
Serve from a subdirectory (QUESTARR_BASE_PATH), e.g. /questarr |
questarr.timezone |
"" |
Container timezone (TZ) |
questarr.databasePath |
/app/data/sqlite.db |
SQLite path (SQLITE_DB_PATH); must live under /app/data |
questarr.allowedOrigins |
"" |
Extra CORS origins (ALLOWED_ORIGINS), comma separated |
questarr.appUrl |
"" |
Public URL used in notification links (APP_URL) |
extraEnv |
[] |
Extra environment variables, in the name/value container form |
extraEnvFrom |
[] |
Extra envFrom sources (ConfigMaps / Secrets) |
Every secret is optional: Questarr generates and persists its own JWT_SECRET and
CREDENTIALS_ENCRYPTION_KEY when they are unset, and the API keys can also be entered
in Settings → Integrations.
| Key | Default | Description |
|---|---|---|
questarr.existingSecret |
"" |
Read the secrets from this Secret instead of creating one |
questarr.existingSecretKeys |
see values.yaml |
Secret key holding each value (the env var name is fixed) |
questarr.secrets.jwtSecret |
"" |
JWT_SECRET |
questarr.secrets.credentialsEncryptionKey |
"" |
CREDENTIALS_ENCRYPTION_KEY, 64 hex chars |
questarr.secrets.igdbClientId |
"" |
IGDB_CLIENT_ID |
questarr.secrets.igdbClientSecret |
"" |
IGDB_CLIENT_SECRET |
questarr.secrets.nexusmodsApiKey |
"" |
NEXUSMODS_API_KEY |
Values under questarr.secrets end up in a chart-managed Secret, which means they also
sit in the Helm release data. For anything you would rather not put in a values file,
create the Secret yourself and point the chart at it:
kubectl create secret generic questarr-secrets \
--namespace questarr \
--from-literal=IGDB_CLIENT_ID=... \
--from-literal=IGDB_CLIENT_SECRET=... \
--from-literal=CREDENTIALS_ENCRYPTION_KEY="$(openssl rand -hex 32)"
helm install questarr charts/questarr --namespace questarr \
--set questarr.existingSecret=questarr-secretsKeys absent from that Secret are injected as optional, so a Secret carrying only some
of them works. Only JWT_SECRET and CREDENTIALS_ENCRYPTION_KEY fall back to values
Questarr generates and persists itself when missing — IGDB_CLIENT_ID,
IGDB_CLIENT_SECRET and NEXUSMODS_API_KEY simply stay unset, leaving discovery and
mods disabled until you enter them in Settings → Services.
questarr.existingSecretKeys only renames the key each value is read from inside the
Secret; the environment variable Questarr sees (JWT_SECRET, IGDB_CLIENT_ID, …) is
fixed by the application. So a Secret that stores the JWT secret under release-jwt
needs questarr.existingSecretKeys.jwtSecret=release-jwt and nothing else.
| Key | Default | Description |
|---|---|---|
persistence.data.enabled |
true |
Persist /app/data; false uses an emptyDir |
persistence.data.existingClaim |
"" |
Use an existing PVC instead of creating one |
persistence.data.storageClass |
"" |
"" = cluster default, - = no dynamic provisioning |
persistence.data.accessModes |
[ReadWriteOnce] |
PVC access modes |
persistence.data.size |
2Gi |
PVC size |
persistence.data.retain |
false |
Keep the PVC on helm uninstall |
extraVolumes / extraVolumeMounts |
[] |
Additional volumes, typically the media/downloads share |
Imports are atomic moves only when Questarr sees your library and completed-downloads
directories at the same paths as your download client. Mount that shared storage with
extraVolumes / extraVolumeMounts:
extraVolumes:
- name: media
persistentVolumeClaim:
claimName: media
extraVolumeMounts:
- name: media
mountPath: /data| Key | Default | Description |
|---|---|---|
service.type |
ClusterIP |
Service type |
service.port |
5000 |
Service port |
service.nodePort |
"" |
Node port, for NodePort / LoadBalancer |
service.annotations |
{} |
Service annotations |
service.labels |
{} |
Extra Service labels |
ingress.enabled |
false |
Create an Ingress |
ingress.className |
"" |
Ingress class |
ingress.annotations |
{} |
Ingress annotations |
ingress.hosts |
questarr.local at / |
Hosts and paths |
ingress.tls |
[] |
TLS configuration |
Serving from a subdirectory (https://host/questarr/) needs questarr.basePath set as
well as the Ingress path — Questarr mounts itself under the prefix rather than relying
on the proxy to rewrite it. See docs/REVERSE_PROXY.md.
questarr:
basePath: /questarr
ingress:
enabled: true
className: nginx
hosts:
- host: home.example.com
paths:
- path: /questarr
pathType: PrefixQuestarr's own HTTPS listener (Settings → SSL) is not exposed by this chart: terminate TLS at the Ingress instead.
| Key | Default | Description |
|---|---|---|
podSecurityContext |
fsGroup: 1000 |
Pod-level security context |
securityContext |
root + narrowed capabilities | Container security context (see below) |
serviceAccount.create |
true |
Create a ServiceAccount |
serviceAccount.automountServiceAccountToken |
false |
Questarr never calls the Kubernetes API |
resources |
modest defaults | Resource requests and limits |
livenessProbe / readinessProbe / startupProbe |
enabled | Probe tuning; each has an enabled flag |
nodeSelector, tolerations, affinity |
empty | Scheduling controls |
topologySpreadConstraints, priorityClassName, dnsPolicy, dnsConfig |
empty | Further pod-spec controls |
resources ships with conservative defaults (256Mi memory / 100m CPU requested, 1Gi
memory and 2Gi ephemeral-storage limits) so the pod is schedulable under a
LimitRange or a namespace quota. Raise them if imports of large archives get
OOM-killed, or set resources: {} to run without any.
extraEnv is rendered before the chart-managed variables, so entries there cannot
override SQLITE_DB_PATH, PORT, PUID/PGID or the base path — use the dedicated
values for those.
Liveness and startup probes hit /api/health, which stays reachable unprefixed even
when questarr.basePath is set; readiness hits /api/ready under the base path, which
also checks database connectivity.
The container starts as root on purpose: its entrypoint applies PUID/PGID to
/app/data and then drops privileges with su-exec before the app runs. The default
securityContext keeps only the capabilities that needs — CHOWN, SETGID and
SETUID, on top of drop: ALL and allowPrivilegeEscalation: false. DAC_OVERRIDE
is deliberately not granted, so a volume whose modes deny root will fail the
entrypoint's writability check with a clear error rather than being forced open. If your cluster forbids root
containers, pre-create the volume with the right ownership and run fully unprivileged:
securityContext:
runAsUser: 1000
runAsGroup: 1000
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop:
- ALLThe entrypoint's groupmod/usermod/chown steps then fail unless the volume is
already owned by 1000:1000, so verify the pod starts before relying on it.
helm upgrade questarr charts/questarr --namespace questarrDatabase migrations run at startup. The Recreate strategy stops the old pod before the
new one starts, so expect a short outage during upgrades. Back up /app/data first — the
PVC is deleted with the release unless persistence.data.retain is true or you use
persistence.data.existingClaim.
helm test questarr --namespace questarrThis runs a short-lived pod that requests /api/health through the Service.