{
  "schema_version": 2,
  "id": "operate/featureform/deploy",
  "title": "Deploy",
  "url": "https://redis.io/docs/latest/operate/featureform/deploy/",
  "summary": "Deploy and operate Feature Form on Kubernetes.",
  "tags": [
    "docs",
    "operate",
    "featureform"
  ],
  "last_updated": "2026-10-05T16:11:44+01:00",
  "page_type": "content",
  "content_hash": "c352ef64f1aed27b7ab170aec4f6f8568978d20b3c95382539a4a1d7fb955beb",
  "sections": [
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "Install Redis Feature Form on Kubernetes with the Helm chart, durable PostgreSQL state, OpenID Connect (OIDC) authentication, and a license key."
    },
    {
      "id": "install",
      "title": "Install",
      "role": "setup",
      "text": "Prerequisites: For production environments, use an external PostgreSQL database and keep credentials and the license key in Kubernetes Secrets.\n\nTo get started:\n\n1. Get the chart.\n2. Create the namespace and Secrets.\n3. Create a values file.\n4. Render and install the release.\n5. Verify the deployment.\n6. Install and connect the `ff` command-line interface (CLI)."
    },
    {
      "id": "prerequisites",
      "title": "Prerequisites",
      "role": "content",
      "text": "- Kubernetes 1.27+.\n- Helm 3.14+.\n- Network access to the chart and image repositories. Configure `imagePullSecrets` if your cluster requires registry credentials.\n- An OIDC issuer URL, an API audience, and a CLI client configured for device authorization. See [Register a CLI client](https://redis.io/docs/latest/operate/featureform/configure-auth#register-a-cli-client).\n- An external PostgreSQL database for production state. Its role must be able to create and alter Feature Form tables during migrations.\n- A Feature Form license key from your Redis account team.\n- A public domain name and Transport Layer Security (TLS) certificate for each externally exposed endpoint."
    },
    {
      "id": "1-get-the-chart",
      "title": "1. Get the chart",
      "role": "content",
      "text": "The Feature Form Helm chart is published as an Open Container Initiative (OCI) artifact on Docker Hub:\n\n[code example]\n\nInstall the chart directly from this path. Always pin `--version` to the Feature Form version you intend to run."
    },
    {
      "id": "2-create-the-namespace-and-secrets",
      "title": "2. Create the namespace and Secrets",
      "role": "content",
      "text": "Create the release namespace before creating referenced Secrets:\n\n[code example]\n\nUse your normal secret-management process to create these Secrets in the same namespace as Feature Form:\n\n- `featureform-postgres`, with a `POSTGRES_URL` key containing the PostgreSQL connection URL. Require TLS according to your database policy.\n- `featureform-license`, with the license key stored in `license.key`.\n\nThe Secret names and local file paths in these examples aren't required. If you choose different Secret names or keys, update the matching `postgres.*` and `license.*` settings in the values file. You can also choose a different values filename and pass it to Helm with `--values`.\n\nFor example, create the PostgreSQL Secret from a restricted environment file whose entry is `POSTGRES_URL=<postgres-connection-url>`:\n\n[code example]\n\nCreate the license Secret from a restricted key file:\n\n[code example]\n\nCreate an image-pull Secret as well if your cluster requires registry authentication.\n\nAdd this entry when you create `values-production.yaml`:\n\n[code example]"
    },
    {
      "id": "3-create-a-values-file",
      "title": "3. Create a values file",
      "role": "content",
      "text": "Create `values-production.yaml` and set `auth.deploymentID` to a stable, lowercase ASCII identifier for this Feature Form environment, such as `acme-featureform-prod-us-west-2`. Keep the value the same across replicas and upgrades, and use a different value for each environment.\n\nFeature Form accepts any value that isn't empty after trimming whitespace. Changing it requires CLI users to sign in again.\n\n[code example]\n\nIn `values-production.yaml`, replace the ingress class and gRPC backend annotation placeholders with the values required by your maintained ingress controller. The controller must support gRPC backends and TLS termination.\n\n`auth.publicRestEndpoint` and `auth.publicGrpcEndpoint` show the public endpoints through authentication discovery. Use addresses reachable by your CLI users. If internal services reach the identity provider through a different URL, configure the internal and public OIDC URLs as described in [Configure authentication and role-based access control](https://redis.io/docs/latest/operate/featureform/configure-auth)."
    },
    {
      "id": "4-render-and-install-the-release",
      "title": "4. Render and install the release",
      "role": "content",
      "text": "Render the chart before changing the cluster:\n\n[code example]\n\nInstall the release and wait for its workloads to become ready:\n\n[code example]"
    },
    {
      "id": "5-verify-the-deployment",
      "title": "5. Verify the deployment",
      "role": "content",
      "text": "Confirm the server rollout, services, and PostgreSQL migration init container:\n\n[code example]\n\nConfirm that the `migrate` init container completed with exit code `0` for every server pod:\n\n[code example]\n\nEach line must end with `Completed` and `0`. If the status fields are empty, wait for the init container to finish and run the command again.\n\nIf migration fails, inspect its logs before restarting the pod:\n\n[code example]\n\nVerify the mounted license. Inspect the printed `Status:` value rather than relying only on the command's exit status:\n\n[code example]\n\nThen verify REST readiness through the public endpoint:\n\n[code example]\n\nThe readiness endpoint checks the server and registered scheduler dependencies. It doesn't prove that every external provider is healthy."
    },
    {
      "id": "6-install-and-connect-the-ff-cli",
      "title": "6. Install and connect the `ff` CLI",
      "role": "content",
      "text": "The Feature Form CLI ships as the `redis-featureform` package on PyPI. **Don't run `pip install featureform`** — that installs an unrelated upstream project. Install it in a virtual environment and pin it to the deployment version:\n\n[code example]\n\nWhen you expose the gRPC endpoint publicly, connect the CLI through that endpoint. gRPC is the CLI default, but the explicit transport makes the saved profile clear:\n\n[code example]\n\nIf you expose only REST, log in through that endpoint instead:\n\n[code example]\n\nIf neither endpoint is public, forward the gRPC service from one terminal:\n\n[code example]\n\nThen log in from another terminal:\n\n[code example]\n\nAfter login, verify the authenticated principal and the selected endpoint:\n\n[code example]\n\nLogin creates or updates the `production` profile and makes it active for subsequent commands. `ff ping` returns a failure status when the selected endpoint is unavailable."
    },
    {
      "id": "configure-production-state",
      "title": "Configure production state",
      "role": "configuration",
      "text": "Use external PostgreSQL so Feature Form state survives restarts and remains consistent across server replicas. Protect the database with your normal TLS, availability, monitoring, backup, and restore controls. Back up the database before upgrading Feature Form.\n\nThis database stores durable Feature Form application state. Register production data providers separately after deploying Feature Form.\n\nBefore starting the server, the chart runs an init container that applies all pending schema migrations. The database role must be able to create and alter the Feature Form schema. A migration failure keeps the server pod from becoming ready."
    },
    {
      "id": "configure-external-access",
      "title": "Configure external access",
      "role": "configuration",
      "text": "All services default to `ClusterIP`. Choose one exposure model:\n\n- Use `rest.ingress.*`, `grpc.ingress.*`, and `dashboard.ingress.*` for separate hosts.\n- Use `ingress.*` for one host with chart-managed paths for the API and dashboard.\n- Use the service `type=LoadBalancer` values when your platform terminates TLS at a load balancer.\n\nDon't combine `ingress.*` with service-specific ingress settings. The chart rejects that configuration.\n\nTerminate TLS at the ingress controller or load balancer. Use `grpc.ingress.*` only with a controller that supports gRPC backends. If ingress isn't suitable, use `grpc.service.type=LoadBalancer` and `rest.service.type=LoadBalancer`."
    },
    {
      "id": "dashboard-requirements",
      "title": "Dashboard requirements",
      "role": "content",
      "text": "Enabling the dashboard requires:\n\n- `dashboard.enabled=true`.\n- `dashboard.publicAPIURL`, a REST ingress host, or unified `ingress.*` configuration.\n- A resolvable dashboard authentication URL.\n- An OIDC client secret and dashboard session secret. Use `dashboard.auth.existingSecret` for production.\n\nThe dashboard Secret keys default to `FEATUREFORM_OIDC_CLIENT_SECRET` and `FEATUREFORM_DASHBOARD_AUTH_SECRET`."
    },
    {
      "id": "configure-availability-and-capacity",
      "title": "Configure availability and capacity",
      "role": "configuration",
      "text": "The chart defaults to one server replica. For multiple replicas, use PostgreSQL state. Configure resource requests before enabling horizontal autoscaling. Each server replica also runs scheduler workers, so adding replicas increases both API and job-processing capacity.\n\nThis example sets resource boundaries, two or more replicas, and a PodDisruptionBudget:\n\n[code example]\n\nHorizontal Pod Autoscaling requires the Kubernetes resource metrics API. Use `server.nodeSelector`, `server.tolerations`, `server.affinity`, or `server.topologySpreadConstraints` when your cluster requires workload placement controls.\n\nBy default, each server process starts two effective scheduler workers. To bound per-replica job concurrency, use `scheduler.workerCount` and `scheduler.workerMaxInflight`. Tune these values with server replica count and downstream provider capacity; increasing API replicas also increases the number of workers that can claim jobs."
    },
    {
      "id": "configure-kubernetes-secret-access",
      "title": "Configure Kubernetes Secret access",
      "role": "configuration",
      "text": "No token is needed to consume Secrets already referenced by pod environment variables or volumes. The chart doesn't mount a Kubernetes service-account token by default.\n\nIf you register a Kubernetes secret provider for server-side secret resolution, enable the token mount and grant the Feature Form service account `get` access to the required Secret objects. For Secrets in the release namespace:\n\n[code example]\n\n> [!NOTE]\n> These `rbac.*` values configure Kubernetes permissions. Feature Form application roles are configured separately in [Configure authentication and role-based access control](https://redis.io/docs/latest/operate/featureform/configure-auth)."
    },
    {
      "id": "configure-observability",
      "title": "Configure observability",
      "role": "configuration",
      "text": "The server exports metrics and traces through OpenTelemetry Protocol (OTLP). Because the Helm chart disables both signals by default, set the endpoint and enable the signals you want. Configure an external collector using an authority without a URL scheme:\n\n[code example]\n\nFeature Form writes server logs to standard output and standard error. Collect them with your Kubernetes logging system."
    },
    {
      "id": "upgrade-and-roll-back",
      "title": "Upgrade and roll back",
      "role": "content",
      "text": "Before upgrading:\n\n1. Back up the Feature Form PostgreSQL database.\n2. Confirm the license key is valid for the target release.\n3. Render the target chart version with the current values file.\n4. Upgrade with an explicit `--version`, `--wait`, and `--timeout`.\n5. Verify the migration init container, server rollout, license status, REST readiness, and `ff ping`.\n\nKeep the server, dashboard, chart, and `ff` versions aligned. Replacing an external license Secret doesn't restart the server; roll out the server deployment after updating the key.\n\nHelm rollback restores chart-managed resources, but it doesn't reverse PostgreSQL migrations or restore externally managed Secrets. Preserve the database backup, previous license key, image version, and Helm revision until the rollback window closes."
    },
    {
      "id": "troubleshoot-installation",
      "title": "Troubleshoot installation",
      "role": "errors",
      "text": "- **Helm reports missing authentication values.** Set `auth.enabled=true`, `auth.oidcIssuerURL`, `auth.oidcClientID`, and a stable `auth.deploymentID`.\n- **A pod shows `ImagePullBackOff`.** Confirm registry network access and configure `imagePullSecrets` when credentials are required.\n- **The migration init container fails.** Check PostgreSQL reachability, TLS settings, credentials, and the database role's schema permissions.\n- **The server reports a missing or invalid license.** Check the Secret name and `license.key` entry, then inspect `featureformctl license status` without printing the key.\n- **OIDC discovery or login fails.** Verify the internal and public issuer URLs, client IDs, redirect URIs, and endpoint reachability.\n- **The dashboard fails chart validation.** Supply its public API URL, authentication URL, OIDC client secret, and session secret.\n- **Ingress values conflict.** Configure either unified `ingress.*` or service-specific ingress settings, not both."
    },
    {
      "id": "next-steps",
      "title": "Next steps",
      "role": "content",
      "text": "- [Configure authentication and role-based access control](https://redis.io/docs/latest/operate/featureform/configure-auth) — set up OIDC and grant roles.\n- [Manage workspaces](https://redis.io/docs/latest/develop/ai/featureform/manage-workspace) — create your first workspace.\n- [Register providers](https://redis.io/docs/latest/develop/ai/featureform/register-providers) — connect production data infrastructure.\n- [Quickstart](https://redis.io/docs/latest/develop/ai/featureform/quickstart) — verify the install end to end."
    }
  ],
  "examples": [
    {
      "id": "1-get-the-chart-ex0",
      "language": "text",
      "code": "oci://registry-1.docker.io/redisfeatureform/featureform",
      "section_id": "1-get-the-chart"
    },
    {
      "id": "2-create-the-namespace-and-secrets-ex0",
      "language": "bash",
      "code": "kubectl create namespace <namespace>",
      "section_id": "2-create-the-namespace-and-secrets"
    },
    {
      "id": "2-create-the-namespace-and-secrets-ex1",
      "language": "bash",
      "code": "kubectl --namespace <namespace> create secret generic featureform-postgres \\\n  --from-env-file=<path-to-postgres-secret-env-file>",
      "section_id": "2-create-the-namespace-and-secrets"
    },
    {
      "id": "2-create-the-namespace-and-secrets-ex2",
      "language": "bash",
      "code": "kubectl --namespace <namespace> create secret generic featureform-license \\\n  --from-file=license.key=<path-to-license-key-file>",
      "section_id": "2-create-the-namespace-and-secrets"
    },
    {
      "id": "2-create-the-namespace-and-secrets-ex3",
      "language": "yaml",
      "code": "imagePullSecrets:\n  - name: <registry-secret-name>",
      "section_id": "2-create-the-namespace-and-secrets"
    },
    {
      "id": "3-create-a-values-file-ex0",
      "language": "yaml",
      "code": "stateBackend: postgres\n\nauth:\n  enabled: true\n  oidcIssuerURL: \"https://idp.example.com/realms/featureform\"\n  oidcClientID: \"featureform-api\"\n  oidcCLIClientID: \"featureform-cli\"\n  oidcCLIScopes: \"openid profile offline_access\"\n  oidcCLILoginMethods: \"device_code\"\n  deploymentID: \"<stable-deployment-id>\"\n  publicRestEndpoint: \"https://api.example.com\"\n  publicGrpcEndpoint: \"grpc.example.com:443\"\n\npostgres:\n  url: \"\"\n  secretName: featureform-postgres\n  secretKey: POSTGRES_URL\n\nlicense:\n  existingSecret: featureform-license\n  secretKey: license.key\n\nrest:\n  ingress:\n    enabled: true\n    className: \"<ingress-class-name>\"\n    hosts:\n      - host: api.example.com\n        paths:\n          - path: /\n            pathType: Prefix\n    tls:\n      - secretName: featureform-api-tls\n        hosts:\n          - api.example.com\n\ngrpc:\n  ingress:\n    enabled: true\n    className: \"<ingress-class-name>\"\n    annotations:\n      \"<grpc-backend-annotation>\": \"<grpc-backend-value>\"\n    hosts:\n      - host: grpc.example.com\n        paths:\n          - path: /\n            pathType: ImplementationSpecific\n    tls:\n      - secretName: featureform-grpc-tls\n        hosts:\n          - grpc.example.com",
      "section_id": "3-create-a-values-file"
    },
    {
      "id": "4-render-and-install-the-release-ex0",
      "language": "bash",
      "code": "helm template featureform \\\n  oci://registry-1.docker.io/redisfeatureform/featureform \\\n  --version <featureform-version> \\\n  --namespace <namespace> \\\n  --values values-production.yaml \\\n  > /tmp/featureform-rendered.yaml",
      "section_id": "4-render-and-install-the-release"
    },
    {
      "id": "4-render-and-install-the-release-ex1",
      "language": "bash",
      "code": "helm upgrade --install featureform \\\n  oci://registry-1.docker.io/redisfeatureform/featureform \\\n  --version <featureform-version> \\\n  --namespace <namespace> \\\n  --values values-production.yaml \\\n  --wait \\\n  --timeout 10m",
      "section_id": "4-render-and-install-the-release"
    },
    {
      "id": "5-verify-the-deployment-ex0",
      "language": "bash",
      "code": "kubectl --namespace <namespace> rollout status \\\n  deployment/featureform-featureform-server\nkubectl --namespace <namespace> get pods\nkubectl --namespace <namespace> get services",
      "section_id": "5-verify-the-deployment"
    },
    {
      "id": "5-verify-the-deployment-ex1",
      "language": "bash",
      "code": "kubectl --namespace <namespace> get pods \\\n  --selector app.kubernetes.io/instance=featureform,app.kubernetes.io/component=server \\\n  --output jsonpath='{range .items[*]}{.metadata.name}{\"\\t\"}{range .status.initContainerStatuses[?(@.name==\"migrate\")]}{.state.terminated.reason}{\"\\t\"}{.state.terminated.exitCode}{\"\\n\"}{end}{end}'",
      "section_id": "5-verify-the-deployment"
    },
    {
      "id": "5-verify-the-deployment-ex2",
      "language": "bash",
      "code": "kubectl --namespace <namespace> logs <server-pod> --container migrate",
      "section_id": "5-verify-the-deployment"
    },
    {
      "id": "5-verify-the-deployment-ex3",
      "language": "bash",
      "code": "kubectl --namespace <namespace> exec \\\n  deployment/featureform-featureform-server \\\n  -- featureformctl license status",
      "section_id": "5-verify-the-deployment"
    },
    {
      "id": "5-verify-the-deployment-ex4",
      "language": "bash",
      "code": "curl --fail --silent --show-error https://api.example.com/health/ready",
      "section_id": "5-verify-the-deployment"
    },
    {
      "id": "6-install-and-connect-the-ff-cli-ex0",
      "language": "bash",
      "code": "python3 -m venv .venv\nsource .venv/bin/activate\npip install redis-featureform==<featureform-version>\nff version --client-only",
      "section_id": "6-install-and-connect-the-ff-cli"
    },
    {
      "id": "6-install-and-connect-the-ff-cli-ex1",
      "language": "bash",
      "code": "ff --server grpc.example.com:443 --transport grpc \\\n  auth login --profile production",
      "section_id": "6-install-and-connect-the-ff-cli"
    },
    {
      "id": "6-install-and-connect-the-ff-cli-ex2",
      "language": "bash",
      "code": "ff --server https://api.example.com --transport rest \\\n  auth login --profile production",
      "section_id": "6-install-and-connect-the-ff-cli"
    },
    {
      "id": "6-install-and-connect-the-ff-cli-ex3",
      "language": "bash",
      "code": "kubectl --namespace <namespace> port-forward \\\n  service/featureform-featureform-grpc 9090:9090",
      "section_id": "6-install-and-connect-the-ff-cli"
    },
    {
      "id": "6-install-and-connect-the-ff-cli-ex4",
      "language": "bash",
      "code": "ff --server localhost:9090 --transport grpc \\\n  auth login --profile production",
      "section_id": "6-install-and-connect-the-ff-cli"
    },
    {
      "id": "6-install-and-connect-the-ff-cli-ex5",
      "language": "bash",
      "code": "ff auth status\nff auth whoami\nff ping",
      "section_id": "6-install-and-connect-the-ff-cli"
    },
    {
      "id": "configure-availability-and-capacity-ex0",
      "language": "yaml",
      "code": "server:\n  resources:\n    requests:\n      cpu: <server-cpu-request>\n      memory: <server-memory-request>\n    limits:\n      cpu: <server-cpu-limit>\n      memory: <server-memory-limit>\n  autoscaling:\n    enabled: true\n    minReplicas: 2\n    maxReplicas: <maximum-server-replicas>\n    targetCPUUtilizationPercentage: 80\n\npodDisruptionBudget:\n  enabled: true\n  minAvailable: 1",
      "section_id": "configure-availability-and-capacity"
    },
    {
      "id": "configure-kubernetes-secret-access-ex0",
      "language": "yaml",
      "code": "serviceAccount:\n  automountServiceAccountToken: true\n\nrbac:\n  create: true\n  rules:\n    - apiGroups: [\"\"]\n      resources: [\"secrets\"]\n      resourceNames: [\"<provider-secret-name>\"]\n      verbs: [\"get\"]",
      "section_id": "configure-kubernetes-secret-access"
    },
    {
      "id": "configure-observability-ex0",
      "language": "yaml",
      "code": "observability:\n  otlpEndpoint: \"otel-collector.telemetry.svc.cluster.local:4317\"\n  tracingEnabled: true\n  metricsEnabled: true\n  serviceName: featureform\n  serviceVersion: \"<featureform-version>\"\n  environment: production\n  logLevel: info\n  traceSampleRate: 0.1",
      "section_id": "configure-observability"
    }
  ]
}
