Configuration
The chart is configured from a Helm values file. example-values.yaml, shipped inside the chart, is
a complete working example — copy it and replace the placeholders, as described in
Installation.
Where a value matches an environment variable on the Docker deployment, this page links to the Docker configuration reference rather than repeating it.
_Required_ means the platform will not work until you set it.
Global and common values
| Value | Default value | Description |
|---|---|---|
envName | None | Environment name, shown for support purposes. Same as ENV_NAME. |
imageCredentials.dockerConfigJson | Required | base64 of a Docker config.json with quay.io/octopize credentials. Octopize provides them. |
global.imageRegistry | None | Pull every image the chart owns from this registry instead. Use it for a mirrored install. |
global.imagePullSecrets | [] | Existing pull Secrets to add to every pod. Entries are maps: - name: my-registry-creds. |
nameOverride | None | Value of the app.kubernetes.io/name label on every object. |
commonLabels | {} | Extra labels on every object the chart creates. |
commonAnnotations | {} | Extra annotations on every object the chart creates. |
Generating the secrets
The chart archive contains a script that makes these values for you. Run it in the directory that
helm pull --untar wrote:
bash scripts/generate-secrets.sh
Use bash, and not ./. Helm packages each file with mode 0644, so the copy in the archive is not
executable.
The script writes avatar-secrets.yaml. Give that file to Helm after your own values file, and give
it to each upgrade:
helm install avatar . --values my-values.yaml --values avatar-secrets.yaml
Helm obeys the last --values file, so the generated file replaces these keys in your own file. Keep
the keys out of your own file. If you already have one of these credentials, write it into the
generated file. A storage.sseKek that protects stored objects is the usual example.
Keep the generated file. A second run of the script writes a new file. It does not replace a file
that exists, because storage.sseKek cannot change after the first install.
The script does not make the values that belong to your own infrastructure. These are the PostgreSQL admin credentials, the registry pull secret, the SMTP relay, the host names, and the credentials of an external S3 endpoint. The script lists them when it stops.
You can also make each value yourself. Each secret that you must invent is 32 random bytes. We write them in hexadecimal, which gives
64 characters, but the chart does not require that. Only storage.sseKek and
databaseConnections.encryptionKeys have a constraint, and the other values accept any text that
is not empty.
python -c "import secrets; print(secrets.token_hex(nbytes=32), end='')"
openssl rand -hex 32
| Value | Constraint | If you change it later |
|---|---|---|
api.auth.jwtSecretKey | None | Each user must log in again. No stored data is lost |
database.password | Any text that PostgreSQL accepts as a password | Change it on the server first. See Rotating a database password |
storage.admin.accessKeyId | None | Safe with the in-chart SeaweedFS, which the chart writes from this value. With your own existingConfigSecret, change both sides at the same time |
storage.admin.secretAccessKey | None | As above |
storage.sseKek | 64 lowercase hexadecimal characters. The chart checks this only when it writes the Secret itself | Never change it. Each stored object becomes unreadable |
storage.seaweedfsAdminPassword | None | Restart the SeaweedFS admin server: kubectl rollout restart statefulset/seaweedfs-admin |
sso.clientId | None. It is not a secret, but you make it in the same way | Restart the Authentik server and worker, then apply the blueprint again |
sso.clientSecret | None | Restart the Authentik server and worker, then apply the blueprint again |
sso.apiToken | None | Restart the Authentik server and worker, then apply the blueprint again |
sso.webhookSecret | None | Restart the Authentik server and worker, then apply the blueprint again |
authentik.authentik.secret_key | None. | Each user must log in to Authentik again |
authentik.authentik.postgresql.password | Any text that PostgreSQL accepts as a password | Change it on the server first. See Rotating a database password |
connector.secret | None. Only read when connector.enabled is true | The tokens of running database jobs become invalid. See Database connector |
databaseConnections.encryptionKeys | A Fernet key, not a hex string. See Database connector | Put the new key first and keep the old one. See Database connector |
database.adminPassword is not in the list, because where it comes from depends on your server.
If you create the admin account for Avatar, make its password like the others. If you point the
chart at an account that already exists, such as a managed-PostgreSQL admin or a role that a DBA
provisioned, use the password that it already has. The chart never creates this role and never
changes its password. To rotate it, change it on the server, then update the value.
You set the four SSO values one time, under sso. Helm cannot write into the values of a subchart,
so the AVATAR_AUTHENTIK_BLUEPRINT_* entries under
authentik.global.env refer to the objects that the chart writes
these values into. The blueprint thus reads back the value that you set. The chart compares the two
references and refuses to render if they do not agree. To keep these values out of the file, see
Using existing Secrets.
Using existing Secrets
By default you write every credential in your values file. The chart can read each one from a
Secret instead. Create that Secret in the namespace before the install, with external-secrets,
sealed-secrets, Vault or kubectl create secret. The chart only reads it, so your values file
holds no credential and you can keep it in version control.
example-values-existing-secrets.yaml, shipped in the chart, is a complete install with no
credential in a values file. Add it after your own:
helm install avatar oci://quay.io/octopize/helm/services-api/avatar \
--version <version> --namespace avatar --create-namespace \
--values my-values.yaml \
--values example-values-existing-secrets.yaml
Each credential takes one of four routes: the seven blocks the chart owns, image pull, Authentik's own settings, or SeaweedFS. The last three exist because Helm cannot write into the values of a subchart.
The blocks the chart owns
Seven blocks hold credentials: database, api.auth, storage, sso, smtp, telemetry.s3 and
authentikDatabase. Each takes the plain values, existingSecret to name the Secret, and
secretKeys to map each credential to a key in it.
database:
password: "" # empty: the Secret below supplies it
adminPassword: ""
existingSecret: avatar-secrets
secretKeys:
password: db-password # the key in that Secret
adminPassword: db-admin-password
One Secret can serve every block, and each block can also have its own. Without secretKeys, each
block looks for these key names:
| Block | Credential | Key it looks for |
|---|---|---|
database | password, adminPassword | db-password, db-admin-password |
api.auth | jwtSecretKey | authjwt-secret-key |
storage | admin.accessKeyId, admin.secretAccessKey | storage-access-key-id, storage-secret-access-key |
sso | clientSecret, apiToken, webhookSecret | sso-client-secret, sso-api-token, sso-webhook-secret |
smtp | password | smtp-password |
telemetry.s3 | accessKeyId, secretAccessKey | telemetry-s3-access-key-id, telemetry-s3-secret-access-key |
authentikDatabase | Authentik's database password | authentik-db-password |
The database connector has two more blocks: connector (key
connector-secret) and databaseConnections (keys database-connections and
connection-encryption-keys). They take existingSecret and secretKeys in the same way.
Four rules apply:
- Each key that
secretKeysnames must exist in the Secret. If a key is absent, the pods that mount it do not start. - To keep one credential in the values file, make its
secretKeysentry empty. - If you set a plain value and
existingSecrettogether, the Secret wins and the plain value is dropped. The chart gives no warning, so empty the values that the Secret supplies. - A new value in a Secret does not restart the pods. Do
kubectl rollout restarton the API and the Authentik server and worker. For a database password, change it on the server first. See Rotating a database password.
To set database.existingSecret or authentikDatabase.existingSecret on a release that already
exists, also increase the chart version. See
Verification & Troubleshooting.
Image pull credentials
Set global.imagePullSecrets to pull Secrets that
you created, and empty imageCredentials.dockerConfigJson. It reaches every pod of the chart and
the Authentik and SeaweedFS pods, but not Dask, which names its pull Secrets itself. Each entry
must be a map, not a bare name. The chart fails the render on a bare name only while Authentik is
enabled.
imageCredentials:
dockerConfigJson: ""
global:
imagePullSecrets:
- name: my-registry-creds
dask:
scheduler:
image:
pullSecrets:
- name: my-registry-creds
worker:
image:
pullSecrets:
- name: my-registry-creds
Authentik
Authentik reads AUTHENTIK_SECRET_KEY, AUTHENTIK_POSTGRESQL__PASSWORD and
AUTHENTIK_EMAIL__PASSWORD from its environment. Add an authentik.global.env entry with a
secretKeyRef for each one, and empty the related authentik.authentik.* setting. The subchart
then keeps that setting out of its own Secret, which frees the variable name.
authentik:
global:
env:
# Helm replaces this list. Write again each entry your install needs.
- name: AUTHENTIK_SECRET_KEY
valueFrom:
secretKeyRef:
name: avatar-secrets
key: authentik-secret-key
- name: AUTHENTIK_POSTGRESQL__PASSWORD
valueFrom:
secretKeyRef:
name: avatar-secrets
key: authentik-db-password
authentik:
secret_key: ""
postgresql:
password: ""
authentikDatabase.existingSecret is the Avatar side of the same database password. It tells the
bootstrap Job and the wait-for-database init containers where to read it.
SeaweedFS
SeaweedFS reads its admin identity from one JSON document, not from a key for each credential. Build that Secret manually. The same Secret can also hold the SeaweedFS admin user and password:
kubectl create secret generic avatar-seaweedfs-secrets \
--from-literal=sse-kek=<64 hex characters> \
--from-literal=admin-user=admin \
--from-literal=admin-password=<password> \
--from-literal=seaweedfs_s3_config='{"identities":[{"name":"power_user","credentials":[{"accessKey":"...","secretKey":"..."}],"actions":["Admin"]}]}'
storage:
sseKek: "" # empty: the entry below supplies it
seaweedfsAdminPassword: "" # empty: admin.secret below supplies it
seaweedfs:
admin:
secret:
existingSecret: avatar-seaweedfs-secrets
userKey: admin-user
pwKey: admin-password
s3:
existingConfigSecret: avatar-seaweedfs-secrets
extraEnvironmentVars:
WEED_S3_SSE_KEK:
secretKeyRef:
name: avatar-seaweedfs-secrets
key: sse-kek
The access key and the secret key in that JSON document are the same pair that
storage.existingSecret gives to the API. The two must agree, and the chart refuses to render if a
credential stays in a place that would win without a message.
After you change the admin password in the Secret, run
kubectl rollout restart statefulset/seaweedfs-admin.
Overriding a value the chart sets
Three of the ConfigMaps that the chart renders hold its runtime configuration. Their keys become environment variables in the containers. The others hold Authentik's branding, templates and translations, which are files and not settings.
| ConfigMap | Read by | Made from |
|---|---|---|
avatar-api-config | the API pods, the sync-tables CronJob, the Dask scheduler, the helm test Pods | ingress, envName, database, storage, sso, smtp, telemetry, api.config |
avatar-web-config | the web pods | ingress, storage, web.config, envName |
avatar-authentik-config | the Authentik server and worker | ingress, api.service.port, sso.trustedProxyCidrs |
Do not edit these objects with kubectl. The next helm upgrade renders them again from your
values. To change a key, set the value that the chart makes it from. The tables on this page give
those values. If the chart has no value for that key, override the key on the container.
On the workloads the chart owns
api, web, syncTables and tests each take an extraEnvVars list in the Kubernetes env
shape. The chart writes its own entries first, then adds this list. The API server accepts
duplicate names and defines no order, but the kubelet builds the environment in sequence, so the
container gets the last entry with a given name. An entry here therefore replaces the value from
the ConfigMap.
This example points DB_HOST at a ConfigMap of your own:
api:
extraEnvVars:
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: my-database-config
key: host
The same list also replaces a credential. The API reads its credentials from files that the chart mounts, but an environment variable has a higher priority than those files. An entry with the name of a credential thus wins:
api:
extraEnvVars:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: my-database-credentials
key: password
An empty value does not override: the API ignores an empty environment variable and uses the mounted file.
Use the same list for a variable that the chart does not have at all, such as an HTTPS_PROXY.
Two more points. api.extraEnvVars also goes to each init container of the chart, which are the
database setup and the CA-bundle merge, because their environment must be correct before the
migrations start. And Helm does not process these values as templates, so a {{ in a value is
text, not syntax.
On the subcharts
extraEnvVars does not go to Authentik, Dask or SeaweedFS. Each subchart has its own key:
| Subchart | Key | Shape |
|---|---|---|
authentik | authentik.global.env for both pods, authentik.server.env or authentik.worker.env for one | a list of env entries with value or valueFrom |
dask | dask.scheduler.env, dask.worker.env | the same list shape |
seaweedfs | seaweedfs.global.seaweedfs.extraEnvironmentVars for all components, seaweedfs.<component>.extraEnvironmentVars for one | a map of variable names: a string becomes value, a map becomes valueFrom |
- Authentik.
avatar-authentik-configarrives throughauthentik.global.envFrom. Kubernetes givesenva higher priority thanenvFrom, so aglobal.enventry with the same name replaces the key. If you overrideenvFrom, keep theconfigMapRefentry. Without it, Authentik loses the domain that it registers the Avatar application under. - Dask.
dask.scheduler.envalready readsENV_NAMEandIS_SENTRY_ENABLEDfromavatar-api-configwithconfigMapKeyRef. Read any other key of that ConfigMap, or a key of your own Secret, in the same way. - SeaweedFS. The
extraEnvironmentVarsof a component replaces the global entry with the same name. A map value becomes avalueFrom, asWEED_S3_SSE_KEKabove shows.
dask:
worker:
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: avatar-api-config
key: LOG_LEVEL
env list, it does not merge itIf your values file sets authentik.global.env, dask.scheduler.env or dask.worker.env, write
again each entry that the shipped values.yaml sets. If you do not, the pods lose those entries.
A change to your own ConfigMap or Secret does not restart anything. The checksum annotations of
the chart cover only what the chart renders. Do kubectl rollout restart on the workloads that
read the value.
Database
You supply the server. The chart creates the databases, the roles and the extensions on it.
| Value | Default value | Description |
|---|---|---|
database.host | Required | Hostname of the PostgreSQL server. |
database.port | 5432 | Port of that server. |
database.name | avatar | Name of the Avatar database. Same as DB_NAME. |
database.username | avatar | Role that the API connects as. Same as DB_USER. |
database.password | Required | Password of that role. |
database.adminUsername | Required | Admin account that creates the roles and the databases. Not mounted into the running API. |
database.adminPassword | Required | Password of the admin account. |
What the chart creates
| Object | Default name | Created by | Connects as |
|---|---|---|---|
| The Avatar role | database.username, avatar | the init-avatar-api init container, which runs bin/dbtool.py setup | database.adminUsername |
| The Avatar database, its tables and the first organization | database.name, avatar | the same init container | database.adminUsername |
| The Authentik role | authentik.authentik.postgresql.user, authentik | the avatar-authentik-db-init-<chart-version> Job | database.adminUsername |
The Authentik database, with the uuid-ossp and citext extensions | authentik.authentik.postgresql.name, authentik | the same Job | database.adminUsername |
| The Authentik tables | — | the Authentik server, at the first start | authentik.authentik.postgresql.user |
Both steps are idempotent and run again at each rollout, because the init container is part of the API pod template and the name of the Job contains the chart version. The init container also migrates the Avatar schema at an upgrade: it runs the migrations on an existing database, and creates the tables on a new one.
authentikDatabase.createDatabase: false
stops the Job. Use it when a DBA already created the Authentik role and database, or when no
account available to the cluster can create one. The Avatar database has no equivalent switch,
because the init container also does the migrations. Create in advance what you want to, and the
init container finds it.
Which role does what
| Role | Does | Used by |
|---|---|---|
database.adminUsername | creates the roles and databases, sets up the schema, migrates | the init-avatar-api init container and the Authentik bootstrap Job |
database.username | each query that Avatar does at run time | the API container, the sync-tables CronJob and the helm test Pods |
authentik.authentik.postgresql.user | the schema and the queries of Authentik | the Authentik server and worker |
The admin password is not in the Secret of the running API. It goes to the init container and the bootstrap Job only, so a running API container cannot read it.
What each role needs
database.adminUsername is the only account that you must create. It does not have to be a
superuser.
| It needs | Because |
|---|---|
LOGIN | It connects to the server. |
CONNECT on template1 | The setup runs its server-wide statements there. It also looks for the role and the database there, even when the two already exist. If your server does not permit this, set DB_MASTER_NAME in api.extraEnvVars. |
CONNECT on postgres | The Authentik bootstrap Job does the same from the postgres database. |
CREATEROLE | It creates the two roles. Not necessary if you create them. |
CREATEDB | It creates the two databases. Not necessary if you create them. |
Ownership of the Avatar database and of its public schema | The setup creates the tables there, runs the migrations, and does REVOKE CREATE ON SCHEMA public FROM PUBLIC. CREATE DATABASE without an OWNER clause gives it the database. On PostgreSQL 15 and later that carries the public schema too. Before 15 the schema stays with the bootstrap superuser and you must hand it over. |
database.username needs LOGIN only. The setup gives it SELECT, INSERT, UPDATE and
DELETE on each table in public, and does this again at each rollout, so a migration that adds a
table does not leave it without permissions. CONNECT comes from the default grant of PostgreSQL
to PUBLIC. The role never gets CREATE and never does DDL.
The Authentik role owns the Authentik database and runs the Authentik migrations. The chart gives it no permission on the Avatar database.
Rotating a database password
The chart sets the password of a role one time, when it creates the role. It never changes it after
that. The setup creates the Avatar role only when the role is absent, and the bootstrap Job creates
the Authentik role only when it is absent. Neither one does ALTER ROLE.
A new value in database.password therefore does not reach PostgreSQL. It only changes the
password that the API sends. The two values no longer agree, so the API cannot log in. The
init-avatar-api init container stops at its connection check, and the pod does not start.
Change the password on the server first:
ALTER ROLE avatar WITH PASSWORD '<new password>';
Then set the new value, or write it into the Secret that holds it, and restart the workloads that read it:
kubectl rollout restart deployment/avatar-api
The sync-tables CronJob and the helm test Pods read the same password. They get it at their next
run, so they need no restart.
authentik.authentik.postgresql.password works in the same way. Run
ALTER ROLE authentik WITH PASSWORD '<new password>', then restart
deployment/avatar-authentik-server and deployment/avatar-authentik-worker.
The API does not serve with a password that does not work, and no data changes. To recover, run the
ALTER ROLE above, or put the old value back.
Creating the databases manually
The SQL below does what the chart does. Run it as a superuser, or as any account that can create roles and databases on that server.
-- 1. The admin account that you give to the chart. You can remove CREATEDB and
-- CREATEROLE if you also do steps 2 and 3.
CREATE ROLE avatar_dba WITH LOGIN PASSWORD '<database.adminPassword>' CREATEDB CREATEROLE;
-- 2. The Avatar role and database. Optional: the setup creates the two if they
-- are absent. Create them here to remove CREATEDB and CREATEROLE above.
CREATE ROLE avatar WITH LOGIN PASSWORD '<database.password>';
CREATE DATABASE avatar OWNER avatar_dba;
-- 3. The Authentik role and database. This is what
-- authentikDatabase.createDatabase: false expects to find.
CREATE ROLE authentik WITH LOGIN PASSWORD '<authentik.authentik.postgresql.password>';
CREATE DATABASE authentik OWNER authentik;
\connect authentik
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "citext";
The bootstrap superuser owns the public schema, not the owner of the database. The setup does not
fail without this step. Its REVOKE CREATE ON SCHEMA public FROM PUBLIC prints
WARNING: no privileges could be revoked for "public" and continues, so the install succeeds and
that one hardening step is skipped with no error. Give the ownership to the admin account:
\connect avatar
ALTER SCHEMA public OWNER TO avatar_dba;
These values then describe what you created:
database:
host: postgres.example.com
port: "5432"
name: avatar
username: avatar
password: <database.password>
adminUsername: avatar_dba
adminPassword: <database.adminPassword>
authentikDatabase:
createDatabase: false # only if you did step 3
authentik:
authentik:
postgresql:
host: postgres.example.com
name: authentik
user: authentik
password: <authentik.authentik.postgresql.password>
Keep the passwords in Secrets, not in that file. See Using existing Secrets.
Ingress and the URL contract
One host, and which path on it serves what:
| Path | Served by | Prefix |
|---|---|---|
/api | The API | Stripped |
/web | The web client | Kept |
/storage | The S3 gateway | Stripped |
/sso, / | Authentik | Kept |
ingress.host is the only place a deployment's domain is set. Every public URL the chart hands the
API, the web client and Authentik comes from it.
| Value | Default value | Description |
|---|---|---|
ingress.enabled | true | Create the Ingress objects. Set it false to route to the services yourself. |
ingress.host | Required | The domain the platform is served on. |
ingress.authentikHost | ingress.host | Give Authentik its own domain. |
ingress.className | nginx | IngressClass. The routing needs ingress-nginx; on another controller set ingress.enabled: false. |
ingress.tls.enabled | true | Serve over TLS, and derive every public URL as https. |
ingress.tls.secretName | None | Name of an existing kubernetes.io/tls Secret. Left empty, the controller serves its own certificate. |
ingress.annotations | {} | Extra annotations on every Ingress the chart creates. |
ingress.proxyBodySize | 1024m | Upload limit. Raise it for larger datasets. |
ingress.proxyBufferSize | 16k | Response buffer size. |
ingress.proxyBuffersNumber | 4 | Number of proxy buffers. |
ingress.wwwRedirect | false | Redirect the apex domain to its www. counterpart. |
ingress.api.path | /api | Path the API is served under. Each route also takes its own annotations. |
ingress.web.path | /web | Path the web client is served under. |
ingress.sso.path | /sso | Path Authentik is served under. Must match authentik.authentik.web.path. |
ingress.storage.path | /storage | Path the object store is served under. |
ingress.storage.service.name | None | Point the storage route at your own Service instead of the in-chart SeaweedFS. |
ingress.storage.service.port | 8333 | Port of that service. |
Setting up the ingress
With ingress-nginx, which is what the chart generates for:
- Point DNS for your domain at the ingress controller's load balancer.
- Create a
kubernetes.io/tlsSecret for that domain in the release namespace. The chart never creates one. - Set
ingress.hostandingress.tls.secretName, and leaveingress.enabled: trueandingress.className: nginx.
The chart then creates the four routes above, and every public URL follows from ingress.host.
Using another ingress controller
Set ingress.enabled: false and keep ingress.host set — the chart still derives every URL from
it. Then create your own routes for the four paths above, with:
- the prefix stripped on
/apiand/storage, kept on/weband/sso x-forwarded-prefix: /storageon the storage route- a request body-size limit large enough for your dataset uploads
X-Forwarded-Forreplaced, not appended to, and the pod network of your controller insso.trustedProxyCidrs
ingress.className only applies to the objects the chart creates, and those carry ingress-nginx
annotations — pointing it at another controller does not make them work.
If you also set networkPolicy.enabled, fill in
networkPolicy.ingressController with your controller's pod labels.
API
| Value | Default value | Description |
|---|---|---|
api.image.registry | quay.io | Registry the API image is pulled from. |
api.image.repository | octopize/avatar-service-api | Repository of the API image. |
api.image.tag | 3.4.0 | API version. Set it to the version Octopize gives you; avoid latest. |
api.image.digest | None | Pull by digest instead of by tag. |
api.image.pullPolicy | IfNotPresent | Image pull policy. |
api.replicaCount | 1 | API replicas. Keep it at 1 — see Upgrading. |
api.resources | 4Gi / 1000m, requests = limits | Compute resources of the API container. |
api.containerPort | 8000 | Port the API listens on. |
api.service.type | ClusterIP | Type of the API Service. |
api.service.port | 8000 | Port the API Service exposes. |
api.service.annotations | {} | Annotations on the API Service. |
api.livenessProbe.failureThreshold | 3 | Failures before the container restarts. periodSeconds and timeoutSeconds sit alongside it. |
api.readinessProbe.failureThreshold | 3 | Failures before the pod leaves the Service. Same companions. |
api also takes the extension points for scheduling,
security and injection.
Application settings
Every URL below is derived from the ingress block when left empty. Set one only to pin it.
| Value | Default value | Description |
|---|---|---|
api.auth.jwtSecretKey | Required | JWT signing key. Generate the same way. |
api.config.organizationName | Required | Name of your organization. Same as ORGANIZATION_NAME. |
api.config.baseUrl | derived | Public API URL. Same as AVATAR_API_URL. |
api.config.webClientUrl | derived | Public web client URL, used in emails. Same as AVATAR_WEB_CLIENT_URL. |
api.config.logLevel | INFO | Same as LOG_LEVEL. |
api.config.useConsoleLogging | false | Human-readable logs instead of structured JSON. Same as USE_CONSOLE_LOGGING. |
api.config.sentry.enabled | false | Report errors to Sentry. Set dsn to your own project before turning it on. |
api.config.sentry.dsn | None | Sentry project the errors go to. |
api.config.compatibilityMappingUrl | https://license.octopize.io/compatibility/compatibility_mapping.json | Where the API downloads the client-compatibility mapping from. Left empty it uses that URL. Set it to "disabled" behind a firewall that blocks outgoing connections, and the API serves the copy shipped in its image. |
TLS and private certificate authorities
| Value | Default value | Description |
|---|---|---|
api.caBundle | None | PEM-encoded CA certificate(s) to trust, on top of the ones the image already has. |
api.caBundleIf your domain's certificate was issued by an internal or self-signed authority, every login fails
with CERTIFICATE_VERIFY_FAILED until you set it:
api:
caBundle: |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
It reaches the API pods and the sync-tables CronJob, but not the Dask workers — see Compute.
Web client
| Value | Default value | Description |
|---|---|---|
web.image.registry | quay.io | Registry the web image is pulled from. |
web.image.repository | octopize/avatar-web | Repository of the web image. |
web.image.tag | 0.68.0 | Web client version. Octopize gives it to you alongside the API version. |
web.image.digest | None | Pull by digest instead of by tag. |
web.image.pullPolicy | IfNotPresent | Image pull policy. |
web.replicaCount | 1 | Web client replicas. |
web.resources | 2Gi / 500m, requests = limits | Compute resources of the web container. |
web.containerPort | 3000 | Port the web client listens on. |
web.service.type | ClusterIP | Type of the web Service. |
web.service.port | 3000 | Port the web Service exposes. |
web.service.annotations | {} | Annotations on the web Service. |
web.livenessProbe.failureThreshold | 3 | Failures before the container restarts. |
web.readinessProbe.failureThreshold | 3 | Failures before the pod leaves the Service. |
web takes the same extension points as api.
The URLs under web.config — publicUrl, apiPublicUrl, storagePublicUrl, authentikUrl,
ssoUrl — are derived from the ingress block when left empty. Set one only to pin it.
web.config.apiInternalUrl and web.config.storageInternalUrl are cluster-internal and rarely
need changing.
The environment name the interface displays comes from
envName.
Fitting the chart to your cluster
Each workload takes the same set of optional values, all empty by default:
| Key (per workload) | Set it for |
|---|---|
nodeSelector, tolerations, affinity, topologySpreadConstraints | A labelled or tainted node pool, spreading over zones |
priorityClassName | Controlling what gets evicted first when a node fills up |
podSecurityContext, containerSecurityContext | A Pod Security Standard, or a policy engine enforcing one |
extraEnvVars | Overriding a value the chart sets |
extraVolumes, extraVolumeMounts | An injected config, a writable path, a mounted certificate |
initContainers, sidecars | A secrets agent, a logging or service-mesh sidecar |
podAnnotations, podLabels | What a sidecar injector, a scraper or a policy engine keys off |
updateStrategy | How a rollout replaces pods |
extraPodSpec | Any pod-spec field the chart does not expose |
api and web take all of them. syncTables takes all but updateStrategy.
authentikDatabase takes the scheduling keys, priorityClassName, extraPodSpec and the two
security contexts.
app.kubernetes.io/component cannot be set in podLabels.
The subcharts have their own keys for the same fields: authentik.server.securityContext and
authentik.server.containerSecurityContext, seaweedfs.<component>.podSecurityContext,
dask.worker.nodeSelector. Their environment variables have their own keys too — see
On the subcharts.
Restricted Pod Security Standard
example-values-restricted.yaml, shipped in the chart, is a ready-made overlay. Apply it on top of
your own values file:
helm install avatar oci://quay.io/octopize/helm/services-api/avatar \
--version <version> --namespace avatar --create-namespace \
--values my-values.yaml \
--values example-values-restricted.yaml
Two things to keep if you write your own instead:
- Set
runAsUseralongsiderunAsNonRoot: true, or the container will not start. It differs per image:1000for the API, sync-tables and Dask, and1001with group65533for the web client. Giving the web client1000fails quietly — it serves, but every cache write fails. - Leave
readOnlyRootFilesystemunset unless you also add a writable mount, which is whatextraVolumesandextraVolumeMountsare for.
Its pods are rejected by a restricted namespace whatever you configure, so Dask has to run
somewhere that admits it. Everything else the chart deploys is covered by the overlay.
Object storage
Where datasets and results live, and the credentials the API reaches them with.
| Value | Default value | Description |
|---|---|---|
storage.sharedPath | in-chart SeaweedFS | Where datasets and generated files live. Same as SHARED_STORAGE_PATH. |
storage.endpoint.host | in-chart SeaweedFS | Cluster-internal host of the S3 gateway. |
storage.endpoint.port | 8333 | Port of that endpoint. |
storage.admin.accessKeyId | Required | Identity the API authenticates to the object store as. Treat it as a root credential. |
storage.admin.secretAccessKey | Required | Its secret. |
storage.sseKek | Required | Key-encryption key for encryption at rest. Required when seaweedfs.enabled. |
storage.seaweedfsAdminPassword | Required | SeaweedFS admin password. Required when seaweedfs.enabled, unless seaweedfs.admin.secret.existingSecret names your own Secret. See SeaweedFS. |
storage.sseKek once and never change itA new key makes every stored object undecryptable. When migrating an existing SeaweedFS, copy the value out of the old filer — see SeaweedFS encryption.
In-chart SeaweedFS
| Value | Default value | Description |
|---|---|---|
seaweedfs.enabled | false | Deploy the in-cluster object store. example-values.yaml sets it true. |
seaweedfs.volume.dataDirs | one 20Gi PVC | The platform's disk budget. Raise it for the data you expect to hold. |
seaweedfs.filer.data | one 10Gi PVC | Filer metadata. This is the volume to back up. |
seaweedfs.filer.extraArgs | see example-values.yaml | Overriding it replaces the chart's list, so repeat the -ip argument it sets. |
seaweedfs.s3.extraArgs | see example-values.yaml | Same. Add -allowedOrigins to narrow browser CORS to the web client. |
seaweedfs.networkPolicy.enabled | false | Must equal networkPolicy.enabled. |
-allowedOriginsNeither the filer nor the S3 gateway is given the argument by default, and SeaweedFS then serves the object store to any origin. Add it to both, listing the web client URL:
seaweedfs:
filer:
extraArgs:
- "-ip=${POD_NAME}.seaweedfs-filer.${NAMESPACE}.svc.cluster.local"
- "-allowedOrigins=https://avatar.mycompany.com"
s3:
extraArgs:
- "-iam.readOnly=false"
- "-allowedOrigins=https://avatar.mycompany.com"
Helm cannot template a subchart's extraArgs, so the value cannot be derived from
api.config.webClientUrl and has to be written out.
SSO
| Value | Default value | Description |
|---|---|---|
sso.clientId | Required | OAuth client id. The blueprint reads it back from the avatar-api-config ConfigMap. |
sso.clientSecret | Required | OAuth client secret. The blueprint reads it back from the api Secret. |
sso.providerAppName | avatar-api | Leave it alone. Any other value breaks login. |
sso.providerUrl | derived | OIDC issuer. |
sso.apiToken | Required | Authentik management-API token. The blueprint reads it back from the api Secret. |
sso.webhookSecret | Required | Webhook shared secret. The blueprint reads it back from the api Secret. |
sso.trustedProxyCidrs | every private range | Networks that Authentik takes X-Forwarded-Proto and X-Forwarded-For from. See The proxy in front of Authentik. |
You choose all four. Authentik issues none of them. Make them as described in Generating the secrets. You set each one time. The blueprint reads them back from the objects that the chart writes them into.
sso.apiToken and sso.webhookSecret are what revoke the API keys of a user you disable or delete
in Authentik: the webhook secret authenticates Authentik telling the API about the event, and the
token lets the API confirm that user's status before revoking anything. API keys are checked against
Avatar's own database rather than against Authentik, so without them a user who has lost their
single sign-on keeps working API keys. The chart refuses to render when either is missing, rather
than installing a deployment where that enforcement is silently absent.
The proxy in front of Authentik
Authentik reads the scheme and the client address of a request from X-Forwarded-Proto and
X-Forwarded-For, but only when the connection comes from an address in sso.trustedProxyCidrs.
That address is the pod of your ingress controller. From Authentik 2026.8, Authentik ignores both
headers when they come from any other address.
The default holds every private range. It covers the pod network of the usual Kubernetes networks,
including 100.64.0.0/10 on Scaleway Kapsule:
sso:
trustedProxyCidrs:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- 100.64.0.0/10
- 127.0.0.0/8
- fc00::/7
- fe80::/10
- ::1/128
If your pod network is public, add it. A list that you set replaces the default, so keep the ranges that you still need. An empty list trusts no address: it does not fall back to the Authentik default.
Authentik reads this value only at start, and helm upgrade does not restart it. After each change,
run kubectl -n avatar rollout restart deploy/avatar-authentik-server.
Replace X-Forwarded-For at the ingress controller
Authentik takes the first address of X-Forwarded-For as the client address. It records that
address and gives it to its policies. Your ingress controller must replace that header with the
address that it receives the connection from. If it appends to a header that the client sent, the
client chooses the address that Authentik records. On ingress-nginx, do one of these:
- Leave
use-forwarded-headersoff. This is the ingress-nginx default. - If you need
use-forwarded-headers, setproxy-real-ip-cidrto the address of your load balancer only. - Use the PROXY protocol between the load balancer and the ingress controller.
Any pod in a trusted network that reaches the Authentik server can also set that address. Do not
expose the Authentik server outside the cluster through a Service of its own.
networkPolicy.enabled limits the pods that reach it. To
trust only the ingress controller, list the addresses of its pods in sso.trustedProxyCidrs.
The login returns to its first screen
When the ingress controller is outside sso.trustedProxyCidrs, Authentik takes every request for
plain HTTP. It builds http:// URLs and sets its session cookie without the Secure flag. The
login then returns to its first screen after each email address. A Chromium browser shows only an error
icon, because the page asks for its API over http://. To fix it:
-
Find the address of the ingress controller pod:
kubectl get pods -A -o wide -
Add its network to
sso.trustedProxyCidrs, and keep the ranges that you still need. -
Run
helm upgrade, then restart the Authentik server:kubectl -n avatar rollout restart deploy/avatar-authentik-server -
Log in again, and read the log of the Authentik server:
kubectl -n avatar logs deploy/avatar-authentik-server --tail=100A login request shows
"scheme": "https", and its"remote"is the address of the client, not the address of the ingress controller pod.
SMTP
Left empty, anything that would send a message fails.
| Value | Default value | Docker equivalent |
|---|---|---|
smtp.host | None | SMTP_HOST |
smtp.port | 587 | SMTP_PORT |
smtp.useTls | true | SMTP_USE_TLS |
smtp.startTls | false | SMTP_START_TLS |
smtp.verify | true | SMTP_VERIFY |
smtp.senderEmail | None | SMTP_SENDER_EMAIL |
smtp.username | None | SMTP_USERNAME |
smtp.password | None | SMTP_PASSWORD |
Authentik sends its own mail and is configured separately under authentik.authentik.email. Set
both.
Telemetry
Telemetry lets Octopize support you faster, follow the versions you run and monitor license usage. Only system data, job metadata and user data (email and organization) are sent — no sensitive data and no datasets. Octopize provides the bucket name and credentials. See Data sent to Octopize for why we recommend it and what is sent.
| Value | Docker equivalent |
|---|---|
telemetry.s3.endpointUrl | TELEMETRY_S3_ENDPOINT_URL |
telemetry.s3.region | TELEMETRY_S3_REGION |
telemetry.s3.bucketName | TELEMETRY_S3_BUCKET_NAME |
telemetry.s3.accessKeyId | telemetry_s3_access_key_id |
telemetry.s3.secretAccessKey | telemetry_s3_secret_access_key |
The export runs as the sync-tables CronJob, so it also needs
syncTables.enabled.
Scheduled jobs
| Value | Default value | Description |
|---|---|---|
syncTables.enabled | false | Enable the table-synchronisation CronJob. |
syncTables.schedule | 0 1 * * * | Cron schedule expression. |
syncTables.failureEmail | None | Address to notify on failure. Required for the CronJob to be created. |
This CronJob is what exports telemetry, so enable it if you send telemetry. It runs
the API image, so api.caBundle reaches it too, and it takes the full set of
extension points under syncTables.*.
Authentik
Avatar embeds Authentik as its identity provider. It configures itself on first start; the only manual step is the first admin account.
| Value | Default value | Description |
|---|---|---|
authentik.enabled | true | Whether to deploy Authentik. |
authentik.global.image.tag | pinned by the chart | Authentik image tag. |
authentik.global.env | see below | Blueprint variables. |
authentik.authentik.secret_key | Required | Signing key for Authentik sessions. |
authentik.authentik.log_level | info | Authentik log level. |
authentik.authentik.web.path | /sso/ | Must match ingress.sso.path with a trailing slash. |
authentik.authentik.postgresql.host | Required | PostgreSQL server for Authentik's own database. |
authentik.authentik.postgresql.port | 5432 | Its port. |
authentik.authentik.postgresql.name | authentik | Database name. |
authentik.authentik.postgresql.user | authentik | Role name. |
authentik.authentik.postgresql.password | Required | Password for that role. |
authentik.authentik.email.* | see example-values.yaml | SMTP settings Authentik sends its own mail through. |
The chart derives the domain and redirect URIs from ingress.host, so only these entries under
authentik.global.env are left to set. They are described in full in
Blueprint variables. Helm replaces a list rather
than merging into it, so a values file that sets authentik.global.env at all has to restate every
entry it needs.
| Environment variable | Description |
|---|---|
AVATAR_AUTHENTIK_BLUEPRINT_CLIENT_ID | Refers to sso.clientId. Leave it as it is. |
AVATAR_AUTHENTIK_BLUEPRINT_CLIENT_SECRET | Refers to sso.clientSecret. Leave it as it is. |
AVATAR_AUTHENTIK_BLUEPRINT_SELF_SERVICE_LICENSE | License type assigned to self-service signup users. |
AVATAR_AUTHENTIK_BLUEPRINT_API_TOKEN | Refers to sso.apiToken. Leave it as it is. |
AVATAR_AUTHENTIK_BLUEPRINT_WEBHOOK_SECRET | Refers to sso.webhookSecret. Leave it as it is. |
_GOOGLE_CLIENT_ID / _GOOGLE_CLIENT_SECRET | Optional. Set both to add a Google OAuth source. |
Each of them can take a valueFrom.secretKeyRef in place of value. See
Authentik. Use the same list to override any other
Authentik setting, including a key that the chart makes itself. See
On the subcharts.
AVATAR_AUTHENTIK_BLUEPRINT_API_WEBHOOK_URL is not in that list. The chart derives it from
api.service.port and delivers it to the Authentik pods through the avatar-authentik-config
ConfigMap named in authentik.global.envFrom, so it survives the list replacement above and needs
no entry. An entry of that name still wins if you add one — Kubernetes gives env precedence over
envFrom — but one restated with an empty value would switch the user-event webhook off, and the
chart refuses to render rather than let that pass unnoticed.
Bootstrapping the Authentik database
A Job creates Authentik's role, database and extensions on first install.
| Value | Default value | Description |
|---|---|---|
authentikDatabase.createDatabase | true | Set false when a DBA has already created them. |
authentikDatabase.image | postgres:17-alpine | Anything with a psql client at least as new as the server. |
authentikDatabase.imagePullPolicy | IfNotPresent | Image pull policy for the Job. |
authentikDatabase.backoffLimit | 5 | Retries before the Job gives up. |
authentikDatabase.ttlSecondsAfterFinished | 3600 | How long the finished Job stays around to be read with kubectl logs. |
It also takes nodeSelector, tolerations, priorityClassName and the two security contexts —
see Fitting the chart to your cluster.
Compute
The Dask scheduler and workers run the avatarization jobs.
| Value | Default value | Description |
|---|---|---|
dask.enabled | true | Whether to deploy the Dask scheduler and workers. |
dask.scheduler.image.repository | quay.io/octopize/avatar-service-api | Scheduler image. Override it for a mirrored install. |
dask.scheduler.image.tag | 3.4.0 | Must equal api.image.tag. |
dask.worker.image.repository | quay.io/octopize/avatar-service-api | Worker image. |
dask.worker.image.tag | 3.4.0 | Must equal api.image.tag. |
dask.worker.replicas | 2 | Number of workers. The main dial for avatarization throughput. |
dask.worker.resources | requests 2Gi / 1 CPU, limits 32Gi / 1 CPU | Raise the memory limit for large datasets. |
dask.jupyter.enabled | false | Leave it off. |
The Dask pods pull with docker-local-pull-secret. If you bring your own pull secret through
global.imagePullSecrets, edit dask.{scheduler,worker}.image.pullSecrets to match.
api.caBundle does not reach the Dask workersIf a worker talks TLS to anything using your private CA, point the workers at it with the block
commented out under dask.worker in values.yaml. There the CA bundle replaces the trust store, so
it has to carry any public roots the worker also needs.
Dask is also the one component that cannot run under a restricted Pod Security Standard.
Network isolation
| Value | Default value | Description |
|---|---|---|
networkPolicy.enabled | false | Create NetworkPolicy objects. Needs a CNI that enforces them. |
networkPolicy.ingressController.podSelector | {} | Pod labels of your ingress controller, so it is allowed to reach the platform. |
networkPolicy.ingressController.namespaceSelector | {} | Only needed when the ingress controller runs in a different namespace than the release. |
seaweedfs.networkPolicy.enabled must be set to the same value; the chart refuses to render
otherwise.