Skip to main content

Installing ister with Helm

This chapter explains how the ister Helm chart works and how to install it. The chart deploys the full ister media server: the API server, the web player, and — unless you bring your own — PostgreSQL, RabbitMQ and Typesense.

What the chart deploys

ComponentWhat it isBundled by default?
serverThe Spring Boot backend (ghcr.io/ister-app/server)always
websiteThe web player (ghcr.io/ister-app/player)always
PostgreSQLThe databaseyes (database.mode: internal)
RabbitMQThe message broker (Bitnami subchart)yes (rabbitmq.enabled: true)
TypesenseThe search engineyes (typesense.enabled: true)
FlywayDatabase migrations (ghcr.io/ister-app/migrations)runs as an init container

The defaults deploy a self-contained ister with no ingress and no media volumes — enough to try it out, not a production setup.

Prerequisites

  • A Kubernetes cluster and Helm 3.
  • A TMDB API key for movie/show metadata.
  • An OIDC issuer (for example Keycloak) — ister does not manage users itself.
  • For database.mode: cnpg: the CloudNativePG operator.
  • For ingress with TLS: an ingress controller and, optionally, cert-manager.

Installing

The chart is published as an OCI artifact:

helm install ister oci://ghcr.io/ister-app/charts/ister \
--namespace ister --create-namespace \
--set server.tmdbApiKey=<your-key> \
--set server.oidc.url=https://keycloak.example.com/realms/auth

For anything beyond a first try, start from values-production.example.yaml in the chart repository and install with -f. Pin a chart version with --version; a GitHub Release exists for every chart version with release notes listing the exact image versions it deploys.

Bundled or external datastores

Everything backing the server can either be deployed by the chart or pointed at an existing service. Whatever you choose, the templates downstream never notice the difference — that is the chart's central design rule.

PostgreSQL — database.mode

  • internal — a single postgres Deployment + PVC. Development only: one instance, no backups.
  • cnpg — a CloudNativePG Cluster (3 instances by default, anti-affinity, optional Barman Cloud backups to S3 under database.cnpg.backup). Requires the CNPG operator.
  • external — an existing PostgreSQL, configured under database.external.

All three modes converge on one Secret with the keys host, port, dbname, user, password — the shape CNPG generates for its <cluster>-app Secret, which the other two modes hand-write to match. If you bring your own Secret (existingSecret), it must have exactly those keys; for a CNPG cluster managed outside this chart, its <cluster>-app Secret already does.

RabbitMQ — rabbitmq.enabled

true deploys the Bitnami RabbitMQ subchart (everything under rabbitmq: is passed straight to it). false uses the externalRabbitmq block instead — its existingSecret needs the key rabbitmq-password.

Typesense — typesense.enabled

true deploys Typesense with its own PVC. false uses typesense.external — its existingSecret needs the key api-key.

Secrets and passwords

  • Nothing secret belongs in a values file. Every credential has an existingSecret escape hatch; use it (or a secret manager) anywhere that matters.
  • Generated passwords survive upgrades. When you let the chart generate a password, it looks up the live Secret on upgrade and keeps the existing value — otherwise every helm upgrade would lock the server out of its own database. This is also why helm template output differs from what helm install actually applies.
  • Rotating a Secret restarts the server. All credentials are hashed into a checksum/secrets pod annotation, so a changed Secret rolls the Deployment.

Database migrations

Flyway runs as an init container on the server pod (preceded by a wait-for-db container), so the server can never start against an unmigrated schema. It is not a Helm hook on purpose: with a bundled database, a pre-install hook would wait for a database that Helm has not created yet. The migrations image publishes the same version as the server, so flyway.image.tag stays empty and follows the server version automatically.

Media libraries and volumes

Two value lists connect the server to your media:

  • server.libraries — the libraries ister scans; each has a name and a type (SHOW, MOVIE, MUSIC, BOOK, PODCAST, COMIC).
  • server.mediaVolumes — the volumes mounted into the server pod, each backed by exactly one of hostPath, existingClaim or nfs, and linked to a library via library (omit it for a mount-only volume). No trailing slash on mountPath — the scanner skips the tree if there is one.

Two more storage knobs matter:

  • cache — a PVC for the server's cache directory (transcodes, images). It is ReadWriteOnce by default, which is why server.replicaCount must stay at 1 — the chart refuses to render more replicas unless you switch to ReadWriteMany.
  • server.tmp — scratch space for transcoding. Disable it when mountPath falls inside one of your media volumes, or the dedicated volume shadows that path.

Ingress

ingress.enabled: true publishes the player at / and the API at server.contextPath (default /api) on ingress.host, with TLS via cert-manager when ingress.tls.certIssuer is set. ingress.wellKnown.enabled additionally serves /.well-known/ister for client discovery — implemented as an ingress-nginx server-snippet, which modern ingress-nginx disables by default (allow-snippet-annotations); enable that on the controller first.

Operations

  • Upgrades: helm upgrade ister oci://ghcr.io/ister-app/charts/ister --version <v> with your values file. Generated passwords are preserved (see above).
  • Smoke test: helm test ister -n ister --logs runs the shipped connectivity test.
  • Monitoring: monitoring.enabled renders a Prometheus Operator ServiceMonitor for the server's actuator; in cnpg mode, database.cnpg.podMonitor covers the database.
  • Uninstall: the database, cache and Typesense PVCs are kept by default (retain: true) so an uninstall is not destructive; set retain: false where you want them purged with the release.

Values reference

Every value the chart accepts, with its default. This table is generated from the values.yaml of the release this documentation shipped with; values.schema.json enforces the same contract on every render.

KeyDefault
nameOverride""
fullnameOverride""
imagePullSecrets[]
serviceAccount.createtrue
serviceAccount.name""
serviceAccount.annotations{}
server.name"Ister"
server.image.repository"ghcr.io/ister-app/server"
server.image.tag"2.0.1"
server.image.digest""
server.image.pullPolicy"IfNotPresent"
server.replicaCount1
server.contextPath"/api"
server.url""
server.oidc.url""
server.tmdbApiKey""
server.existingSecret""
server.managementPort8081
server.enableServiceLinksfalse
server.extraEnv[]
server.podAnnotations{}
server.nodeSelector{}
server.tolerations[]
server.affinity{}
server.resources.requests.cpu"500m"
server.resources.requests.memory"1Gi"
server.resources.limits.memory"4Gi"
server.podSecurityContext.runAsNonRoottrue
server.podSecurityContext.runAsUser1000
server.podSecurityContext.fsGroup1000
server.podSecurityContext.seccompProfile.type"RuntimeDefault"
server.securityContext.allowPrivilegeEscalationfalse
server.securityContext.capabilities.drop["ALL"]
server.probes.enabledtrue
server.probes.path"/actuator/health"
server.probes.startup.failureThreshold30
server.probes.startup.periodSeconds10
server.probes.liveness.periodSeconds20
server.probes.liveness.failureThreshold3
server.probes.readiness.periodSeconds10
server.probes.readiness.failureThreshold3
server.tmp.mountPath"/tmp/ister"
server.tmp.enabledtrue
server.tmp.sizeLimit"10Gi"
server.tmp.existingClaim""
server.libraries[]
server.mediaVolumes[]
website.image.repository"ghcr.io/ister-app/player"
website.image.tag"1.0.0"
website.image.digest""
website.image.pullPolicy"IfNotPresent"
website.replicaCount1
website.port8080
website.podAnnotations{}
website.nodeSelector{}
website.tolerations[]
website.affinity{}
website.resources.requests.cpu"10m"
website.resources.requests.memory"32Mi"
website.resources.limits.memory"128Mi"
website.podSecurityContext.runAsNonRoottrue
website.podSecurityContext.seccompProfile.type"RuntimeDefault"
website.securityContext.allowPrivilegeEscalationfalse
website.securityContext.capabilities.drop["ALL"]
website.probes.enabledtrue
website.probes.path"/"
cache.enabledtrue
cache.existingClaim""
cache.mountPath"/cache"
cache.storageClass""
cache.size"20Gi"
cache.accessMode"ReadWriteOnce"
cache.retaintrue
ingress.enabledfalse
ingress.className"nginx"
ingress.host""
ingress.annotations{}
ingress.tls.enabledtrue
ingress.tls.secretName""
ingress.tls.certIssuer""
ingress.wellKnown.enabledfalse
database.mode"internal"
database.internal.image.repository"postgres"
database.internal.image.tag"18"
database.internal.image.pullPolicy"IfNotPresent"
database.internal.dbName"ister"
database.internal.user"ister"
database.internal.password""
database.internal.existingSecret""
database.internal.storageClass""
database.internal.storageSize"8Gi"
database.internal.retaintrue
database.internal.podSecurityContext.runAsNonRoottrue
database.internal.podSecurityContext.runAsUser999
database.internal.podSecurityContext.runAsGroup999
database.internal.podSecurityContext.fsGroup999
database.internal.podSecurityContext.seccompProfile.type"RuntimeDefault"
database.internal.resources.requests.cpu"100m"
database.internal.resources.requests.memory"256Mi"
database.internal.resources.limits.memory"1Gi"
database.cnpg.instances3
database.cnpg.imageName""
database.cnpg.dbName"ister"
database.cnpg.user"ister"
database.cnpg.storageClass""
database.cnpg.storageSize"12Gi"
database.cnpg.enablePodAntiAffinitytrue
database.cnpg.podMonitor.enabledtrue
database.cnpg.podMonitor.interval"30s"
database.cnpg.podMonitor.labels{}
database.cnpg.parameters.pgaudit.log"all, -misc"
database.cnpg.parameters.pgaudit.log_parameter"on"
database.cnpg.parameters.pgaudit.log_relation"on"
database.cnpg.resources{}
database.cnpg.backup.enabledfalse
database.cnpg.backup.destinationPath""
database.cnpg.backup.endpointURL""
database.cnpg.backup.region""
database.cnpg.backup.walCompression"gzip"
database.cnpg.backup.existingSecret""
database.cnpg.backup.accessKeyId""
database.cnpg.backup.secretAccessKey""
database.cnpg.backup.schedule.enabledtrue
database.cnpg.backup.schedule.cron"0 0 3 * * *"
database.cnpg.backup.schedule.backupOwnerReference"self"
database.external.host""
database.external.port5432
database.external.dbName"ister"
database.external.user"ister"
database.external.password""
database.external.existingSecret""
rabbitmq.enabledtrue
rabbitmq.image.repository"bitnamilegacy/rabbitmq"
rabbitmq.global.security.allowInsecureImagestrue
rabbitmq.ingress.enabledfalse
rabbitmq.ingress.tlsfalse
rabbitmq.auth.username"user"
rabbitmq.auth.password""
rabbitmq.auth.existingPasswordSecret""
rabbitmq.auth.existingErlangSecret""
rabbitmq.persistence.enabledtrue
rabbitmq.persistence.size"8Gi"
rabbitmq.resources.requests.cpu"100m"
rabbitmq.resources.requests.memory"256Mi"
rabbitmq.resources.limits.memory"1Gi"
externalRabbitmq.host""
externalRabbitmq.port5672
externalRabbitmq.user"user"
externalRabbitmq.password""
externalRabbitmq.existingSecret""
typesense.enabledtrue
typesense.image.repository"docker.io/typesense/typesense"
typesense.image.tag"30.2"
typesense.image.pullPolicy"IfNotPresent"
typesense.port8108
typesense.collection"media"
typesense.apiKey""
typesense.existingSecret""
typesense.persistence.enabledtrue
typesense.persistence.existingClaim""
typesense.persistence.storageClass""
typesense.persistence.size"8Gi"
typesense.persistence.retaintrue
typesense.podSecurityContext.runAsNonRoottrue
typesense.podSecurityContext.runAsUser1000
typesense.podSecurityContext.fsGroup1000
typesense.podSecurityContext.seccompProfile.type"RuntimeDefault"
typesense.securityContext.allowPrivilegeEscalationfalse
typesense.securityContext.capabilities.drop["ALL"]
typesense.nodeSelector{}
typesense.tolerations[]
typesense.resources.requests.cpu"100m"
typesense.resources.requests.memory"256Mi"
typesense.resources.limits.memory"1Gi"
typesense.external.host""
typesense.external.protocol"http"
typesense.external.existingSecret""
flyway.enabledtrue
flyway.image.repository"ghcr.io/ister-app/migrations"
flyway.image.tag""
flyway.image.digest""
flyway.image.pullPolicy"IfNotPresent"
flyway.waitImage.repository"postgres"
flyway.waitImage.tag"18-alpine"
flyway.waitImage.pullPolicy"IfNotPresent"
flyway.resources.requests.cpu"100m"
flyway.resources.requests.memory"256Mi"
flyway.resources.limits.memory"512Mi"
monitoring.enabledfalse
monitoring.serviceMonitorLabels{}
monitoring.interval"30s"
podDisruptionBudget.enabledfalse
podDisruptionBudget.minAvailable1
tests.image.repository"curlimages/curl"
tests.image.tag"8.21.0"
tests.image.pullPolicy"IfNotPresent"