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
- Helm renders
fabric-webwith the new values and validates them againstvalues.schema.json— a bad override fails here, naming the key, with nothing created. - Helm runs the
<release>-migrateJob first (apre-upgradehook): 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. - Then the Deployment rolls. The new Pod's own startup migrate is a no-op, because the hook already applied it.
- After the release is applied, the
<release>-seed-destinationJob (apost-upgradehook) 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 of | Set |
|---|---|
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.
| Value | Default |
|---|---|
destination.type | minio — any S3-compatible endpoint, path-style; use s3 for AWS virtual-hosted addressing |
destination.warehouse | required when enabled, e.g. s3://my-bucket/warehouse/ |
destination.s3Endpoint | required when enabled, e.g. https://s3.us-east-1.amazonaws.com |
destination.s3AccessKeyId / s3SecretAccessKey | required when enabled — or the latter in fabric.existingSecret |
destination.catalogUri | the metadata Postgres — jdbc:postgresql://<database.host>:<database.port>/<database.name> |
destination.catalogUser / catalogPassword | database.user / database.password |
destination.catalogType / catalogName | jdbc / iceberg |
destination.s3Region / s3PathStyleAccess | us-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/icebergOn 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
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 keyFABRIC_ADMIN_PASSWORD— the initialadminpasswordFABRIC_DB_PASSWORD— the metadata databaseFABRIC_ENCRYPTION_KEY— optional; pins the key that encrypts stored source and destination credentials, so rotatingFABRIC_SECRET_KEYdoes not orphan themDESTINATION_CATALOG_PASSWORDandDESTINATION_S3_SECRET_ACCESS_KEY— the seeded default destination's credentials (see the default destination); needed only whiledestination.enabledistrueZOHO_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
kubectl lines for runner Pods and logs.