Skip to content

Latest commit

 

History

History

questarr

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

README.md

Questarr Helm chart

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).

Requirements

  • Kubernetes >= 1.23
  • Helm >= 3.8
  • A StorageClass able to provision a ReadWriteOnce volume (unless you bring your own PVC)

Install

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-namespace

Then reach the UI (the release notes print the exact command for your Service type):

kubectl port-forward -n questarr svc/questarr 5000:5000

The first account created in the UI becomes the administrator.

Single replica, by design

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.

Configuration

Image and release

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

Application

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)

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-secrets

Keys 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.

Storage

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

Networking

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: Prefix

Questarr's own HTTPS listener (Settings → SSL) is not exposed by this chart: terminate TLS at the Ingress instead.

Scheduling, security and probes

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:
      - ALL

The 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.

Upgrading

helm upgrade questarr charts/questarr --namespace questarr

Database 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.

Testing a release

helm test questarr --namespace questarr

This runs a short-lived pod that requests /api/health through the Service.