docs/how-to-guides/feast-operator/02-persistence.md
The offline store, online store, and registry each need a place to store data. The operator supports two persistence patterns for each:
The pattern is the same for all three services; only the store types differ.
| Pattern | Best for | Data survives pod restart? |
|---|---|---|
| File — emptyDir | Dev / CI | No |
| File — PVC (ref) | Single-node prod or testing | Yes (if PVC is retained) |
| File — PVC (create) | Operator-managed storage | Yes |
| DB store | Production, HA, multi-pod | Yes |
The default when no persistence block is set. Data lives on the pod's local disk and is
lost on restart. Suitable for development only.
services:
onlineStore:
server: {} # no persistence block → emptyDir
When you already have a PVC provisioned (e.g. by your storage team):
services:
onlineStore:
persistence:
file:
path: online_store.db # filename relative to mountPath
pvc:
ref:
name: online-pvc # name of the existing PVC
mountPath: /data/online # where the PVC is mounted in the pod
services:
offlineStore:
persistence:
file:
type: duckdb # offline file type: file | dask | duckdb
pvc:
create:
storageClassName: standard
resources:
requests:
storage: 20Gi
mountPath: /data/offline
Omitting storageClassName uses the cluster default StorageClass. Omitting create
entirely creates a PVC with the operator's built-in defaults (1 Gi, default StorageClass).
registry:
local:
persistence:
file:
path: registry.db
pvc:
create: {} # operator defaults: 1 Gi, default StorageClass
mountPath: /data/registry
For production, point the operator at an external database. The operator reads connection
details from a Kubernetes Secret and writes them into feature_store.yaml.
The Secret must contain one key per store component. The key name is the store type
(e.g. postgres, sql, redis). The value is a YAML snippet identical to what you
would write under the corresponding section in feature_store.yaml, minus the type: key
(the operator inserts it from persistence.store.type).
apiVersion: v1
kind: Secret
metadata:
name: feast-data-stores
stringData:
# Key for online store type "postgres"
postgres: |
host: postgres.feast.svc.cluster.local
port: 5432
database: feast
db_schema: public
user: feast
password: feast
# Key for registry type "sql" (SQLAlchemy URL)
sql: |
path: postgresql+psycopg://feast:[email protected]:5432/feast #pragma: allowlist secret
cache_ttl_seconds: 60
sqlalchemy_config_kwargs:
echo: false
pool_pre_ping: true
Reference the Secret from the CR:
services:
onlineStore:
persistence:
store:
type: postgres
secretRef:
name: feast-data-stores # key "postgres" is read automatically
registry:
local:
persistence:
store:
type: sql
secretRef:
name: feast-data-stores # key "sql" is read automatically
Key lookup rule: the operator looks up the Secret key that matches
persistence.store.type(e.g.type: postgres→ keypostgres). Keep all stores in one Secret or split across multiple — both work.
The Secret key values in the example above hard-code passwords. For production, keep
credentials in a separate Secret and inject them as environment variables using envFrom,
then reference them with ${VAR} substitution in the data-stores Secret value:
apiVersion: v1
kind: Secret
metadata:
name: postgres-creds
stringData:
POSTGRES_USER: feast
POSTGRES_PASSWORD: s3cr3t
---
apiVersion: v1
kind: Secret
metadata:
name: feast-data-stores
stringData:
postgres: |
host: postgres.feast.svc.cluster.local
port: 5432
database: feast
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
services:
onlineStore:
server:
envFrom:
- secretRef:
name: postgres-creds # expands ${POSTGRES_USER} / ${POSTGRES_PASSWORD}
persistence:
store:
type: postgres
secretRef:
name: feast-data-stores
type | Secret key | SDK docs |
|---|---|---|
sqlite | sqlite | SQLite |
redis | redis | Redis |
postgres | postgres | Postgres |
cassandra | cassandra | Cassandra |
hazelcast | hazelcast | Hazelcast |
hbase | hbase | HBase |
datastore | datastore | Datastore |
dynamodb | dynamodb | DynamoDB |
bigtable | bigtable | Bigtable |
For all store-specific YAML keys (connection options, pool sizes, etc.) see the linked SDK docs — the Secret value accepts the same keys.
type (file) | Notes |
|---|---|
file | Default pandas-based parquet offline store |
dask | Dask-based parallel parquet |
duckdb | DuckDB in-process analytical engine |
For external DB-backed offline stores (BigQuery, Snowflake, Spark, Trino, etc.), use
persistence.store.type and a Secret with the matching key. See
Offline Stores in the SDK docs.
type | Secret key | Notes |
|---|---|---|
file | (file persistence, no Secret) | SQLite-backed file registry |
sql | sql | SQLAlchemy URL — supports PostgreSQL, MySQL, SQLite |
snowflake.registry | snowflake.registry | Snowflake-backed registry |
For sql, the Secret value is a path: (SQLAlchemy URL) plus optional cache_ttl_seconds
and sqlalchemy_config_kwargs:
# feast-data-stores Secret, key "sql"
sql: |
path: postgresql+psycopg://user:pass@host:5432/feast #pragma: allowlist secret
cache_ttl_seconds: 60
apiVersion: v1
kind: Secret
metadata:
name: feast-data-stores
stringData:
redis: |
connection_string: redis.feast.svc.cluster.local:6379
---
apiVersion: feast.dev/v1
kind: FeatureStore
metadata:
name: sample-redis
spec:
feastProject: my_project
services:
onlineStore:
server: {}
persistence:
store:
type: redis
secretRef:
name: feast-data-stores
services:
onlineStore:
persistence:
store:
type: postgres
secretRef:
name: feast-data-stores # reads key "postgres"
offlineStore:
persistence:
file:
type: duckdb
pvc:
create: {}
mountPath: /data/offline
registry:
local:
persistence:
store:
type: sql
secretRef:
name: feast-data-stores # reads key "sql"