Deploys Ryot - "The only self-hosted tracker you will ever need" - on Kubernetes, with an optional bundled PostgreSQL database.
The chart is published to GitHub Container Registry as an OCI artifact (requires Helm 3.8+):
helm install ryot oci://ghcr.io/ignisda/charts/ryot \
--version x.y.z \
--set secret.adminAccessToken.value="$(openssl rand -hex 16)" \
--set postgres.auth.password="$(openssl rand -hex 16)" \
--set config.frontendUrl="https://ryot.your-domain.com"Omitting --version installs the latest published chart version.
helm install ryot ./ci/helm/ryot \
--set secret.adminAccessToken.value="$(openssl rand -hex 16)" \
--set config.frontendUrl="https://ryot.your-domain.com" \
--set postgres.auth.password="$(openssl rand -hex 16)"Each published GitHub release packages the chart and pushes it to
oci://ghcr.io/ignisda/charts/ryot. The release tag drives the chart version
(a leading v is stripped).
- A single
Deploymentrunning the Ryot container (frontend + backend + Caddy proxy in one image, listening on port8000). - A
Service(ClusterIP by default) exposing port8000. - An optional
Ingress. - An optional bundled PostgreSQL
StatefulSet+ headlessService+Secret, using the officialpostgresimage. - A
ConfigMapfor non-sensitive env and aSecretfor sensitive env.
No ServiceAccount is created.
postgres.enabled=true (default) deploys a single-node Postgres StatefulSet and
wires DATABASE_URL automatically. The password comes from
postgres.auth.password (stored in a chart-managed secret) or from an existing
secret via postgres.auth.existingSecret.
DATABASE_URL is composed at runtime inside the pod, so the password is only
ever read from the Postgres secret and never written into a second manifest.
The chart ships no default password. With
postgres.enabled=trueyou must set a strongpostgres.auth.passwordor reference one viapostgres.auth.existingSecret, otherwise the chart refuses to render. Keep it URL-safe (it is embedded in the connection string).
Set postgres.enabled=false. Three ways to point Ryot at your database, in
order of precedence:
-
Full URL inline - stored in a chart-managed
Secret(keydatabase-url):postgres: enabled: false externalDatabase: url: "postgres://user:pass@db.example.com:5432/ryot"
-
Full URL from an existing secret - reference a secret you manage:
postgres: enabled: false externalDatabase: existingSecret: my-ryot-db existingSecretKey: database-url # default
-
Individual components - used when
urlandexistingSecretare both empty. The chart composesDATABASE_URLat runtime. Each component may be inline (value) or pulled from a secret (existingSecret+existingSecretKey). Any component referencing an existing secret is read viasecretKeyRefand never appears in plaintext in the manifest. For inline values,passwordis written to the chart-managed Secret; the other inline components (host, port, database, username) are passed as plain env values.postgres: enabled: false externalDatabase: host: value: db.example.com port: value: "5432" database: value: ryot username: value: ryotuser password: # inline -> chart Secret, or: existingSecret: ryot-db-creds existingSecretKey: password
Because the password is read via
secretKeyRef, it never appears in the composedDATABASE_URLliteral in the manifest.
These are always sourced from a Secret (chart-managed or existing), never
from the ConfigMap:
| Env var | Value | Existing-secret keys |
|---|---|---|
SERVER_ADMIN_ACCESS_TOKEN |
secret.adminAccessToken.value (required) |
secret.adminAccessToken.existingSecret / .existingSecretKey |
SERVER_PRO_KEY |
secret.proKey.value (optional) |
secret.proKey.existingSecret / .existingSecretKey |
DATABASE_URL |
bundled / externalDatabase.url |
externalDatabase.existingSecret / .existingSecretKey |
| provider tokens (any) | secretEnv map |
secretEnvFrom map |
ingress:
enabled: true
className: nginx
annotations: {}
hosts:
- host: ryot.your-domain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: ryot-tls
hosts:
- ryot.your-domain.comProvider access tokens and client IDs/secrets (TMDB, Twitch, Trakt, MyAnimeList, ...) are sensitive and must go in a Secret, never the ConfigMap. The chart provides a dynamic secret-env mapping for this. See the Ryot configuration docs.
Inline values - written to the chart-managed Secret:
secretEnv:
MOVIES_AND_SHOWS_TMDB_ACCESS_TOKEN: "xxxx"
VIDEO_GAMES_TWITCH_CLIENT_ID: "xxxx"
VIDEO_GAMES_TWITCH_CLIENT_SECRET: "xxxx"Or map env vars to keys in your own existing secret(s):
secretEnvFrom:
VIDEO_GAMES_TWITCH_CLIENT_SECRET:
existingSecret: ryot-providers
key: twitch-client-secret
ANIME_AND_MANGA_MAL_CLIENT_ID:
existingSecret: ryot-providers
key: mal-client-idconfig.extraEnv remains available for genuinely non-sensitive options only
(it lands in a ConfigMap).
The chart fails to render (helm install / template / lint) with a clear
message when required configuration is missing or inconsistent, including:
image.repositoryandservice.portunset.secret.adminAccessTokennot provided (value or existing secret).- Any
existingSecretset without its corresponding key. postgres.enabled=truewith no password, blank username/database, or (when persistence is on) missing size/mountPath.postgres.enabled=falsewith no usable external database: nourl, noexistingSecret, and missing component(s) (host/port/database/username/password).ingress.enabled=truewith no hosts, a host missinghost, or a host with no paths.
Render-time tests live in tests/ and use the
helm-unittest plugin. They
assert every guardrail above fails as expected and that the happy paths render
correctly. database_url_test.yaml covers DATABASE_URL creation across all
modes (bundled, external URL inline/existing-secret, and components inline /
from-secret / mixed).
helm plugin install https://github.com/helm-unittest/helm-unittest
helm unittest ci/helm/ryotPost-install smoke tests live in templates/tests/ as
helm.sh/hook: test Pods. Run them against a deployed release:
helm test <release-name>- test-http-connection - GETs
/healththrough the Service (with retries) to confirm Ryot is serving. - test-database-url - injects the exact same database env the app
receives, then resolves the composed
DATABASE_URL, checks it is a validpostgres://string, and runspg_isready+psql 'SELECT 1'against it. This validates DATABASE_URL creation end-to-end (the$(VAR)interpolation a render-time test cannot resolve).
Ryot exposes /health on the service port; liveness and readiness probes use it.
| Key | Default | Description |
|---|---|---|
image.repository |
ghcr.io/ignisda/ryot |
Image repo |
image.tag |
"" (chart appVersion) |
Image tag |
config.timezone |
GMT |
TZ |
config.frontendUrl |
"" |
FRONTEND_URL |
config.usersAllowRegistration |
true |
USERS_ALLOW_REGISTRATION |
postgres.enabled |
true |
Deploy bundled PostgreSQL |
postgres.auth.password |
"" (required) |
Bundled DB password (no default) |
postgres.persistence.size |
8Gi |
Data volume size |
service.port |
8000 |
Service port |
ingress.enabled |
false |
Enable ingress |
See values.yaml for the full list, or the auto-generated VALUES.md for the rendered reference table (regenerated on every PR by helm-docs).