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
| Component | What it is | Bundled by default? |
|---|---|---|
| server | The Spring Boot backend (ghcr.io/ister-app/server) | always |
| website | The web player (ghcr.io/ister-app/player) | always |
| PostgreSQL | The database | yes (database.mode: internal) |
| RabbitMQ | The message broker (Bitnami subchart) | yes (rabbitmq.enabled: true) |
| Typesense | The search engine | yes (typesense.enabled: true) |
| Flyway | Database 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 CloudNativePGCluster(3 instances by default, anti-affinity, optional Barman Cloud backups to S3 underdatabase.cnpg.backup). Requires the CNPG operator.external— an existing PostgreSQL, configured underdatabase.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
existingSecretescape 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 upgradewould lock the server out of its own database. This is also whyhelm templateoutput differs from whathelm installactually applies. - Rotating a Secret restarts the server. All credentials are hashed into a
checksum/secretspod 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 anameand atype(SHOW,MOVIE,MUSIC,BOOK,PODCAST,COMIC).server.mediaVolumes— the volumes mounted into the server pod, each backed by exactly one ofhostPath,existingClaimornfs, and linked to a library vialibrary(omit it for a mount-only volume). No trailing slash onmountPath— 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 isReadWriteOnceby default, which is whyserver.replicaCountmust stay at 1 — the chart refuses to render more replicas unless you switch toReadWriteMany.server.tmp— scratch space for transcoding. Disable it whenmountPathfalls 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 --logsruns the shipped connectivity test. - Monitoring:
monitoring.enabledrenders a Prometheus Operator ServiceMonitor for the server's actuator; incnpgmode,database.cnpg.podMonitorcovers the database. - Uninstall: the database, cache and Typesense PVCs are kept by default
(
retain: true) so an uninstall is not destructive; setretain: falsewhere 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.
| Key | Default |
|---|---|
nameOverride | "" |
fullnameOverride | "" |
imagePullSecrets | [] |
serviceAccount.create | true |
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.replicaCount | 1 |
server.contextPath | "/api" |
server.url | "" |
server.oidc.url | "" |
server.tmdbApiKey | "" |
server.existingSecret | "" |
server.managementPort | 8081 |
server.enableServiceLinks | false |
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.runAsNonRoot | true |
server.podSecurityContext.runAsUser | 1000 |
server.podSecurityContext.fsGroup | 1000 |
server.podSecurityContext.seccompProfile.type | "RuntimeDefault" |
server.securityContext.allowPrivilegeEscalation | false |
server.securityContext.capabilities.drop | ["ALL"] |
server.probes.enabled | true |
server.probes.path | "/actuator/health" |
server.probes.startup.failureThreshold | 30 |
server.probes.startup.periodSeconds | 10 |
server.probes.liveness.periodSeconds | 20 |
server.probes.liveness.failureThreshold | 3 |
server.probes.readiness.periodSeconds | 10 |
server.probes.readiness.failureThreshold | 3 |
server.tmp.mountPath | "/tmp/ister" |
server.tmp.enabled | true |
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.replicaCount | 1 |
website.port | 8080 |
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.runAsNonRoot | true |
website.podSecurityContext.seccompProfile.type | "RuntimeDefault" |
website.securityContext.allowPrivilegeEscalation | false |
website.securityContext.capabilities.drop | ["ALL"] |
website.probes.enabled | true |
website.probes.path | "/" |
cache.enabled | true |
cache.existingClaim | "" |
cache.mountPath | "/cache" |
cache.storageClass | "" |
cache.size | "20Gi" |
cache.accessMode | "ReadWriteOnce" |
cache.retain | true |
ingress.enabled | false |
ingress.className | "nginx" |
ingress.host | "" |
ingress.annotations | {} |
ingress.tls.enabled | true |
ingress.tls.secretName | "" |
ingress.tls.certIssuer | "" |
ingress.wellKnown.enabled | false |
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.retain | true |
database.internal.podSecurityContext.runAsNonRoot | true |
database.internal.podSecurityContext.runAsUser | 999 |
database.internal.podSecurityContext.runAsGroup | 999 |
database.internal.podSecurityContext.fsGroup | 999 |
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.instances | 3 |
database.cnpg.imageName | "" |
database.cnpg.dbName | "ister" |
database.cnpg.user | "ister" |
database.cnpg.storageClass | "" |
database.cnpg.storageSize | "12Gi" |
database.cnpg.enablePodAntiAffinity | true |
database.cnpg.podMonitor.enabled | true |
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.enabled | false |
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.enabled | true |
database.cnpg.backup.schedule.cron | "0 0 3 * * *" |
database.cnpg.backup.schedule.backupOwnerReference | "self" |
database.external.host | "" |
database.external.port | 5432 |
database.external.dbName | "ister" |
database.external.user | "ister" |
database.external.password | "" |
database.external.existingSecret | "" |
rabbitmq.enabled | true |
rabbitmq.image.repository | "bitnamilegacy/rabbitmq" |
rabbitmq.global.security.allowInsecureImages | true |
rabbitmq.ingress.enabled | false |
rabbitmq.ingress.tls | false |
rabbitmq.auth.username | "user" |
rabbitmq.auth.password | "" |
rabbitmq.auth.existingPasswordSecret | "" |
rabbitmq.auth.existingErlangSecret | "" |
rabbitmq.persistence.enabled | true |
rabbitmq.persistence.size | "8Gi" |
rabbitmq.resources.requests.cpu | "100m" |
rabbitmq.resources.requests.memory | "256Mi" |
rabbitmq.resources.limits.memory | "1Gi" |
externalRabbitmq.host | "" |
externalRabbitmq.port | 5672 |
externalRabbitmq.user | "user" |
externalRabbitmq.password | "" |
externalRabbitmq.existingSecret | "" |
typesense.enabled | true |
typesense.image.repository | "docker.io/typesense/typesense" |
typesense.image.tag | "30.2" |
typesense.image.pullPolicy | "IfNotPresent" |
typesense.port | 8108 |
typesense.collection | "media" |
typesense.apiKey | "" |
typesense.existingSecret | "" |
typesense.persistence.enabled | true |
typesense.persistence.existingClaim | "" |
typesense.persistence.storageClass | "" |
typesense.persistence.size | "8Gi" |
typesense.persistence.retain | true |
typesense.podSecurityContext.runAsNonRoot | true |
typesense.podSecurityContext.runAsUser | 1000 |
typesense.podSecurityContext.fsGroup | 1000 |
typesense.podSecurityContext.seccompProfile.type | "RuntimeDefault" |
typesense.securityContext.allowPrivilegeEscalation | false |
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.enabled | true |
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.enabled | false |
monitoring.serviceMonitorLabels | {} |
monitoring.interval | "30s" |
podDisruptionBudget.enabled | false |
podDisruptionBudget.minAvailable | 1 |
tests.image.repository | "curlimages/curl" |
tests.image.tag | "8.21.0" |
tests.image.pullPolicy | "IfNotPresent" |