Skip to main content

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​

ValueDefault valueDescription
envNameNoneEnvironment name, shown for support purposes. Same as ENV_NAME.
imageCredentials.dockerConfigJsonRequiredbase64 of a Docker config.json with quay.io/octopize credentials. Octopize provides them.
global.imageRegistryNonePull 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.
nameOverrideNoneValue 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
ValueConstraintIf you change it later
api.auth.jwtSecretKeyNoneEach user must log in again. No stored data is lost
database.passwordAny text that PostgreSQL accepts as a passwordChange it on the server first. See Rotating a database password
storage.admin.accessKeyIdNoneSafe 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.secretAccessKeyNoneAs above
storage.sseKek64 lowercase hexadecimal characters. The chart checks this only when it writes the Secret itselfNever change it. Each stored object becomes unreadable
storage.seaweedfsAdminPasswordNoneRestart the SeaweedFS admin server: kubectl rollout restart statefulset/seaweedfs-admin
sso.clientIdNone. It is not a secret, but you make it in the same wayRestart the Authentik server and worker, then apply the blueprint again
sso.clientSecretNoneRestart the Authentik server and worker, then apply the blueprint again
sso.apiTokenNoneRestart the Authentik server and worker, then apply the blueprint again
sso.webhookSecretNoneRestart the Authentik server and worker, then apply the blueprint again
authentik.authentik.secret_keyNone.Each user must log in to Authentik again
authentik.authentik.postgresql.passwordAny text that PostgreSQL accepts as a passwordChange it on the server first. See Rotating a database password
connector.secretNone. Only read when connector.enabled is trueThe tokens of running database jobs become invalid. See Database connector
databaseConnections.encryptionKeysA Fernet key, not a hex string. See Database connectorPut 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:

BlockCredentialKey it looks for
databasepassword, adminPassworddb-password, db-admin-password
api.authjwtSecretKeyauthjwt-secret-key
storageadmin.accessKeyId, admin.secretAccessKeystorage-access-key-id, storage-secret-access-key
ssoclientSecret, apiToken, webhookSecretsso-client-secret, sso-api-token, sso-webhook-secret
smtppasswordsmtp-password
telemetry.s3accessKeyId, secretAccessKeytelemetry-s3-access-key-id, telemetry-s3-secret-access-key
authentikDatabaseAuthentik's database passwordauthentik-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 secretKeys names 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 secretKeys entry empty.
  • If you set a plain value and existingSecret together, 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 restart on 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.

ConfigMapRead byMade from
avatar-api-configthe API pods, the sync-tables CronJob, the Dask scheduler, the helm test Podsingress, envName, database, storage, sso, smtp, telemetry, api.config
avatar-web-configthe web podsingress, storage, web.config, envName
avatar-authentik-configthe Authentik server and workeringress, 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:

SubchartKeyShape
authentikauthentik.global.env for both pods, authentik.server.env or authentik.worker.env for onea list of env entries with value or valueFrom
daskdask.scheduler.env, dask.worker.envthe same list shape
seaweedfsseaweedfs.global.seaweedfs.extraEnvironmentVars for all components, seaweedfs.<component>.extraEnvironmentVars for onea map of variable names: a string becomes value, a map becomes valueFrom
  • Authentik. avatar-authentik-config arrives through authentik.global.envFrom. Kubernetes gives env a higher priority than envFrom, so a global.env entry with the same name replaces the key. If you override envFrom, keep the configMapRef entry. Without it, Authentik loses the domain that it registers the Avatar application under.
  • Dask. dask.scheduler.env already reads ENV_NAME and IS_SENTRY_ENABLED from avatar-api-config with configMapKeyRef. Read any other key of that ConfigMap, or a key of your own Secret, in the same way.
  • SeaweedFS. The extraEnvironmentVars of a component replaces the global entry with the same name. A map value becomes a valueFrom, as WEED_S3_SSE_KEK above shows.
dask:
worker:
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: avatar-api-config
key: LOG_LEVEL
Helm replaces an env list, it does not merge it

If 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 container reads its environment once, at start

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.

ValueDefault valueDescription
database.hostRequiredHostname of the PostgreSQL server.
database.port5432Port of that server.
database.nameavatarName of the Avatar database. Same as DB_NAME.
database.usernameavatarRole that the API connects as. Same as DB_USER.
database.passwordRequiredPassword of that role.
database.adminUsernameRequiredAdmin account that creates the roles and the databases. Not mounted into the running API.
database.adminPasswordRequiredPassword of the admin account.

What the chart creates​

ObjectDefault nameCreated byConnects as
The Avatar roledatabase.username, avatarthe init-avatar-api init container, which runs bin/dbtool.py setupdatabase.adminUsername
The Avatar database, its tables and the first organizationdatabase.name, avatarthe same init containerdatabase.adminUsername
The Authentik roleauthentik.authentik.postgresql.user, authentikthe avatar-authentik-db-init-<chart-version> Jobdatabase.adminUsername
The Authentik database, with the uuid-ossp and citext extensionsauthentik.authentik.postgresql.name, authentikthe same Jobdatabase.adminUsername
The Authentik tables—the Authentik server, at the first startauthentik.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​

RoleDoesUsed by
database.adminUsernamecreates the roles and databases, sets up the schema, migratesthe init-avatar-api init container and the Authentik bootstrap Job
database.usernameeach query that Avatar does at run timethe API container, the sync-tables CronJob and the helm test Pods
authentik.authentik.postgresql.userthe schema and the queries of Authentikthe 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 needsBecause
LOGINIt connects to the server.
CONNECT on template1The 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 postgresThe Authentik bootstrap Job does the same from the postgres database.
CREATEROLEIt creates the two roles. Not necessary if you create them.
CREATEDBIt creates the two databases. Not necessary if you create them.
Ownership of the Avatar database and of its public schemaThe 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.

This failure is loud, and it loses nothing

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";
PostgreSQL 14 and earlier

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:

PathServed byPrefix
/apiThe APIStripped
/webThe web clientKept
/storageThe S3 gatewayStripped
/sso, /AuthentikKept

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.

ValueDefault valueDescription
ingress.enabledtrueCreate the Ingress objects. Set it false to route to the services yourself.
ingress.hostRequiredThe domain the platform is served on.
ingress.authentikHostingress.hostGive Authentik its own domain.
ingress.classNamenginxIngressClass. The routing needs ingress-nginx; on another controller set ingress.enabled: false.
ingress.tls.enabledtrueServe over TLS, and derive every public URL as https.
ingress.tls.secretNameNoneName 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.proxyBodySize1024mUpload limit. Raise it for larger datasets.
ingress.proxyBufferSize16kResponse buffer size.
ingress.proxyBuffersNumber4Number of proxy buffers.
ingress.wwwRedirectfalseRedirect the apex domain to its www. counterpart.
ingress.api.path/apiPath the API is served under. Each route also takes its own annotations.
ingress.web.path/webPath the web client is served under.
ingress.sso.path/ssoPath Authentik is served under. Must match authentik.authentik.web.path.
ingress.storage.path/storagePath the object store is served under.
ingress.storage.service.nameNonePoint the storage route at your own Service instead of the in-chart SeaweedFS.
ingress.storage.service.port8333Port of that service.

Setting up the ingress​

With ingress-nginx, which is what the chart generates for:

  1. Point DNS for your domain at the ingress controller's load balancer.
  2. Create a kubernetes.io/tls Secret for that domain in the release namespace. The chart never creates one.
  3. Set ingress.host and ingress.tls.secretName, and leave ingress.enabled: true and ingress.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 /api and /storage, kept on /web and /sso
  • x-forwarded-prefix: /storage on the storage route
  • a request body-size limit large enough for your dataset uploads
  • X-Forwarded-For replaced, not appended to, and the pod network of your controller in sso.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​

ValueDefault valueDescription
api.image.registryquay.ioRegistry the API image is pulled from.
api.image.repositoryoctopize/avatar-service-apiRepository of the API image.
api.image.tag3.4.0API version. Set it to the version Octopize gives you; avoid latest.
api.image.digestNonePull by digest instead of by tag.
api.image.pullPolicyIfNotPresentImage pull policy.
api.replicaCount1API replicas. Keep it at 1 — see Upgrading.
api.resources4Gi / 1000m, requests = limitsCompute resources of the API container.
api.containerPort8000Port the API listens on.
api.service.typeClusterIPType of the API Service.
api.service.port8000Port the API Service exposes.
api.service.annotations{}Annotations on the API Service.
api.livenessProbe.failureThreshold3Failures before the container restarts. periodSeconds and timeoutSeconds sit alongside it.
api.readinessProbe.failureThreshold3Failures 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.

ValueDefault valueDescription
api.auth.jwtSecretKeyRequiredJWT signing key. Generate the same way.
api.config.organizationNameRequiredName of your organization. Same as ORGANIZATION_NAME.
api.config.baseUrlderivedPublic API URL. Same as AVATAR_API_URL.
api.config.webClientUrlderivedPublic web client URL, used in emails. Same as AVATAR_WEB_CLIENT_URL.
api.config.logLevelINFOSame as LOG_LEVEL.
api.config.useConsoleLoggingfalseHuman-readable logs instead of structured JSON. Same as USE_CONSOLE_LOGGING.
api.config.sentry.enabledfalseReport errors to Sentry. Set dsn to your own project before turning it on.
api.config.sentry.dsnNoneSentry project the errors go to.
api.config.compatibilityMappingUrlhttps://license.octopize.io/compatibility/compatibility_mapping.jsonWhere 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​

ValueDefault valueDescription
api.caBundleNonePEM-encoded CA certificate(s) to trust, on top of the ones the image already has.
A certificate from a private CA needs api.caBundle

If 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​

ValueDefault valueDescription
web.image.registryquay.ioRegistry the web image is pulled from.
web.image.repositoryoctopize/avatar-webRepository of the web image.
web.image.tag0.68.0Web client version. Octopize gives it to you alongside the API version.
web.image.digestNonePull by digest instead of by tag.
web.image.pullPolicyIfNotPresentImage pull policy.
web.replicaCount1Web client replicas.
web.resources2Gi / 500m, requests = limitsCompute resources of the web container.
web.containerPort3000Port the web client listens on.
web.service.typeClusterIPType of the web Service.
web.service.port3000Port the web Service exposes.
web.service.annotations{}Annotations on the web Service.
web.livenessProbe.failureThreshold3Failures before the container restarts.
web.readinessProbe.failureThreshold3Failures 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.

info

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, topologySpreadConstraintsA labelled or tainted node pool, spreading over zones
priorityClassNameControlling what gets evicted first when a node fills up
podSecurityContext, containerSecurityContextA Pod Security Standard, or a policy engine enforcing one
extraEnvVarsOverriding a value the chart sets
extraVolumes, extraVolumeMountsAn injected config, a writable path, a mounted certificate
initContainers, sidecarsA secrets agent, a logging or service-mesh sidecar
podAnnotations, podLabelsWhat a sidecar injector, a scraper or a policy engine keys off
updateStrategyHow a rollout replaces pods
extraPodSpecAny 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 runAsUser alongside runAsNonRoot: true, or the container will not start. It differs per image: 1000 for the API, sync-tables and Dask, and 1001 with group 65533 for the web client. Giving the web client 1000 fails quietly — it serves, but every cache write fails.
  • Leave readOnlyRootFilesystem unset unless you also add a writable mount, which is what extraVolumes and extraVolumeMounts are for.
The Dask subchart cannot run under this profile

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.

ValueDefault valueDescription
storage.sharedPathin-chart SeaweedFSWhere datasets and generated files live. Same as SHARED_STORAGE_PATH.
storage.endpoint.hostin-chart SeaweedFSCluster-internal host of the S3 gateway.
storage.endpoint.port8333Port of that endpoint.
storage.admin.accessKeyIdRequiredIdentity the API authenticates to the object store as. Treat it as a root credential.
storage.admin.secretAccessKeyRequiredIts secret.
storage.sseKekRequiredKey-encryption key for encryption at rest. Required when seaweedfs.enabled.
storage.seaweedfsAdminPasswordRequiredSeaweedFS admin password. Required when seaweedfs.enabled, unless seaweedfs.admin.secret.existingSecret names your own Secret. See SeaweedFS.
Generate storage.sseKek once and never change it

A 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​

ValueDefault valueDescription
seaweedfs.enabledfalseDeploy the in-cluster object store. example-values.yaml sets it true.
seaweedfs.volume.dataDirsone 20Gi PVCThe platform's disk budget. Raise it for the data you expect to hold.
seaweedfs.filer.dataone 10Gi PVCFiler metadata. This is the volume to back up.
seaweedfs.filer.extraArgssee example-values.yamlOverriding it replaces the chart's list, so repeat the -ip argument it sets.
seaweedfs.s3.extraArgssee example-values.yamlSame. Add -allowedOrigins to narrow browser CORS to the web client.
seaweedfs.networkPolicy.enabledfalseMust equal networkPolicy.enabled.
SeaweedFS allows every browser origin until you set -allowedOrigins

Neither 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​

ValueDefault valueDescription
sso.clientIdRequiredOAuth client id. The blueprint reads it back from the avatar-api-config ConfigMap.
sso.clientSecretRequiredOAuth client secret. The blueprint reads it back from the api Secret.
sso.providerAppNameavatar-apiLeave it alone. Any other value breaks login.
sso.providerUrlderivedOIDC issuer.
sso.apiTokenRequiredAuthentik management-API token. The blueprint reads it back from the api Secret.
sso.webhookSecretRequiredWebhook shared secret. The blueprint reads it back from the api Secret.
sso.trustedProxyCidrsevery private rangeNetworks that Authentik takes X-Forwarded-Proto and X-Forwarded-For from. See The proxy in front of Authentik.
tip

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-headers off. This is the ingress-nginx default.
  • If you need use-forwarded-headers, set proxy-real-ip-cidr to 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:

  1. Find the address of the ingress controller pod:

    kubectl get pods -A -o wide
  2. Add its network to sso.trustedProxyCidrs, and keep the ranges that you still need.

  3. Run helm upgrade, then restart the Authentik server:

    kubectl -n avatar rollout restart deploy/avatar-authentik-server
  4. Log in again, and read the log of the Authentik server:

    kubectl -n avatar logs deploy/avatar-authentik-server --tail=100

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

ValueDefault valueDocker equivalent
smtp.hostNoneSMTP_HOST
smtp.port587SMTP_PORT
smtp.useTlstrueSMTP_USE_TLS
smtp.startTlsfalseSMTP_START_TLS
smtp.verifytrueSMTP_VERIFY
smtp.senderEmailNoneSMTP_SENDER_EMAIL
smtp.usernameNoneSMTP_USERNAME
smtp.passwordNoneSMTP_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.

ValueDocker equivalent
telemetry.s3.endpointUrlTELEMETRY_S3_ENDPOINT_URL
telemetry.s3.regionTELEMETRY_S3_REGION
telemetry.s3.bucketNameTELEMETRY_S3_BUCKET_NAME
telemetry.s3.accessKeyIdtelemetry_s3_access_key_id
telemetry.s3.secretAccessKeytelemetry_s3_secret_access_key

The export runs as the sync-tables CronJob, so it also needs syncTables.enabled.

Scheduled jobs​

ValueDefault valueDescription
syncTables.enabledfalseEnable the table-synchronisation CronJob.
syncTables.schedule0 1 * * *Cron schedule expression.
syncTables.failureEmailNoneAddress 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.

ValueDefault valueDescription
authentik.enabledtrueWhether to deploy Authentik.
authentik.global.image.tagpinned by the chartAuthentik image tag.
authentik.global.envsee belowBlueprint variables.
authentik.authentik.secret_keyRequiredSigning key for Authentik sessions.
authentik.authentik.log_levelinfoAuthentik log level.
authentik.authentik.web.path/sso/Must match ingress.sso.path with a trailing slash.
authentik.authentik.postgresql.hostRequiredPostgreSQL server for Authentik's own database.
authentik.authentik.postgresql.port5432Its port.
authentik.authentik.postgresql.nameauthentikDatabase name.
authentik.authentik.postgresql.userauthentikRole name.
authentik.authentik.postgresql.passwordRequiredPassword for that role.
authentik.authentik.email.*see example-values.yamlSMTP 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 variableDescription
AVATAR_AUTHENTIK_BLUEPRINT_CLIENT_IDRefers to sso.clientId. Leave it as it is.
AVATAR_AUTHENTIK_BLUEPRINT_CLIENT_SECRETRefers to sso.clientSecret. Leave it as it is.
AVATAR_AUTHENTIK_BLUEPRINT_SELF_SERVICE_LICENSELicense type assigned to self-service signup users.
AVATAR_AUTHENTIK_BLUEPRINT_API_TOKENRefers to sso.apiToken. Leave it as it is.
AVATAR_AUTHENTIK_BLUEPRINT_WEBHOOK_SECRETRefers to sso.webhookSecret. Leave it as it is.
_GOOGLE_CLIENT_ID / _GOOGLE_CLIENT_SECRETOptional. 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.

ValueDefault valueDescription
authentikDatabase.createDatabasetrueSet false when a DBA has already created them.
authentikDatabase.imagepostgres:17-alpineAnything with a psql client at least as new as the server.
authentikDatabase.imagePullPolicyIfNotPresentImage pull policy for the Job.
authentikDatabase.backoffLimit5Retries before the Job gives up.
authentikDatabase.ttlSecondsAfterFinished3600How 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.

ValueDefault valueDescription
dask.enabledtrueWhether to deploy the Dask scheduler and workers.
dask.scheduler.image.repositoryquay.io/octopize/avatar-service-apiScheduler image. Override it for a mirrored install.
dask.scheduler.image.tag3.4.0Must equal api.image.tag.
dask.worker.image.repositoryquay.io/octopize/avatar-service-apiWorker image.
dask.worker.image.tag3.4.0Must equal api.image.tag.
dask.worker.replicas2Number of workers. The main dial for avatarization throughput.
dask.worker.resourcesrequests 2Gi / 1 CPU, limits 32Gi / 1 CPURaise the memory limit for large datasets.
dask.jupyter.enabledfalseLeave it off.
info

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 workers

If 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​

ValueDefault valueDescription
networkPolicy.enabledfalseCreate 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.