Operations

Upgrades & configuration

What `helm upgrade` does, the seed and migration hooks, the destination the charts install, validated values, and bringing your own Postgres, Redis, or object storage.

Everything helm upgrade does to the fabric-web release is one of two things: apply the schema migration, or roll the Pods. Both are values-driven, and every value is validated before anything is created. The fabric-worker release has no schema step of its own — the API owns the schema — so upgrading it is a rollout of its worker Pods.

What an upgrade does, in order

  1. Helm renders fabric-web with the new values and validates them against values.schema.json — a bad override fails here, naming the key, with nothing created.
  2. Helm runs the <release>-migrate Job first (a pre-upgrade hook): it waits for the metadata database, then runs the same migration steps a fresh install runs — makemigrations, migrate, and the admin bootstrap. Because Helm finishes hooks before it touches the release's resources, the schema is current while the old Pods are still serving.
  3. Then the Deployment rolls. The new Pod's own startup migrate is a no-op, because the hook already applied it.
  4. After the release is applied, the <release>-seed-destination Job (a post-upgrade hook) reconciles the default Iceberg destination with the values you just installed — see the default destination. It runs after the ConfigMap is updated on purpose: the migrate Job reads the previous ConfigMap, so a seed there would silently do nothing on the upgrade that first sets these values.

Runner Pods are not part of the rollout: a runner is recreated only when it stops, or when its image reference changes (the periodic sweep notices a republished tag — see runner.imagePollSeconds). Either way it resumes from its last committed cursor, so an upgrade does not reset a pipeline.

Bringing your own dependencies

The bundled Postgres and Redis are fabric-web's installs-for-you, not requirements, and object storage is always yours — neither chart installs a bucket. Each has a documented place to point Fabric at what you already run:

Instead ofSet
Bundled Postgres in fabric-web (Fabric's metadata DB)postgres.enabled: false, then database.host, database.port, database.name, database.user, database.password
Bundled Redis in fabric-web (Celery broker)redis.enabled: false and redis.external.url, e.g. redis://my-redis.example.com:6379

Both are fabric-web values. fabric-worker bundles no broker: it reads the Redis coordinates the web release publishes, so the broker you name here is the one its workers use.

The default destination

Fabric writes to a destination — a row in its own database holding the warehouse location, the Iceberg catalog, and the object storage credentials. fabric-web seeds one from destination.*, so a fresh install can stream as soon as a source exists, with nothing to build in the UI first. The bucket is yours: the warehouse and its credentials have no default and the install fails without them.

ValueDefault
destination.typeminio — any S3-compatible endpoint, path-style; use s3 for AWS virtual-hosted addressing
destination.warehouserequired when enabled, e.g. s3://my-bucket/warehouse/
destination.s3Endpointrequired when enabled, e.g. https://s3.us-east-1.amazonaws.com
destination.s3AccessKeyId / s3SecretAccessKeyrequired when enabled — or the latter in fabric.existingSecret
destination.catalogUrithe metadata Postgres — jdbc:postgresql://<database.host>:<database.port>/<database.name>
destination.catalogUser / catalogPassworddatabase.user / database.password
destination.catalogType / catalogNamejdbc / iceberg
destination.s3Region / s3PathStyleAccessus-east-1 / true

Override whichever ones describe your warehouse — a values file, not a UI step:

destination:
  warehouse: s3://my-bucket/warehouse/
  s3Endpoint: https://s3.us-east-1.amazonaws.com
  s3AccessKeyId: AKIA...
  catalogUri: jdbc:postgresql://my-postgres:5432/iceberg

On a fresh install the app's own startup seeds it; on an upgrade the post-upgrade Job does. Both run the same idempotent command, and with destination.enabled: false both do nothing — you add destinations yourself under Destinations in the UI, exactly as before this value existed.

A Docker Compose install never runs that Job, but its entrypoint runs the same command with the same DESTINATION_* variables. If you point a compose stack at your own Postgres and S3, set them in your env file; with none set, the only default is the row migration 0019 seeds for the bundled compose MinIO and Postgres (legacy-env), so add your destination under Destinations in the UI instead.

Which row fabric-web owns

The seeded row is named chart-default, and fabric-web updates that row in place on every install and upgrade. It never edits a destination you created, and it only takes the default flag while the current default is one of its own rows — the row migration 0019 seeds from the old environment variables (legacy-env, whose compose-era credentials are what a Helm install must replace) or chart-default itself. Point your sources at your own destination and it stays the default.

A misconfigured destination fails loudly

A half-set destination block fails the upgrade's seed Job with the names of the missing keys, instead of leaving a destination that cannot connect and a runner reporting password authentication failed. With fabric.existingSecret, your Secret supplies DESTINATION_CATALOG_PASSWORD and DESTINATION_S3_SECRET_ACCESS_KEY; the remaining keys come from fabric-web's ConfigMap.

Credentials

Preferred: create the Secret yourself and point fabric-web at it with fabric.existingSecret — from Vault, External Secrets Operator, Sealed Secrets, or kubectl create secret. fabric-web then templates no Secret and never sees the plaintext. fabric-worker templates no Secret at all: it requires fabric.existingSecret to name the web release's, so the runners decrypt stored credentials with the same key the API does. The keys fabric-web expects are:

  • FABRIC_SECRET_KEY — Django signing key
  • FABRIC_ADMIN_PASSWORD — the initial admin password
  • FABRIC_DB_PASSWORD — the metadata database
  • FABRIC_ENCRYPTION_KEY — optional; pins the key that encrypts stored source and destination credentials, so rotating FABRIC_SECRET_KEY does not orphan them
  • DESTINATION_CATALOG_PASSWORD and DESTINATION_S3_SECRET_ACCESS_KEY — the seeded default destination's credentials (see the default destination); needed only while destination.enabled is true
  • ZOHO_OAUTH_CLIENT_ID / ZOHO_OAUTH_CLIENT_SECRET, EMAIL_HOST_PASSWORD — optional, if you use those integrations

Without existingSecret, fabric-web templates the Secret from fabric.secretKey, fabric.adminPassword, and database.password — fine for an evaluation install, not a way to hold production credentials.

Permissions the worker chart asks for

fabric-worker creates one ServiceAccount, one Role, and one RoleBinding, all in its own release's namespace: create, get, list, and delete Pods, plus read Pod logs. That is what replaces a mounted Docker socket — Fabric manages runners through the Kubernetes API and nothing else, and a runner Pod carries no credentials of its own. If your platform team prefers to own the RBAC, set serviceAccount.create: false (and rbac.create: false) and bind the Role to an account you manage; serviceAccount.annotations is the hook for IRSA / Workload Identity.

Versions

Each chart's version tracks the product's release line, and appVersion is the default image tag (image.tag overrides it, and a digest is the immutable choice). Upgrade is helm upgrade --install with your values file, once per release: the migration hook runs in fabric-web, the Pods roll, and the runners keep their cursors. Rollback is helm rollback — the schema may be ahead of the image you roll back to, so roll back the image and the charts together.

After install

Helm prints a NOTES block on install: how to reach the UI on your Service type, which Secret holds the admin password, and the kubectl lines for runner Pods and logs.