From be0c664c6eb15af992d7c57b371d4d77916ab7a3 Mon Sep 17 00:00:00 2001 From: Dimosthenis Schizas Date: Thu, 19 Mar 2026 15:39:15 +0200 Subject: [PATCH 01/13] chore(typesense): bump to v1.1.0 --- Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Chart.yaml b/Chart.yaml index e553b69..06295be 100644 --- a/Chart.yaml +++ b/Chart.yaml @@ -5,7 +5,7 @@ description: >- Prometheus metrics, and Gateway API support. This chart is not officially maintained by or affiliated with the Typesense project. type: application -version: 1.0.0 +version: 1.1.0 appVersion: "30.1" icon: https://typesense.org/typesense-logo.svg home: https://github.com/hackthebox/typesense-helm From 5c58662c60f840f7458b430ade284f4e31b669f2 Mon Sep 17 00:00:00 2001 From: Dimosthenis Schizas Date: Thu, 19 Mar 2026 15:39:38 +0200 Subject: [PATCH 02/13] feat(typesense): add backup values block --- values.yaml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/values.yaml b/values.yaml index 2e5e7e0..c59a753 100644 --- a/values.yaml +++ b/values.yaml @@ -318,3 +318,35 @@ metrics: interval: 30s # -- Additional labels for ServiceMonitor labels: {} + +backup: + # @schema + # type: boolean + # @schema + # -- Enable the backup sidecar. Requires backup.image and restic env vars to be configured. + enabled: false + # -- Cron schedule for backups (cron format) + schedule: "0 2 * * *" + image: + # -- Backup sidecar image. Defaults to the official restic image which includes wget and crond via BusyBox. + repository: ghcr.io/restic/restic + # -- Image tag + tag: "0.18.1" + # -- Image pull policy + pullPolicy: IfNotPresent + retention: + # @schema + # type: integer + # minimum: 1 + # @schema + # -- Number of most recent restic snapshots to keep + keepLast: 7 + # -- Extra env vars for the backup sidecar (e.g. RESTIC_REPOSITORY, AWS_ACCESS_KEY_ID) + env: [] + # -- References to existing Secrets/ConfigMaps to inject into the backup sidecar + envFrom: [] + # -- Resource requests and limits for the backup sidecar + resources: + requests: + cpu: 10m + memory: 32Mi From 7ad9c1346bfb1d56f9e52ce7d541f219ba121298 Mon Sep 17 00:00:00 2001 From: Dimosthenis Schizas Date: Thu, 19 Mar 2026 15:40:40 +0200 Subject: [PATCH 03/13] feat(typesense): add backup scripts configmap --- templates/backup-scripts-configmap.yaml | 68 +++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 templates/backup-scripts-configmap.yaml diff --git a/templates/backup-scripts-configmap.yaml b/templates/backup-scripts-configmap.yaml new file mode 100644 index 0000000..329e2a8 --- /dev/null +++ b/templates/backup-scripts-configmap.yaml @@ -0,0 +1,68 @@ +{{- if .Values.backup.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "typesense.fullname" . }}-backup-scripts + namespace: {{ .Release.Namespace }} + labels: + {{- include "typesense.labels" . | nindent 4 }} +data: + entrypoint.sh: | + #!/bin/sh + set -e + mkdir -p /var/spool/cron/crontabs + echo "${BACKUP_SCHEDULE:-0 2 * * *} /scripts/backup.sh >> /proc/1/fd/1 2>&1" > /var/spool/cron/crontabs/root + echo "[backup-sidecar] Starting crond with schedule: ${BACKUP_SCHEDULE:-0 2 * * *}" + crond -f -L /dev/stdout & + CROND_PID=$! + trap 'echo "[backup-sidecar] Shutting down"; kill $CROND_PID 2>/dev/null; wait $CROND_PID 2>/dev/null; exit 0' TERM INT + wait $CROND_PID + + backup.sh: | + #!/bin/sh + set -eu + + TYPESENSE_PORT="${TYPESENSE_PORT}" + SNAPSHOT_BASE="/usr/share/typesense/data/snapshots" + TIMESTAMP=$(date +%Y%m%dT%H%M%S) + SNAPSHOT_DIR="${SNAPSHOT_BASE}/${TIMESTAMP}" + KEEP_LAST="${BACKUP_KEEP_LAST:-{{ .Values.backup.retention.keepLast }}}" + + # Only pod-0 runs the backup + ORDINAL=$(hostname | awk -F'-' '{print $NF}') + if [ "${ORDINAL}" != "0" ]; then + echo "[backup] Skipping: pod ordinal ${ORDINAL} is not 0" + exit 0 + fi + + echo "[backup] Starting backup at ${TIMESTAMP}" + + # Clean up local snapshot dir on exit (success or failure) + trap 'echo "[backup] Cleaning up ${SNAPSHOT_DIR}"; rm -rf "${SNAPSHOT_DIR}" 2>/dev/null || true' EXIT + + # Initialize restic repository if it does not exist yet + echo "[backup] Initializing restic repo if needed..." + restic snapshots --quiet > /dev/null 2>&1 || restic init + + # Remove stale restic locks from previous crashed runs + echo "[backup] Unlocking restic repo..." + restic unlock 2>/dev/null || true + + # Trigger Typesense snapshot — writes to data PVC at SNAPSHOT_DIR + echo "[backup] Triggering Typesense snapshot to ${SNAPSHOT_DIR}..." + RESULT=$(wget -q -O - \ + --header="X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \ + --post-data="" \ + "http://$(hostname -i):${TYPESENSE_PORT}/operations/snapshot?snapshot_path=${SNAPSHOT_DIR}") + echo "[backup] Snapshot result: ${RESULT}" + + # Upload snapshot to restic repository + echo "[backup] Running restic backup..." + restic backup "${SNAPSHOT_DIR}" --tag "typesense" --tag "${TIMESTAMP}" + + # Enforce retention policy (trap handles local cleanup) + echo "[backup] Pruning old snapshots, keeping last ${KEEP_LAST}..." + restic forget --keep-last "${KEEP_LAST}" --tag "typesense" --prune + + echo "[backup] Backup complete." +{{- end }} From 1a97afb61f34cb8a1f41e7ad2b21364983f52b7d Mon Sep 17 00:00:00 2001 From: Dimosthenis Schizas Date: Thu, 19 Mar 2026 15:41:07 +0200 Subject: [PATCH 04/13] feat(typesense): add backup sidecar to statefulset --- templates/statefulset.yaml | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/templates/statefulset.yaml b/templates/statefulset.yaml index 5b22c57..6f55643 100644 --- a/templates/statefulset.yaml +++ b/templates/statefulset.yaml @@ -140,7 +140,48 @@ spec: containerPort: {{ .Values.metrics.port }} resources: {{- toYaml .Values.metrics.resources | nindent 12 }} {{- end }} + {{- if .Values.backup.enabled }} + - name: backup-sidecar + image: "{{ .Values.backup.image.repository }}:{{ .Values.backup.image.tag }}" + imagePullPolicy: {{ .Values.backup.image.pullPolicy }} + command: ["/bin/sh", "/scripts/entrypoint.sh"] + securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: false + runAsUser: 0 + env: + - name: BACKUP_SCHEDULE + value: {{ .Values.backup.schedule | quote }} + - name: BACKUP_KEEP_LAST + value: {{ .Values.backup.retention.keepLast | quote }} + - name: TYPESENSE_PORT + value: {{ .Values.service.port | quote }} + {{- with .Values.backup.env }} + {{- toYaml . | nindent 12 }} + {{- end }} + envFrom: + - secretRef: + name: {{ .Values.secrets.secretName | default (printf "%s-secret" (include "typesense.fullname" .)) }} + optional: {{ .Values.secrets.optional }} + {{- with .Values.backup.envFrom }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: {{ include "typesense.fullname" . }}-data + mountPath: /usr/share/typesense/data + - name: backup-scripts + mountPath: /scripts + resources: {{- toYaml .Values.backup.resources | nindent 12 }} + {{- end }} + volumes: + {{- if .Values.backup.enabled }} + - name: backup-scripts + configMap: + name: {{ include "typesense.fullname" . }}-backup-scripts + defaultMode: 0755 + {{- end }} + - name: nodeslist configMap: name: {{ include "typesense.fullname" . }}-nodeslist From 810a978abe1f09627bd169f6c786146bf890e6ac Mon Sep 17 00:00:00 2001 From: Dimosthenis Schizas Date: Thu, 19 Mar 2026 15:43:18 +0200 Subject: [PATCH 05/13] feat(typesense): add backup sidecar tests and update version refs --- tests/backup_test.yaml | 233 ++++++++++++++++++++++++++++++++++++ tests/statefulset_test.yaml | 4 +- 2 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 tests/backup_test.yaml diff --git a/tests/backup_test.yaml b/tests/backup_test.yaml new file mode 100644 index 0000000..3ee822c --- /dev/null +++ b/tests/backup_test.yaml @@ -0,0 +1,233 @@ +suite: Backup +chart: + version: 1.1.0 +release: + name: foo + namespace: bar +set: + replicaCount: 3 +templates: + - backup-scripts-configmap.yaml + - statefulset.yaml +tests: + - it: should not render backup scripts configmap when backup is disabled + template: backup-scripts-configmap.yaml + asserts: + - notFailedTemplate: {} + - hasDocuments: + count: 0 + + - it: should render backup scripts configmap when backup is enabled + template: backup-scripts-configmap.yaml + set: + backup: + enabled: true + image: + repository: myrepo/backup + tag: "1.0" + asserts: + - notFailedTemplate: {} + - hasDocuments: + count: 1 + - containsDocument: + apiVersion: v1 + kind: ConfigMap + name: foo-typesense-backup-scripts + namespace: bar + - isNotEmpty: + path: .data["entrypoint.sh"] + - isNotEmpty: + path: .data["backup.sh"] + + - it: should not add backup sidecar when disabled + template: statefulset.yaml + asserts: + - notFailedTemplate: {} + - lengthEqual: + path: .spec.template.spec.containers + count: 1 + + - it: should add backup sidecar when enabled + template: statefulset.yaml + set: + metrics: + enabled: false + backup: + enabled: true + image: + repository: myrepo/backup + tag: "1.0" + asserts: + - notFailedTemplate: {} + - lengthEqual: + path: .spec.template.spec.containers + count: 2 + - equal: + path: .spec.template.spec.containers[1].name + value: backup-sidecar + + - it: should mount scripts configmap in backup sidecar + template: statefulset.yaml + set: + metrics: + enabled: false + backup: + enabled: true + image: + repository: myrepo/backup + tag: "1.0" + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.template.spec.containers[1].volumeMounts[1].name + value: backup-scripts + - equal: + path: .spec.template.spec.containers[1].volumeMounts[1].mountPath + value: /scripts + + - it: should mount data PVC in backup sidecar + template: statefulset.yaml + set: + metrics: + enabled: false + backup: + enabled: true + image: + repository: myrepo/backup + tag: "1.0" + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.template.spec.containers[1].volumeMounts[0].mountPath + value: /usr/share/typesense/data + + - it: should pass BACKUP_SCHEDULE and BACKUP_KEEP_LAST env vars to sidecar + template: statefulset.yaml + set: + metrics: + enabled: false + backup: + enabled: true + schedule: "0 3 * * *" + retention: + keepLast: 14 + image: + repository: myrepo/backup + tag: "1.0" + asserts: + - notFailedTemplate: {} + - contains: + path: .spec.template.spec.containers[1].env + content: + name: BACKUP_SCHEDULE + value: "0 3 * * *" + - contains: + path: .spec.template.spec.containers[1].env + content: + name: BACKUP_KEEP_LAST + value: "14" + + - it: should share typesense secret with backup sidecar + template: statefulset.yaml + set: + metrics: + enabled: false + backup: + enabled: true + image: + repository: myrepo/backup + tag: "1.0" + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.template.spec.containers[1].envFrom[0].secretRef.name + value: foo-typesense-secret + + - it: should merge extra envFrom into backup sidecar + template: statefulset.yaml + set: + metrics: + enabled: false + backup: + enabled: true + image: + repository: myrepo/backup + tag: "1.0" + envFrom: + - secretRef: + name: restic-credentials + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.template.spec.containers[1].envFrom[1].secretRef.name + value: restic-credentials + + - it: should render default restic image when no image is configured + template: statefulset.yaml + set: + metrics: + enabled: false + backup: + enabled: true + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.template.spec.containers[1].image + value: "ghcr.io/restic/restic:0.18.1" + + - it: backup sidecar should use configured image + template: statefulset.yaml + set: + metrics: + enabled: false + backup: + enabled: true + image: + repository: myrepo/backup + tag: "1.0" + pullPolicy: Always + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.template.spec.containers[1].image + value: "myrepo/backup:1.0" + - equal: + path: .spec.template.spec.containers[1].imagePullPolicy + value: Always + + - it: should place backup sidecar at containers[2] when metrics also enabled + template: statefulset.yaml + set: + metrics: + enabled: true + backup: + enabled: true + image: + repository: myrepo/backup + tag: "1.0" + asserts: + - notFailedTemplate: {} + - lengthEqual: + path: .spec.template.spec.containers + count: 3 + - equal: + path: .spec.template.spec.containers[2].name + value: backup-sidecar + + - it: should mount backup-scripts volume in pod when backup enabled + template: statefulset.yaml + set: + backup: + enabled: true + image: + repository: myrepo/backup + tag: "1.0" + asserts: + - notFailedTemplate: {} + - contains: + path: .spec.template.spec.volumes + content: + name: backup-scripts + configMap: + name: foo-typesense-backup-scripts + defaultMode: 0755 diff --git a/tests/statefulset_test.yaml b/tests/statefulset_test.yaml index 3e97f30..5e01705 100644 --- a/tests/statefulset_test.yaml +++ b/tests/statefulset_test.yaml @@ -1,6 +1,6 @@ suite: App chart: - version: 1.0.0 + version: 1.1.0 release: name: foo namespace: bar @@ -26,7 +26,7 @@ tests: - equal: path: .spec.template.metadata.labels value: - helm.sh/chart: typesense-1.0.0 + helm.sh/chart: typesense-1.1.0 app.kubernetes.io/name: typesense app.kubernetes.io/instance: foo app.kubernetes.io/version: "30.1" From 72defecc7cca2bc4db251003791bf847d113f79f Mon Sep 17 00:00:00 2001 From: Dimosthenis Schizas Date: Thu, 19 Mar 2026 15:44:39 +0200 Subject: [PATCH 06/13] docs: update changelog, acknowledgments, and regenerate docs for v1.1.0 --- ACKNOWLEDGMENTS.md | 8 ++++++++ Changelog.md | 12 ++++++++++++ Chart.yaml | 6 +++++- README.md | 11 ++++++++++- values.md | 11 ++++++++++- 5 files changed, 45 insertions(+), 3 deletions(-) diff --git a/ACKNOWLEDGMENTS.md b/ACKNOWLEDGMENTS.md index 5d54426..e744308 100644 --- a/ACKNOWLEDGMENTS.md +++ b/ACKNOWLEDGMENTS.md @@ -15,3 +15,11 @@ Typesense is an open-source, typo-tolerant search engine optimized for instant s - **Repository:** [imatefx/typesense-prometheus-exporter](https://github.com/imatefx/typesense-prometheus-exporter) Provides Prometheus metrics for Typesense server instances. Used as an optional sidecar container when `metrics.enabled` is set to `true`. + +## Restic + +- **Project:** [Restic](https://restic.net) +- **Repository:** [restic/restic](https://github.com/restic/restic) +- **License:** BSD-2-Clause + +A fast, secure, and efficient backup program. Used as the backup sidecar image for scheduled Typesense data snapshots when `backup.enabled` is set to `true`. diff --git a/Changelog.md b/Changelog.md index a6fc411..20830a2 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,17 @@ # Changelog +## 1.1.0 ![AppVersion: 30.1](https://img.shields.io/static/v1?label=AppVersion&message=30.1&color=success&logo=) ![Helm: v3](https://img.shields.io/static/v1?label=Helm&message=v3&color=informational&logo=helm) + +**Release date:** 2026-03-19 + +### Added + +- Backup sidecar with restic for scheduled S3 snapshots +- Backup scripts configmap with entrypoint and backup logic +- Pod-0-only backup enforcement +- Configurable cron schedule, retention policy, and restic image +- Extra env/envFrom support for backup credentials + ## 1.0.0 ![AppVersion: 30.1](https://img.shields.io/static/v1?label=AppVersion&message=30.1&color=success&logo=) ![Helm: v3](https://img.shields.io/static/v1?label=Helm&message=v3&color=informational&logo=helm) **Release date:** 2026-03-19 diff --git a/Chart.yaml b/Chart.yaml index 06295be..2a7ab5d 100644 --- a/Chart.yaml +++ b/Chart.yaml @@ -37,6 +37,10 @@ annotations: image: typesense/typesense:30.1 - name: metrics-exporter image: imatefx/typesense-prometheus-exporter:v0.1.5 + - name: backup + image: ghcr.io/restic/restic:0.18.1 artifacthub.io/changes: | - kind: added - description: Initial open-source release + description: Backup sidecar with restic for scheduled S3 snapshots + - kind: added + description: Backup scripts configmap with entrypoint and backup logic diff --git a/README.md b/README.md index 5efcc50..53b5253 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # typesense -![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 30.1](https://img.shields.io/badge/AppVersion-30.1-informational?style=flat-square) +![Version: 1.1.0](https://img.shields.io/badge/Version-1.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 30.1](https://img.shields.io/badge/AppVersion-30.1-informational?style=flat-square) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/typesense)](https://artifacthub.io/packages/search?repo=typesense) @@ -218,6 +218,15 @@ storage: | Key | Type | Default | Description | |-----|------|---------|-------------| | affinity | object | `{}` | Affinity rules for pod scheduling | +| backup.enabled | bool | `false` | Enable the backup sidecar. Requires backup.image and restic env vars to be configured. | +| backup.env | list | `[]` | Extra env vars for the backup sidecar (e.g. RESTIC_REPOSITORY, AWS_ACCESS_KEY_ID) | +| backup.envFrom | list | `[]` | References to existing Secrets/ConfigMaps to inject into the backup sidecar | +| backup.image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | +| backup.image.repository | string | `"ghcr.io/restic/restic"` | Backup sidecar image. Defaults to the official restic image which includes wget and crond via BusyBox. | +| backup.image.tag | string | `"0.18.1"` | Image tag | +| backup.resources | object | `{"requests":{"cpu":"10m","memory":"32Mi"}}` | Resource requests and limits for the backup sidecar | +| backup.retention.keepLast | int | `7` | Number of most recent restic snapshots to keep | +| backup.schedule | string | `"0 2 * * *"` | Cron schedule for backups (cron format) | | 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) | diff --git a/values.md b/values.md index 5efcc50..53b5253 100644 --- a/values.md +++ b/values.md @@ -1,6 +1,6 @@ # typesense -![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 30.1](https://img.shields.io/badge/AppVersion-30.1-informational?style=flat-square) +![Version: 1.1.0](https://img.shields.io/badge/Version-1.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 30.1](https://img.shields.io/badge/AppVersion-30.1-informational?style=flat-square) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/typesense)](https://artifacthub.io/packages/search?repo=typesense) @@ -218,6 +218,15 @@ storage: | Key | Type | Default | Description | |-----|------|---------|-------------| | affinity | object | `{}` | Affinity rules for pod scheduling | +| backup.enabled | bool | `false` | Enable the backup sidecar. Requires backup.image and restic env vars to be configured. | +| backup.env | list | `[]` | Extra env vars for the backup sidecar (e.g. RESTIC_REPOSITORY, AWS_ACCESS_KEY_ID) | +| backup.envFrom | list | `[]` | References to existing Secrets/ConfigMaps to inject into the backup sidecar | +| backup.image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | +| backup.image.repository | string | `"ghcr.io/restic/restic"` | Backup sidecar image. Defaults to the official restic image which includes wget and crond via BusyBox. | +| backup.image.tag | string | `"0.18.1"` | Image tag | +| backup.resources | object | `{"requests":{"cpu":"10m","memory":"32Mi"}}` | Resource requests and limits for the backup sidecar | +| backup.retention.keepLast | int | `7` | Number of most recent restic snapshots to keep | +| backup.schedule | string | `"0 2 * * *"` | Cron schedule for backups (cron format) | | 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) | From 92a2573d4e4c5eebcd6d9abdadb39a1478fa6d34 Mon Sep 17 00:00:00 2001 From: Dimosthenis Schizas Date: Thu, 19 Mar 2026 15:47:17 +0200 Subject: [PATCH 07/13] docs: add backup configuration section to README, remove empty snapshot dir --- README.md | 35 +++++++++++++++++++++++++++++++++++ README.md.gotmpl | 35 +++++++++++++++++++++++++++++++++++ values.md | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) diff --git a/README.md b/README.md index 53b5253..99cbd15 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ A production-ready Helm chart for deploying [Typesense](https://typesense.org), - **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 +- **Automated Backups** -- Optional restic-based backup sidecar with cron scheduling and retention policies - **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 @@ -213,6 +214,40 @@ storage: > **Warning:** Reducing `storage.size` after initial deployment has no effect. PVC resize depends on your StorageClass supporting volume expansion. +### Backup + +The chart includes an optional backup sidecar that uses [restic](https://restic.net) to take scheduled snapshots of Typesense data and upload them to a remote repository (S3, GCS, Azure Blob, etc.). + +```yaml +backup: + enabled: true + schedule: "0 2 * * *" # daily at 2am + retention: + keepLast: 7 + env: + - name: RESTIC_REPOSITORY + value: s3:s3.amazonaws.com/my-bucket/typesense + - name: RESTIC_PASSWORD + value: my-restic-password + - name: AWS_DEFAULT_REGION + value: eu-central-1 +``` + +For production, pass credentials via `backup.envFrom` referencing an existing Secret rather than inline env vars: + +```yaml +backup: + enabled: true + envFrom: + - secretRef: + name: restic-credentials +``` + +Key details: +- Backups only run on **pod-0** (other pods skip automatically) +- The restic repository is initialized automatically on the first run +- See [RESTORE_RUNBOOK.md](RESTORE_RUNBOOK.md) for disaster recovery procedures + ## Values | Key | Type | Default | Description | diff --git a/README.md.gotmpl b/README.md.gotmpl index 51b2216..5c605f3 100644 --- a/README.md.gotmpl +++ b/README.md.gotmpl @@ -18,6 +18,7 @@ A production-ready Helm chart for deploying [Typesense](https://typesense.org), - **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 +- **Automated Backups** -- Optional restic-based backup sidecar with cron scheduling and retention policies - **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 @@ -215,6 +216,40 @@ storage: > **Warning:** Reducing `storage.size` after initial deployment has no effect. PVC resize depends on your StorageClass supporting volume expansion. +### Backup + +The chart includes an optional backup sidecar that uses [restic](https://restic.net) to take scheduled snapshots of Typesense data and upload them to a remote repository (S3, GCS, Azure Blob, etc.). + +```yaml +backup: + enabled: true + schedule: "0 2 * * *" # daily at 2am + retention: + keepLast: 7 + env: + - name: RESTIC_REPOSITORY + value: s3:s3.amazonaws.com/my-bucket/typesense + - name: RESTIC_PASSWORD + value: my-restic-password + - name: AWS_DEFAULT_REGION + value: eu-central-1 +``` + +For production, pass credentials via `backup.envFrom` referencing an existing Secret rather than inline env vars: + +```yaml +backup: + enabled: true + envFrom: + - secretRef: + name: restic-credentials +``` + +Key details: +- Backups only run on **pod-0** (other pods skip automatically) +- The restic repository is initialized automatically on the first run +- See [RESTORE_RUNBOOK.md](RESTORE_RUNBOOK.md) for disaster recovery procedures + {{ template "chart.valuesSection" . }} ## Upgrading diff --git a/values.md b/values.md index 53b5253..99cbd15 100644 --- a/values.md +++ b/values.md @@ -16,6 +16,7 @@ A production-ready Helm chart for deploying [Typesense](https://typesense.org), - **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 +- **Automated Backups** -- Optional restic-based backup sidecar with cron scheduling and retention policies - **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 @@ -213,6 +214,40 @@ storage: > **Warning:** Reducing `storage.size` after initial deployment has no effect. PVC resize depends on your StorageClass supporting volume expansion. +### Backup + +The chart includes an optional backup sidecar that uses [restic](https://restic.net) to take scheduled snapshots of Typesense data and upload them to a remote repository (S3, GCS, Azure Blob, etc.). + +```yaml +backup: + enabled: true + schedule: "0 2 * * *" # daily at 2am + retention: + keepLast: 7 + env: + - name: RESTIC_REPOSITORY + value: s3:s3.amazonaws.com/my-bucket/typesense + - name: RESTIC_PASSWORD + value: my-restic-password + - name: AWS_DEFAULT_REGION + value: eu-central-1 +``` + +For production, pass credentials via `backup.envFrom` referencing an existing Secret rather than inline env vars: + +```yaml +backup: + enabled: true + envFrom: + - secretRef: + name: restic-credentials +``` + +Key details: +- Backups only run on **pod-0** (other pods skip automatically) +- The restic repository is initialized automatically on the first run +- See [RESTORE_RUNBOOK.md](RESTORE_RUNBOOK.md) for disaster recovery procedures + ## Values | Key | Type | Default | Description | From 9557e9f85e9359f60583946af00629c441ef7b78 Mon Sep 17 00:00:00 2001 From: Dimosthenis Schizas Date: Thu, 19 Mar 2026 15:47:52 +0200 Subject: [PATCH 08/13] docs: note chart is battle-tested on AWS EKS with S3 backups --- README.md | 1 + README.md.gotmpl | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 99cbd15..cf6a267 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ A production-ready Helm chart for deploying [Typesense](https://typesense.org), - **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 +- **Battle-tested** -- Actively used in production on AWS EKS with S3-based backups ## TL;DR diff --git a/README.md.gotmpl b/README.md.gotmpl index 5c605f3..131d4b1 100644 --- a/README.md.gotmpl +++ b/README.md.gotmpl @@ -22,6 +22,7 @@ A production-ready Helm chart for deploying [Typesense](https://typesense.org), - **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 +- **Battle-tested** -- Actively used in production on AWS EKS with S3-based backups ## TL;DR From ceb9df3965d56855e87dad3cc661e46deae571a3 Mon Sep 17 00:00:00 2001 From: Dimosthenis Schizas Date: Thu, 19 Mar 2026 15:48:58 +0200 Subject: [PATCH 09/13] docs: regenerate values.md --- values.md | 1 + 1 file changed, 1 insertion(+) diff --git a/values.md b/values.md index 99cbd15..cf6a267 100644 --- a/values.md +++ b/values.md @@ -20,6 +20,7 @@ A production-ready Helm chart for deploying [Typesense](https://typesense.org), - **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 +- **Battle-tested** -- Actively used in production on AWS EKS with S3-based backups ## TL;DR From fecd66001e1316dcc5f28d2ae3651cc63aac01d4 Mon Sep 17 00:00:00 2001 From: Dimosthenis Schizas Date: Thu, 19 Mar 2026 18:06:29 +0200 Subject: [PATCH 10/13] chore: bump chart version to 1.1.0 in all test files --- tests/config_test.yaml | 2 +- tests/external_secret_test.yaml | 2 +- tests/ingress_test.yaml | 2 +- tests/pdb_test.yaml | 2 +- tests/service_test.yaml | 2 +- tests/serviceaccount_test.yaml | 2 +- tests/servicemonitor_test.yaml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/config_test.yaml b/tests/config_test.yaml index 711a68a..c247670 100644 --- a/tests/config_test.yaml +++ b/tests/config_test.yaml @@ -1,6 +1,6 @@ suite: Configuration chart: - version: 1.0.0 + version: 1.1.0 release: name: foo namespace: bar diff --git a/tests/external_secret_test.yaml b/tests/external_secret_test.yaml index aff57b2..83fe7f6 100644 --- a/tests/external_secret_test.yaml +++ b/tests/external_secret_test.yaml @@ -1,6 +1,6 @@ suite: Secrets chart: - version: 1.0.0 + version: 1.1.0 release: name: foo namespace: bar diff --git a/tests/ingress_test.yaml b/tests/ingress_test.yaml index d16b621..6917ac5 100644 --- a/tests/ingress_test.yaml +++ b/tests/ingress_test.yaml @@ -1,6 +1,6 @@ suite: Networking chart: - version: 1.0.0 + version: 1.1.0 release: name: foo namespace: bar diff --git a/tests/pdb_test.yaml b/tests/pdb_test.yaml index e08b958..a83ca25 100644 --- a/tests/pdb_test.yaml +++ b/tests/pdb_test.yaml @@ -1,6 +1,6 @@ suite: PodDisruptionBudget chart: - version: 1.0.0 + version: 1.1.0 release: name: foo namespace: bar diff --git a/tests/service_test.yaml b/tests/service_test.yaml index e1c2e5a..5d33a27 100644 --- a/tests/service_test.yaml +++ b/tests/service_test.yaml @@ -1,6 +1,6 @@ suite: Networking chart: - version: 1.0.0 + version: 1.1.0 release: name: foo namespace: bar diff --git a/tests/serviceaccount_test.yaml b/tests/serviceaccount_test.yaml index 80d6a07..3e31c2a 100644 --- a/tests/serviceaccount_test.yaml +++ b/tests/serviceaccount_test.yaml @@ -1,6 +1,6 @@ suite: ServiceAccount chart: - version: 1.0.0 + version: 1.1.0 release: name: foo namespace: bar diff --git a/tests/servicemonitor_test.yaml b/tests/servicemonitor_test.yaml index 95f53ca..8b06152 100644 --- a/tests/servicemonitor_test.yaml +++ b/tests/servicemonitor_test.yaml @@ -1,6 +1,6 @@ suite: ServiceMonitor chart: - version: 1.0.0 + version: 1.1.0 release: name: foo namespace: bar From 692e1d6a4f4cebd5c2c2933c31b44b207e93413d Mon Sep 17 00:00:00 2001 From: Dimosthenis Schizas Date: Thu, 19 Mar 2026 18:07:37 +0200 Subject: [PATCH 11/13] chore: remove hardcoded chart.version from test files Let tests inherit the version from Chart.yaml instead of duplicating it in every test file. Use matchRegex for the helm.sh/chart label assertion to avoid version-dependent test updates. --- tests/backup_test.yaml | 2 -- tests/config_test.yaml | 2 -- tests/external_secret_test.yaml | 2 -- tests/ingress_test.yaml | 2 -- tests/pdb_test.yaml | 2 -- tests/service_test.yaml | 2 -- tests/serviceaccount_test.yaml | 2 -- tests/servicemonitor_test.yaml | 2 -- tests/statefulset_test.yaml | 10 +++++----- 9 files changed, 5 insertions(+), 21 deletions(-) diff --git a/tests/backup_test.yaml b/tests/backup_test.yaml index 3ee822c..21c8808 100644 --- a/tests/backup_test.yaml +++ b/tests/backup_test.yaml @@ -1,6 +1,4 @@ suite: Backup -chart: - version: 1.1.0 release: name: foo namespace: bar diff --git a/tests/config_test.yaml b/tests/config_test.yaml index c247670..1c24c4c 100644 --- a/tests/config_test.yaml +++ b/tests/config_test.yaml @@ -1,6 +1,4 @@ suite: Configuration -chart: - version: 1.1.0 release: name: foo namespace: bar diff --git a/tests/external_secret_test.yaml b/tests/external_secret_test.yaml index 83fe7f6..58eed3f 100644 --- a/tests/external_secret_test.yaml +++ b/tests/external_secret_test.yaml @@ -1,6 +1,4 @@ suite: Secrets -chart: - version: 1.1.0 release: name: foo namespace: bar diff --git a/tests/ingress_test.yaml b/tests/ingress_test.yaml index 6917ac5..2abc0a8 100644 --- a/tests/ingress_test.yaml +++ b/tests/ingress_test.yaml @@ -1,6 +1,4 @@ suite: Networking -chart: - version: 1.1.0 release: name: foo namespace: bar diff --git a/tests/pdb_test.yaml b/tests/pdb_test.yaml index a83ca25..05b9598 100644 --- a/tests/pdb_test.yaml +++ b/tests/pdb_test.yaml @@ -1,6 +1,4 @@ suite: PodDisruptionBudget -chart: - version: 1.1.0 release: name: foo namespace: bar diff --git a/tests/service_test.yaml b/tests/service_test.yaml index 5d33a27..f3e8df7 100644 --- a/tests/service_test.yaml +++ b/tests/service_test.yaml @@ -1,6 +1,4 @@ suite: Networking -chart: - version: 1.1.0 release: name: foo namespace: bar diff --git a/tests/serviceaccount_test.yaml b/tests/serviceaccount_test.yaml index 3e31c2a..3b7a486 100644 --- a/tests/serviceaccount_test.yaml +++ b/tests/serviceaccount_test.yaml @@ -1,6 +1,4 @@ suite: ServiceAccount -chart: - version: 1.1.0 release: name: foo namespace: bar diff --git a/tests/servicemonitor_test.yaml b/tests/servicemonitor_test.yaml index 8b06152..7a4cf83 100644 --- a/tests/servicemonitor_test.yaml +++ b/tests/servicemonitor_test.yaml @@ -1,6 +1,4 @@ suite: ServiceMonitor -chart: - version: 1.1.0 release: name: foo namespace: bar diff --git a/tests/statefulset_test.yaml b/tests/statefulset_test.yaml index 5e01705..9ebe050 100644 --- a/tests/statefulset_test.yaml +++ b/tests/statefulset_test.yaml @@ -1,6 +1,4 @@ suite: App -chart: - version: 1.1.0 release: name: foo namespace: bar @@ -23,14 +21,16 @@ tests: - equal: path: .spec.replicas value: 3 - - equal: + - isSubset: path: .spec.template.metadata.labels - value: - helm.sh/chart: typesense-1.1.0 + content: app.kubernetes.io/name: typesense app.kubernetes.io/instance: foo app.kubernetes.io/version: "30.1" app.kubernetes.io/managed-by: Helm + - matchRegex: + path: .spec.template.metadata.labels["helm.sh/chart"] + pattern: ^typesense-\d+\.\d+\.\d+$ - equal: path: .spec.template.spec.securityContext From fe92da05b00f6c6f20b76df308c1c25bc7a7394a Mon Sep 17 00:00:00 2001 From: Dimosthenis Schizas Date: Thu, 19 Mar 2026 19:43:18 +0200 Subject: [PATCH 12/13] fix(backup): replace crond with sleep loop, run as non-root Replace crond-based scheduling with a sleep-based interval loop. This eliminates the root requirement entirely: the backup sidecar now runs as UID 10000 (same as Typesense) with read-only root filesystem and all capabilities dropped. This fixes a bug where snapshot cleanup failed because root without DAC_OVERRIDE capability could not delete files owned by UID 10000. Running as the same UID resolves this without adding capabilities. Also adds snapshot success validation before uploading to restic. Breaking change: backup.schedule (cron expression) is replaced by backup.intervalSeconds (integer, default 86400 = 24h). --- README.md | 27 +++--- README.md.gotmpl | 25 +++--- templates/backup-scripts-configmap.yaml | 29 ++++--- templates/statefulset.yaml | 13 ++- tests/backup_test.yaml | 29 ++++++- values.md | 27 +++--- values.schema.json | 110 +++++++++++++++++++++++- values.yaml | 8 +- 8 files changed, 209 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index cf6a267..612ae9e 100644 --- a/README.md +++ b/README.md @@ -222,31 +222,32 @@ The chart includes an optional backup sidecar that uses [restic](https://restic. ```yaml backup: enabled: true - schedule: "0 2 * * *" # daily at 2am + intervalSeconds: 86400 # every 24 hours retention: keepLast: 7 - env: - - name: RESTIC_REPOSITORY - value: s3:s3.amazonaws.com/my-bucket/typesense - - name: RESTIC_PASSWORD - value: my-restic-password - - name: AWS_DEFAULT_REGION - value: eu-central-1 + envFrom: + - secretRef: + name: restic-credentials ``` -For production, pass credentials via `backup.envFrom` referencing an existing Secret rather than inline env vars: +The `restic-credentials` Secret should contain `RESTIC_REPOSITORY`, `RESTIC_PASSWORD`, and any cloud provider credentials (e.g., `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`). + +For quick testing, you can pass credentials inline (not recommended for production): ```yaml backup: enabled: true - envFrom: - - secretRef: - name: restic-credentials + env: + - name: RESTIC_REPOSITORY + value: s3:s3.amazonaws.com/my-bucket/typesense + - name: RESTIC_PASSWORD + value: my-restic-password ``` Key details: - Backups only run on **pod-0** (other pods skip automatically) - The restic repository is initialized automatically on the first run +- The backup sidecar runs as non-root (UID 10000) with all capabilities dropped - See [RESTORE_RUNBOOK.md](RESTORE_RUNBOOK.md) for disaster recovery procedures ## Values @@ -260,9 +261,9 @@ Key details: | backup.image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | | backup.image.repository | string | `"ghcr.io/restic/restic"` | Backup sidecar image. Defaults to the official restic image which includes wget and crond via BusyBox. | | backup.image.tag | string | `"0.18.1"` | Image tag | +| backup.intervalSeconds | int | `86400` | Interval in seconds between backup runs (default: 86400 = 24h) | | backup.resources | object | `{"requests":{"cpu":"10m","memory":"32Mi"}}` | Resource requests and limits for the backup sidecar | | backup.retention.keepLast | int | `7` | Number of most recent restic snapshots to keep | -| backup.schedule | string | `"0 2 * * *"` | Cron schedule for backups (cron format) | | 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) | diff --git a/README.md.gotmpl b/README.md.gotmpl index 131d4b1..d6b92df 100644 --- a/README.md.gotmpl +++ b/README.md.gotmpl @@ -224,31 +224,32 @@ The chart includes an optional backup sidecar that uses [restic](https://restic. ```yaml backup: enabled: true - schedule: "0 2 * * *" # daily at 2am + intervalSeconds: 86400 # every 24 hours retention: keepLast: 7 - env: - - name: RESTIC_REPOSITORY - value: s3:s3.amazonaws.com/my-bucket/typesense - - name: RESTIC_PASSWORD - value: my-restic-password - - name: AWS_DEFAULT_REGION - value: eu-central-1 + envFrom: + - secretRef: + name: restic-credentials ``` -For production, pass credentials via `backup.envFrom` referencing an existing Secret rather than inline env vars: +The `restic-credentials` Secret should contain `RESTIC_REPOSITORY`, `RESTIC_PASSWORD`, and any cloud provider credentials (e.g., `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`). + +For quick testing, you can pass credentials inline (not recommended for production): ```yaml backup: enabled: true - envFrom: - - secretRef: - name: restic-credentials + env: + - name: RESTIC_REPOSITORY + value: s3:s3.amazonaws.com/my-bucket/typesense + - name: RESTIC_PASSWORD + value: my-restic-password ``` Key details: - Backups only run on **pod-0** (other pods skip automatically) - The restic repository is initialized automatically on the first run +- The backup sidecar runs as non-root (UID 10000) with all capabilities dropped - See [RESTORE_RUNBOOK.md](RESTORE_RUNBOOK.md) for disaster recovery procedures {{ template "chart.valuesSection" . }} diff --git a/templates/backup-scripts-configmap.yaml b/templates/backup-scripts-configmap.yaml index 329e2a8..a5a6016 100644 --- a/templates/backup-scripts-configmap.yaml +++ b/templates/backup-scripts-configmap.yaml @@ -10,23 +10,29 @@ data: entrypoint.sh: | #!/bin/sh set -e - mkdir -p /var/spool/cron/crontabs - echo "${BACKUP_SCHEDULE:-0 2 * * *} /scripts/backup.sh >> /proc/1/fd/1 2>&1" > /var/spool/cron/crontabs/root - echo "[backup-sidecar] Starting crond with schedule: ${BACKUP_SCHEDULE:-0 2 * * *}" - crond -f -L /dev/stdout & - CROND_PID=$! - trap 'echo "[backup-sidecar] Shutting down"; kill $CROND_PID 2>/dev/null; wait $CROND_PID 2>/dev/null; exit 0' TERM INT - wait $CROND_PID + + INTERVAL="${BACKUP_INTERVAL_SECONDS}" + + echo "[backup-sidecar] Starting with interval: ${INTERVAL}s" + echo "[backup-sidecar] First backup will run in ${INTERVAL}s" + + trap 'echo "[backup-sidecar] Shutting down"; exit 0' TERM INT + + while true; do + sleep "${INTERVAL}" & + SLEEP_PID=$! + wait $SLEEP_PID || exit 0 + /scripts/backup.sh || echo "[backup-sidecar] Backup failed, will retry in ${INTERVAL}s" + done backup.sh: | #!/bin/sh set -eu - TYPESENSE_PORT="${TYPESENSE_PORT}" SNAPSHOT_BASE="/usr/share/typesense/data/snapshots" TIMESTAMP=$(date +%Y%m%dT%H%M%S) SNAPSHOT_DIR="${SNAPSHOT_BASE}/${TIMESTAMP}" - KEEP_LAST="${BACKUP_KEEP_LAST:-{{ .Values.backup.retention.keepLast }}}" + KEEP_LAST="${BACKUP_KEEP_LAST}" # Only pod-0 runs the backup ORDINAL=$(hostname | awk -F'-' '{print $NF}') @@ -48,7 +54,7 @@ data: echo "[backup] Unlocking restic repo..." restic unlock 2>/dev/null || true - # Trigger Typesense snapshot — writes to data PVC at SNAPSHOT_DIR + # Trigger Typesense snapshot echo "[backup] Triggering Typesense snapshot to ${SNAPSHOT_DIR}..." RESULT=$(wget -q -O - \ --header="X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \ @@ -56,6 +62,9 @@ data: "http://$(hostname -i):${TYPESENSE_PORT}/operations/snapshot?snapshot_path=${SNAPSHOT_DIR}") echo "[backup] Snapshot result: ${RESULT}" + # Validate snapshot succeeded + echo "${RESULT}" | grep -q '"success":true' || { echo "[backup] Snapshot failed: ${RESULT}"; exit 1; } + # Upload snapshot to restic repository echo "[backup] Running restic backup..." restic backup "${SNAPSHOT_DIR}" --tag "typesense" --tag "${TIMESTAMP}" diff --git a/templates/statefulset.yaml b/templates/statefulset.yaml index 6f55643..e92c85d 100644 --- a/templates/statefulset.yaml +++ b/templates/statefulset.yaml @@ -147,11 +147,16 @@ spec: command: ["/bin/sh", "/scripts/entrypoint.sh"] securityContext: allowPrivilegeEscalation: false - runAsNonRoot: false - runAsUser: 0 + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 10000 + runAsGroup: 3000 + capabilities: + drop: + - ALL env: - - name: BACKUP_SCHEDULE - value: {{ .Values.backup.schedule | quote }} + - name: BACKUP_INTERVAL_SECONDS + value: {{ .Values.backup.intervalSeconds | quote }} - name: BACKUP_KEEP_LAST value: {{ .Values.backup.retention.keepLast | quote }} - name: TYPESENSE_PORT diff --git a/tests/backup_test.yaml b/tests/backup_test.yaml index 21c8808..7390279 100644 --- a/tests/backup_test.yaml +++ b/tests/backup_test.yaml @@ -99,14 +99,14 @@ tests: path: .spec.template.spec.containers[1].volumeMounts[0].mountPath value: /usr/share/typesense/data - - it: should pass BACKUP_SCHEDULE and BACKUP_KEEP_LAST env vars to sidecar + - it: should pass BACKUP_INTERVAL_SECONDS and BACKUP_KEEP_LAST env vars to sidecar template: statefulset.yaml set: metrics: enabled: false backup: enabled: true - schedule: "0 3 * * *" + intervalSeconds: 3600 retention: keepLast: 14 image: @@ -117,14 +117,35 @@ tests: - contains: path: .spec.template.spec.containers[1].env content: - name: BACKUP_SCHEDULE - value: "0 3 * * *" + name: BACKUP_INTERVAL_SECONDS + value: "3600" - contains: path: .spec.template.spec.containers[1].env content: name: BACKUP_KEEP_LAST value: "14" + - it: should harden backup sidecar with securityContext + template: statefulset.yaml + set: + metrics: + enabled: false + backup: + enabled: true + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.template.spec.containers[1].securityContext + value: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 10000 + runAsGroup: 3000 + capabilities: + drop: + - ALL + - it: should share typesense secret with backup sidecar template: statefulset.yaml set: diff --git a/values.md b/values.md index cf6a267..612ae9e 100644 --- a/values.md +++ b/values.md @@ -222,31 +222,32 @@ The chart includes an optional backup sidecar that uses [restic](https://restic. ```yaml backup: enabled: true - schedule: "0 2 * * *" # daily at 2am + intervalSeconds: 86400 # every 24 hours retention: keepLast: 7 - env: - - name: RESTIC_REPOSITORY - value: s3:s3.amazonaws.com/my-bucket/typesense - - name: RESTIC_PASSWORD - value: my-restic-password - - name: AWS_DEFAULT_REGION - value: eu-central-1 + envFrom: + - secretRef: + name: restic-credentials ``` -For production, pass credentials via `backup.envFrom` referencing an existing Secret rather than inline env vars: +The `restic-credentials` Secret should contain `RESTIC_REPOSITORY`, `RESTIC_PASSWORD`, and any cloud provider credentials (e.g., `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`). + +For quick testing, you can pass credentials inline (not recommended for production): ```yaml backup: enabled: true - envFrom: - - secretRef: - name: restic-credentials + env: + - name: RESTIC_REPOSITORY + value: s3:s3.amazonaws.com/my-bucket/typesense + - name: RESTIC_PASSWORD + value: my-restic-password ``` Key details: - Backups only run on **pod-0** (other pods skip automatically) - The restic repository is initialized automatically on the first run +- The backup sidecar runs as non-root (UID 10000) with all capabilities dropped - See [RESTORE_RUNBOOK.md](RESTORE_RUNBOOK.md) for disaster recovery procedures ## Values @@ -260,9 +261,9 @@ Key details: | backup.image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | | backup.image.repository | string | `"ghcr.io/restic/restic"` | Backup sidecar image. Defaults to the official restic image which includes wget and crond via BusyBox. | | backup.image.tag | string | `"0.18.1"` | Image tag | +| backup.intervalSeconds | int | `86400` | Interval in seconds between backup runs (default: 86400 = 24h) | | backup.resources | object | `{"requests":{"cpu":"10m","memory":"32Mi"}}` | Resource requests and limits for the backup sidecar | | backup.retention.keepLast | int | `7` | Number of most recent restic snapshots to keep | -| backup.schedule | string | `"0 2 * * *"` | Cron schedule for backups (cron format) | | 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) | diff --git a/values.schema.json b/values.schema.json index bfa6c4b..6b9cdd0 100644 --- a/values.schema.json +++ b/values.schema.json @@ -6,6 +6,113 @@ "required": [], "title": "affinity" }, + "backup": { + "properties": { + "enabled": { + "default": false, + "description": "Enable the backup sidecar. Requires backup.image and restic env vars to be configured.", + "title": "enabled", + "type": "boolean" + }, + "env": { + "description": "Extra env vars for the backup sidecar (e.g. RESTIC_REPOSITORY, AWS_ACCESS_KEY_ID)", + "items": { + "required": [] + }, + "required": [], + "title": "env" + }, + "envFrom": { + "description": "References to existing Secrets/ConfigMaps to inject into the backup sidecar", + "items": { + "required": [] + }, + "required": [], + "title": "envFrom" + }, + "image": { + "properties": { + "pullPolicy": { + "default": "IfNotPresent", + "description": "Image pull policy", + "required": [], + "title": "pullPolicy" + }, + "repository": { + "default": "ghcr.io/restic/restic", + "description": "Backup sidecar image. Defaults to the official restic image which includes wget and crond via BusyBox.", + "required": [], + "title": "repository" + }, + "tag": { + "default": "0.18.1", + "description": "Image tag", + "required": [], + "title": "tag" + } + }, + "required": [], + "title": "image", + "type": "object" + }, + "intervalSeconds": { + "default": 86400, + "description": "Interval in seconds between backup runs (default: 86400 = 24h)", + "minimum": 60, + "title": "intervalSeconds", + "type": "integer" + }, + "resources": { + "description": "Resource requests and limits for the backup sidecar", + "properties": { + "requests": { + "properties": { + "cpu": { + "default": "10m", + "title": "cpu", + "type": "string" + }, + "memory": { + "default": "32Mi", + "title": "memory", + "type": "string" + } + }, + "required": [ + "cpu", + "memory" + ], + "title": "requests", + "type": "object" + } + }, + "required": [ + "requests" + ], + "title": "resources" + }, + "retention": { + "properties": { + "keepLast": { + "default": 7, + "description": "Number of most recent restic snapshots to keep", + "minimum": 1, + "title": "keepLast", + "type": "integer" + } + }, + "required": [], + "title": "retention", + "type": "object" + } + }, + "required": [ + "image", + "retention" + ], + "title": "backup", + "type": "object" + }, "extraArgs": { "description": "Extra command-line arguments for Typesense server (e.g., [\"--filter-by-max-ops=200\"])", "items": { @@ -839,7 +946,8 @@ "secrets", "typesense", "serviceAccount", - "metrics" + "metrics", + "backup" ], "type": "object" } diff --git a/values.yaml b/values.yaml index c59a753..33d85f2 100644 --- a/values.yaml +++ b/values.yaml @@ -325,8 +325,12 @@ backup: # @schema # -- Enable the backup sidecar. Requires backup.image and restic env vars to be configured. enabled: false - # -- Cron schedule for backups (cron format) - schedule: "0 2 * * *" + # @schema + # type: integer + # minimum: 60 + # @schema + # -- Interval in seconds between backup runs (default: 86400 = 24h) + intervalSeconds: 86400 image: # -- Backup sidecar image. Defaults to the official restic image which includes wget and crond via BusyBox. repository: ghcr.io/restic/restic From 2160d94229db7cd185f478a634255e5e3f4777fb Mon Sep 17 00:00:00 2001 From: Dimosthenis Schizas Date: Thu, 19 Mar 2026 22:32:34 +0200 Subject: [PATCH 13/13] fix(backup): add tmp emptyDir and RESTIC_CACHE_DIR for read-only rootfs Restic needs writable /tmp for temp packs and /.cache for its cache. Mount an emptyDir at /tmp and set RESTIC_CACHE_DIR to /tmp/.cache/restic. Validated on EKS dev cluster with rc.9 of the internal chart. --- templates/statefulset.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/templates/statefulset.yaml b/templates/statefulset.yaml index e92c85d..c511038 100644 --- a/templates/statefulset.yaml +++ b/templates/statefulset.yaml @@ -161,6 +161,8 @@ spec: value: {{ .Values.backup.retention.keepLast | quote }} - name: TYPESENSE_PORT value: {{ .Values.service.port | quote }} + - name: RESTIC_CACHE_DIR + value: /tmp/.cache/restic {{- with .Values.backup.env }} {{- toYaml . | nindent 12 }} {{- end }} @@ -176,6 +178,8 @@ spec: mountPath: /usr/share/typesense/data - name: backup-scripts mountPath: /scripts + - name: backup-tmp + mountPath: /tmp resources: {{- toYaml .Values.backup.resources | nindent 12 }} {{- end }} @@ -185,6 +189,8 @@ spec: configMap: name: {{ include "typesense.fullname" . }}-backup-scripts defaultMode: 0755 + - name: backup-tmp + emptyDir: {} {{- end }} - name: nodeslist