# Deploy

```json metadata
{
  "schema_version": 2,
  "title": "Deploy",
  "description": "Deploy and operate Feature Form on Kubernetes.",
  "categories": ["docs","operate","featureform"],
  "tableOfContents": {"sections":[{"children":[{"id":"prerequisites","title":"Prerequisites"},{"id":"1-get-the-chart","title":"1. Get the chart"},{"id":"2-create-the-namespace-and-secrets","title":"2. Create the namespace and Secrets"},{"id":"3-create-a-values-file","title":"3. Create a values file"},{"id":"4-render-and-install-the-release","title":"4. Render and install the release"},{"id":"5-verify-the-deployment","title":"5. Verify the deployment"},{"id":"6-install-and-connect-the-ff-cli","title":"6. Install and connect the ff CLI"}],"id":"install","title":"Install"},{"id":"configure-production-state","title":"Configure production state"},{"children":[{"id":"dashboard-requirements","title":"Dashboard requirements"}],"id":"configure-external-access","title":"Configure external access"},{"id":"configure-availability-and-capacity","title":"Configure availability and capacity"},{"id":"configure-kubernetes-secret-access","title":"Configure Kubernetes Secret access"},{"id":"configure-observability","title":"Configure observability"},{"id":"upgrade-and-roll-back","title":"Upgrade and roll back"},{"id":"troubleshoot-installation","title":"Troubleshoot installation"},{"id":"next-steps","title":"Next steps"}]}

,
  "codeExamples": []
}
```


Install Redis Feature Form on Kubernetes with the Helm chart, durable PostgreSQL state, OpenID Connect (OIDC) authentication, and a license key.

## Install

Prerequisites: For production environments, use an external PostgreSQL database and keep credentials and the license key in Kubernetes Secrets.

To get started:

1. Get the chart.
2. Create the namespace and Secrets.
3. Create a values file.
4. Render and install the release.
5. Verify the deployment.
6. Install and connect the `ff` command-line interface (CLI).

### Prerequisites

- Kubernetes 1.27+.
- Helm 3.14+.
- Network access to the chart and image repositories. Configure `imagePullSecrets` if your cluster requires registry credentials.
- 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).
- An external PostgreSQL database for production state. Its role must be able to create and alter Feature Form tables during migrations.
- A Feature Form license key from your Redis account team.
- A public domain name and Transport Layer Security (TLS) certificate for each externally exposed endpoint.

### 1. Get the chart

The Feature Form Helm chart is published as an Open Container Initiative (OCI) artifact on Docker Hub:

```text
oci://registry-1.docker.io/redisfeatureform/featureform
```

Install the chart directly from this path. Always pin `--version` to the Feature Form version you intend to run.

### 2. Create the namespace and Secrets

Create the release namespace before creating referenced Secrets:

```bash
kubectl create namespace <namespace>
```

Use your normal secret-management process to create these Secrets in the same namespace as Feature Form:

- `featureform-postgres`, with a `POSTGRES_URL` key containing the PostgreSQL connection URL. Require TLS according to your database policy.
- `featureform-license`, with the license key stored in `license.key`.

The 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`.

For example, create the PostgreSQL Secret from a restricted environment file whose entry is `POSTGRES_URL=<postgres-connection-url>`:

```bash
kubectl --namespace <namespace> create secret generic featureform-postgres \
  --from-env-file=<path-to-postgres-secret-env-file>
```

Create the license Secret from a restricted key file:

```bash
kubectl --namespace <namespace> create secret generic featureform-license \
  --from-file=license.key=<path-to-license-key-file>
```

Create an image-pull Secret as well if your cluster requires registry authentication.

Add this entry when you create `values-production.yaml`:

```yaml
imagePullSecrets:
  - name: <registry-secret-name>
```

### 3. Create a values file

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.

Feature Form accepts any value that isn't empty after trimming whitespace. Changing it requires CLI users to sign in again.

```yaml
stateBackend: postgres

auth:
  enabled: true
  oidcIssuerURL: "https://idp.example.com/realms/featureform"
  oidcClientID: "featureform-api"
  oidcCLIClientID: "featureform-cli"
  oidcCLIScopes: "openid profile offline_access"
  oidcCLILoginMethods: "device_code"
  deploymentID: "<stable-deployment-id>"
  publicRestEndpoint: "https://api.example.com"
  publicGrpcEndpoint: "grpc.example.com:443"

postgres:
  url: ""
  secretName: featureform-postgres
  secretKey: POSTGRES_URL

license:
  existingSecret: featureform-license
  secretKey: license.key

rest:
  ingress:
    enabled: true
    className: "<ingress-class-name>"
    hosts:
      - host: api.example.com
        paths:
          - path: /
            pathType: Prefix
    tls:
      - secretName: featureform-api-tls
        hosts:
          - api.example.com

grpc:
  ingress:
    enabled: true
    className: "<ingress-class-name>"
    annotations:
      "<grpc-backend-annotation>": "<grpc-backend-value>"
    hosts:
      - host: grpc.example.com
        paths:
          - path: /
            pathType: ImplementationSpecific
    tls:
      - secretName: featureform-grpc-tls
        hosts:
          - grpc.example.com
```

In `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.

`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).

### 4. Render and install the release

Render the chart before changing the cluster:

```bash
helm template featureform \
  oci://registry-1.docker.io/redisfeatureform/featureform \
  --version <featureform-version> \
  --namespace <namespace> \
  --values values-production.yaml \
  > /tmp/featureform-rendered.yaml
```

Install the release and wait for its workloads to become ready:

```bash
helm upgrade --install featureform \
  oci://registry-1.docker.io/redisfeatureform/featureform \
  --version <featureform-version> \
  --namespace <namespace> \
  --values values-production.yaml \
  --wait \
  --timeout 10m
```

### 5. Verify the deployment

Confirm the server rollout, services, and PostgreSQL migration init container:

```bash
kubectl --namespace <namespace> rollout status \
  deployment/featureform-featureform-server
kubectl --namespace <namespace> get pods
kubectl --namespace <namespace> get services
```

Confirm that the `migrate` init container completed with exit code `0` for every server pod:

```bash
kubectl --namespace <namespace> get pods \
  --selector app.kubernetes.io/instance=featureform,app.kubernetes.io/component=server \
  --output jsonpath='{range .items[*]}{.metadata.name}{"\t"}{range .status.initContainerStatuses[?(@.name=="migrate")]}{.state.terminated.reason}{"\t"}{.state.terminated.exitCode}{"\n"}{end}{end}'
```

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

If migration fails, inspect its logs before restarting the pod:

```bash
kubectl --namespace <namespace> logs <server-pod> --container migrate
```

Verify the mounted license. Inspect the printed `Status:` value rather than relying only on the command's exit status:

```bash
kubectl --namespace <namespace> exec \
  deployment/featureform-featureform-server \
  -- featureformctl license status
```

Then verify REST readiness through the public endpoint:

```bash
curl --fail --silent --show-error https://api.example.com/health/ready
```

The readiness endpoint checks the server and registered scheduler dependencies. It doesn't prove that every external provider is healthy.

### 6. Install and connect the `ff` CLI

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:

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install redis-featureform==<featureform-version>
ff version --client-only
```

When 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:

```bash
ff --server grpc.example.com:443 --transport grpc \
  auth login --profile production
```

If you expose only REST, log in through that endpoint instead:

```bash
ff --server https://api.example.com --transport rest \
  auth login --profile production
```

If neither endpoint is public, forward the gRPC service from one terminal:

```bash
kubectl --namespace <namespace> port-forward \
  service/featureform-featureform-grpc 9090:9090
```

Then log in from another terminal:

```bash
ff --server localhost:9090 --transport grpc \
  auth login --profile production
```

After login, verify the authenticated principal and the selected endpoint:

```bash
ff auth status
ff auth whoami
ff ping
```

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

## Configure production state

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.

This database stores durable Feature Form application state. Register production data providers separately after deploying Feature Form.

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

## Configure external access

All services default to `ClusterIP`. Choose one exposure model:

- Use `rest.ingress.*`, `grpc.ingress.*`, and `dashboard.ingress.*` for separate hosts.
- Use `ingress.*` for one host with chart-managed paths for the API and dashboard.
- Use the service `type=LoadBalancer` values when your platform terminates TLS at a load balancer.

Don't combine `ingress.*` with service-specific ingress settings. The chart rejects that configuration.

Terminate 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`.

### Dashboard requirements

Enabling the dashboard requires:

- `dashboard.enabled=true`.
- `dashboard.publicAPIURL`, a REST ingress host, or unified `ingress.*` configuration.
- A resolvable dashboard authentication URL.
- An OIDC client secret and dashboard session secret. Use `dashboard.auth.existingSecret` for production.

The dashboard Secret keys default to `FEATUREFORM_OIDC_CLIENT_SECRET` and `FEATUREFORM_DASHBOARD_AUTH_SECRET`.

## Configure availability and capacity

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.

This example sets resource boundaries, two or more replicas, and a PodDisruptionBudget:

```yaml
server:
  resources:
    requests:
      cpu: <server-cpu-request>
      memory: <server-memory-request>
    limits:
      cpu: <server-cpu-limit>
      memory: <server-memory-limit>
  autoscaling:
    enabled: true
    minReplicas: 2
    maxReplicas: <maximum-server-replicas>
    targetCPUUtilizationPercentage: 80

podDisruptionBudget:
  enabled: true
  minAvailable: 1
```

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

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

## Configure Kubernetes Secret access

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.

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

```yaml
serviceAccount:
  automountServiceAccountToken: true

rbac:
  create: true
  rules:
    - apiGroups: [""]
      resources: ["secrets"]
      resourceNames: ["<provider-secret-name>"]
      verbs: ["get"]
```

> [!NOTE]
> 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).

## Configure observability

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:

```yaml
observability:
  otlpEndpoint: "otel-collector.telemetry.svc.cluster.local:4317"
  tracingEnabled: true
  metricsEnabled: true
  serviceName: featureform
  serviceVersion: "<featureform-version>"
  environment: production
  logLevel: info
  traceSampleRate: 0.1
```

Feature Form writes server logs to standard output and standard error. Collect them with your Kubernetes logging system.

## Upgrade and roll back

Before upgrading:

1. Back up the Feature Form PostgreSQL database.
2. Confirm the license key is valid for the target release.
3. Render the target chart version with the current values file.
4. Upgrade with an explicit `--version`, `--wait`, and `--timeout`.
5. Verify the migration init container, server rollout, license status, REST readiness, and `ff ping`.

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

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

## Troubleshoot installation

- **Helm reports missing authentication values.** Set `auth.enabled=true`, `auth.oidcIssuerURL`, `auth.oidcClientID`, and a stable `auth.deploymentID`.
- **A pod shows `ImagePullBackOff`.** Confirm registry network access and configure `imagePullSecrets` when credentials are required.
- **The migration init container fails.** Check PostgreSQL reachability, TLS settings, credentials, and the database role's schema permissions.
- **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.
- **OIDC discovery or login fails.** Verify the internal and public issuer URLs, client IDs, redirect URIs, and endpoint reachability.
- **The dashboard fails chart validation.** Supply its public API URL, authentication URL, OIDC client secret, and session secret.
- **Ingress values conflict.** Configure either unified `ingress.*` or service-specific ingress settings, not both.

## Next steps

- [Configure authentication and role-based access control](https://redis.io/docs/latest/operate/featureform/configure-auth) — set up OIDC and grant roles.
- [Manage workspaces](https://redis.io/docs/latest/develop/ai/featureform/manage-workspace) — create your first workspace.
- [Register providers](https://redis.io/docs/latest/develop/ai/featureform/register-providers) — connect production data infrastructure.
- [Quickstart](https://redis.io/docs/latest/develop/ai/featureform/quickstart) — verify the install end to end.

