Disclaimer: This Helm chart is maintained by Hack The Box and is not officially affiliated with or endorsed by the Typesense project. It is provided on a best-effort basis with no guarantees. Use at your own risk.
A production-ready Helm chart for deploying Typesense, an open-source, typo-tolerant search engine built for instant search. This chart deploys Typesense as a StatefulSet with Raft-based clustering for high availability.
- High Availability -- Raft-based clustering with odd-replica enforcement (3, 5, 7, ...)
- Security Hardened -- Non-root container (UID 10000), read-only root filesystem, all capabilities dropped
- Gateway API & Ingress -- Supports both Kubernetes Ingress and Gateway API (HTTPRoute)
- Prometheus Metrics -- Optional sidecar exporter with ServiceMonitor support
- External Secrets -- Optional integration with the External Secrets Operator
- PodDisruptionBudget -- Protect Raft quorum during node maintenance
- Comprehensive Tuning -- CORS, analytics, cache, thread pools, logging, health thresholds
helm install typesense oci://ghcr.io/hackthebox/helm/typesense \
--namespace typesense --create-namespace \
--set secrets.optional=false- Kubernetes >=1.26.0-0
- Helm 3.x
- A pre-created Kubernetes Secret containing at minimum
TYPESENSE_API_KEY
helm install typesense oci://ghcr.io/hackthebox/helm/typesense \
--namespace typesense --create-namespace \
--values my-values.yamlCreate a secret with your Typesense API key:
kubectl create namespace typesense
kubectl create secret generic typesense-secret \
--namespace typesense \
--from-literal=TYPESENSE_API_KEY=$(openssl rand -hex 32)Install with defaults (3-replica HA cluster):
helm install typesense oci://ghcr.io/hackthebox/helm/typesense \
--namespace typesense \
--set secrets.optional=falsereplicaCount: 5
resources:
requests:
cpu: 500m
memory: 2Gi
storage:
className: gp3
size: 50Gi
pdb:
enabled: true
maxUnavailable: 1
gateway:
enabled: true
hosts:
- search.example.com
parentRefs:
default:
name: my-gateway
namespace: istio-system
metrics:
enabled: true
serviceMonitor:
enabled: true
secrets:
optional: false
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app.kubernetes.io/name: typesensehelm uninstall typesense --namespace typesenseNote: PersistentVolumeClaims created by the StatefulSet are not deleted automatically. To remove all data:
kubectl delete pvc -l app.kubernetes.io/name=typesense -n typesense
Typesense requires at minimum a TYPESENSE_API_KEY environment variable. You have two options:
Option A: Pre-created Secret (default)
Create a Kubernetes Secret and the chart will mount it via envFrom:
kubectl create secret generic typesense-secret \
--namespace typesense \
--from-literal=TYPESENSE_API_KEY=your-api-key-hereOption B: External Secrets Operator
If you use the External Secrets Operator, enable the ExternalSecret resource:
secrets:
externalSecret:
enabled: true
storeName: my-secret-store
extractKey: path/to/typesense/secretThe chart supports both traditional Ingress and the newer Gateway API:
Ingress:
ingress:
enabled: true
className: nginx
hosts:
- search.example.com
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prodGateway API (HTTPRoute):
gateway:
enabled: true
hosts:
- search.example.com
parentRefs:
default:
name: my-gateway
namespace: istio-systemWarning: Enabling both
ingressandgatewaysimultaneously will create duplicate routing resources. Use one or the other unless you have a specific reason to use both.
Enable the Prometheus metrics sidecar and optionally create a ServiceMonitor:
metrics:
enabled: true
serviceMonitor:
enabled: true
labels:
release: prometheusThe exporter serves metrics on port 8888 using typesense-prometheus-exporter.
Typesense uses the Raft consensus protocol. Key considerations:
replicaCountmust be odd (1, 3, 5, 7, ...) for quorum. The chart rejects even values > 1.- A 3-node cluster tolerates 1 failure. A 5-node cluster tolerates 2 failures.
- Enable
pdb.enabled=trueto protect quorum during planned node maintenance. - Nodes discover each other via a headless Service and DNS.
Data is stored on PersistentVolumeClaims attached to each StatefulSet pod:
storage:
className: gp3 # omit to use the cluster default StorageClass
size: 50GiWarning: Reducing
storage.sizeafter initial deployment has no effect. PVC resize depends on your StorageClass supporting volume expansion.
| Key | Type | Default | Description |
|---|---|---|---|
| affinity | object | {} |
Affinity rules for pod scheduling. When unset or empty and replicaCount > 1, a soft pod anti-affinity on kubernetes.io/hostname is automatically applied. Set to a non-empty affinity object to override this default behavior. |
| extraArgs | list | [] |
Extra command-line arguments for Typesense server (e.g., ["--filter-by-max-ops=200"]) |
| extraEnv | list | [] |
Extra environment variables for the Typesense container |
| fullnameOverride | string | "" |
Override the full name of the release (optional) |
| gateway.enabled | bool | false |
Enable or disable Gateway API for the application |
| gateway.hosts | list | [] |
List of hostnames the Gateway will route traffic for |
| gateway.parentRefs | object | {"default":{"name":"","namespace":"istio-system"},"extras":[]} |
Configuration controlling which Gateway(s) this HTTPRoute attaches to. |
| gateway.prefix | string | "/" |
The URL path prefix for the application |
| image.pullPolicy | string | "IfNotPresent" |
Image pull policy (Always, IfNotPresent, etc.) |
| image.repository | string | "typesense/typesense" |
Docker image repository for Typesense |
| image.tag | string | "" |
Overrides the image tag. If left empty, it uses the appVersion from Chart.yaml |
| ingress.annotations | object | {} |
Extra annotations to add to the Ingress resource |
| ingress.className | string | "nginx" |
The name of the Ingress class to use (e.g., nginx) |
| ingress.enabled | bool | false |
Enable or disable Ingress for the application |
| ingress.hosts | list | [] |
List of hostnames the Ingress will route traffic for |
| ingress.prefix | string | "/" |
The URL path prefix for the application |
| livenessProbe.failureThreshold | int | 2 |
Number of failed liveness checks before restarting the container |
| livenessProbe.httpGet | object | {"path":"/health","port":"http"} |
HTTP GET path and port to check Typesense health for liveness |
| livenessProbe.periodSeconds | int | 10 |
Period (in seconds) to perform the liveness check |
| metrics.enabled | bool | false |
Enable Prometheus metrics sidecar |
| metrics.image.pullPolicy | string | "IfNotPresent" |
Image pull policy for metrics exporter |
| metrics.image.repository | string | "imatefx/typesense-prometheus-exporter" |
Metrics exporter image repository |
| metrics.image.tag | string | "v0.1.5" |
Metrics exporter image tag |
| metrics.port | int | 8888 |
Port for Prometheus metrics endpoint |
| metrics.resources | object | {} |
Resource requests and limits for the metrics sidecar |
| metrics.serviceMonitor.enabled | bool | false |
Enable ServiceMonitor for Prometheus scraping |
| metrics.serviceMonitor.interval | string | "30s" |
Scrape interval |
| metrics.serviceMonitor.labels | object | {} |
Additional labels for ServiceMonitor |
| nameOverride | string | "" |
Override the name of the release (optional) |
| nodeSelector | object | {} |
Node selector to schedule pods on specific nodes (optional) |
| pdb.enabled | bool | true |
Enable PodDisruptionBudget for Typesense StatefulSet. Automatically skipped when replicaCount is 1. |
| pdb.maxUnavailable | string/int | "auto" |
Maximum number of pods that can be unavailable during disruption. Set to "auto" (default) to auto-calculate as floor(replicaCount/2), preserving Raft quorum. Set to 0 to block all voluntary disruptions. Any positive value is used directly and must not exceed floor(replicaCount/2). |
| podAnnotations | object | {} |
Additional annotations to add to the Typesense pod(s) |
| podLabels | object | {} |
Additional labels to add to the Typesense pod(s) |
| podSecurityContext.fsGroup | int | 2000 |
Group ID for the filesystem of the Typesense container |
| podSecurityContext.runAsGroup | int | 3000 |
Group ID for running the Typesense process |
| podSecurityContext.runAsNonRoot | bool | true |
Ensure the container does not run as root |
| podSecurityContext.runAsUser | int | 10000 |
User ID for running the Typesense process |
| readinessProbe.failureThreshold | int | 3 |
Number of failed readiness checks before marking the pod as unready |
| readinessProbe.httpGet | object | {"path":"/health","port":"http"} |
HTTP GET path and port to check Typesense readiness |
| readinessProbe.periodSeconds | int | 5 |
Period (in seconds) to perform the readiness check |
| replicaCount | int | 3 |
Number of replicas for the Typesense deployment |
| resources | object | {} |
Resource requests and limits for the Typesense container |
| secrets.externalSecret.enabled | bool | false |
Enable or disable ExternalSecret creation (requires external-secrets operator) |
| secrets.externalSecret.extractKey | string | "" |
The key path to extract secrets from |
| secrets.externalSecret.storeName | string | "" |
The name of the ClusterSecretStore or SecretStore to use |
| secrets.optional | bool | true |
Whether the secretRef in envFrom is optional (pods start even if secret is missing) |
| secrets.secretName | string | "" |
Name of the Kubernetes Secret to mount via envFrom. Defaults to -secret |
| securityContext.allowPrivilegeEscalation | bool | false |
Prevent privilege escalation |
| securityContext.capabilities | object | {"drop":["ALL"]} |
Drop all Linux capabilities |
| securityContext.readOnlyRootFilesystem | bool | true |
Read-only root filesystem (Typesense only writes to data PVC) |
| service.port | int | 8108 |
Port that the Typesense service will listen on |
| service.type | string | "ClusterIP" |
Kubernetes service type (ClusterIP, NodePort, LoadBalancer) |
| serviceAccount.annotations | object | {} |
Annotations to add to the ServiceAccount (e.g., for IRSA) |
| serviceAccount.automountServiceAccountToken | bool | false |
Whether to automount the ServiceAccount token |
| serviceAccount.create | bool | true |
Whether to create a ServiceAccount |
| serviceAccount.name | string | "" |
Name of the ServiceAccount. Defaults to fullname |
| startupProbe.failureThreshold | int | 10 |
Number of failed startup checks before marking the container as unhealthy |
| startupProbe.httpGet | object | {"path":"/health","port":"http"} |
HTTP GET path and port to check Typesense health for startup |
| startupProbe.periodSeconds | int | 10 |
Period (in seconds) to perform the startup check |
| storage.className | string | nil |
Storage class to use for Persistent Volume Claims (PVC) |
| storage.size | string | "10Gi" |
Size of the persistent storage volume (e.g., 10Gi) |
| terminationGracePeriodSeconds | int | 300 |
Termination grace period in seconds. Typesense recommends 300s to allow graceful shutdown. |
| tolerations | list | [] |
Tolerations for pod scheduling |
| topologySpreadConstraints | list | [] |
Topology spread constraints for pod distribution across nodes/zones |
| typesense.analytics.enabled | bool | false |
Enable aggregated search query analytics |
| typesense.analytics.flushInterval | string | nil |
How often analytics are persisted to disk in seconds. Unset uses Typesense default (3600) |
| typesense.cache.numEntries | string | nil |
LRU cache size for search responses. Unset uses Typesense default (1000) |
| typesense.cors.domains | list | [] |
List of allowed origins (no trailing slashes) |
| typesense.cors.enabled | bool | false |
Enable CORS for browser/JS access |
| typesense.health.readLag | string | nil |
Update lag threshold before rejecting reads. Unset uses Typesense default (1000) |
| typesense.health.writeLag | string | nil |
Update lag threshold before rejecting writes. Unset uses Typesense default (500) |
| typesense.limits.diskUsedMaxPercentage | string | nil |
Reject writes above this disk usage percentage. Unset uses Typesense default (100) |
| typesense.limits.memoryUsedMaxPercentage | string | nil |
Reject writes above this memory usage percentage. Unset uses Typesense default (100) |
| typesense.logging.enableSearchLogging | bool | false |
Log search request payloads |
| typesense.logging.slowRequestsTimeMs | string | nil |
Threshold in ms for slow request logging (-1 disables). Unset uses Typesense default (-1) |
| typesense.snapshots.intervalSeconds | string | nil |
Replication log snapshot frequency in seconds. Unset uses Typesense default (3600) |
| typesense.threadPoolSize | string | nil |
Concurrent request handler threads. Unset uses Typesense default (NUM_CORES * 8) |
| updateStrategy | object | {"rollingUpdate":{"maxUnavailable":"auto"},"type":"RollingUpdate"} |
StatefulSet update strategy configuration. When type is RollingUpdate, rollingUpdate.maxUnavailable is required. When type is OnDelete, rollingUpdate is optional and ignored. |
| updateStrategy.rollingUpdate.maxUnavailable | string/int | "auto" |
Maximum number of pods that can be unavailable during a rolling update. Set to "auto" (default) to auto-calculate as floor(replicaCount/2), preserving Raft quorum. Set to 0 to block all voluntary pod replacements. Any positive value is used directly and must not exceed floor(replicaCount/2). Ignored when updateStrategy.type is OnDelete. |
| updateStrategy.type | string | "RollingUpdate" |
StatefulSet update strategy type. Use RollingUpdate (default) for zero-downtime upgrades or OnDelete for manual pod-by-pod control. |
Initial release. No upgrade considerations.
See RESTORE_RUNBOOK.md for step-by-step instructions on restoring a Typesense cluster from a restic backup snapshot.
Contributions are welcome. Please open an issue or pull request on GitHub.
# Install tools
mise install
# Install Helm plugins
mise run setup
# Run tests
mise run test
# Lint
mise run lint
# Generate docs
mise run docsSee ACKNOWLEDGMENTS.md for a list of third-party projects this chart depends on.
This chart is licensed under the MIT License.
| Name | Url | |
|---|---|---|
| Hack The Box SRE | oss@hackthebox.com | https://github.com/hackthebox |