diff --git a/packages/apps/ferretdb/Chart.yaml b/packages/apps/ferretdb/Chart.yaml
new file mode 100644
index 00000000..6d35a52b
--- /dev/null
+++ b/packages/apps/ferretdb/Chart.yaml
@@ -0,0 +1,25 @@
+apiVersion: v2
+name: ferretdb
+description: Managed FerretDB service
+icon: ferretdb.svg
+
+# A chart can be either an 'application' or a 'library' chart.
+#
+# Application charts are a collection of templates that can be packaged into versioned archives
+# to be deployed.
+#
+# Library charts provide useful utilities or functions for the chart developer. They're included as
+# a dependency of application charts to inject those utilities and functions into the rendering
+# pipeline. Library charts do not define any templates and therefore cannot be deployed.
+type: application
+
+# This is the chart version. This version number should be incremented each time you make changes
+# to the chart and its templates, including the app version.
+# Versions are expected to follow Semantic Versioning (https://semver.org/)
+version: 0.1.0
+
+# This is the version number of the application being deployed. This version number should be
+# incremented each time you make changes to the application. Versions are not expected to
+# follow Semantic Versioning. They should reflect the version the application is using.
+# It is recommended to use it with quotes.
+appVersion: "1.22.0"
diff --git a/packages/apps/ferretdb/Makefile b/packages/apps/ferretdb/Makefile
new file mode 100644
index 00000000..207e2133
--- /dev/null
+++ b/packages/apps/ferretdb/Makefile
@@ -0,0 +1,2 @@
+generate:
+ readme-generator -v values.yaml -s values.schema.json -r README.md
diff --git a/packages/apps/ferretdb/README.md b/packages/apps/ferretdb/README.md
new file mode 100644
index 00000000..afa4f961
--- /dev/null
+++ b/packages/apps/ferretdb/README.md
@@ -0,0 +1,34 @@
+# Managed FerretDB Service
+
+## Parameters
+
+### Common parameters
+
+| Name | Description | Value |
+| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | ------- |
+| `external` | Enable external access from outside the cluster | `false` |
+| `size` | Persistent Volume size | `10Gi` |
+| `replicas` | Number of Postgres replicas | `2` |
+| `quorum.minSyncReplicas` | Minimum number of synchronous replicas that must acknowledge a transaction before it is considered committed. | `0` |
+| `quorum.maxSyncReplicas` | Maximum number of synchronous replicas that can acknowledge a transaction (must be lower than the number of instances). | `0` |
+
+### Configuration parameters
+
+| Name | Description | Value |
+| ------- | ------------------- | ----- |
+| `users` | Users configuration | `{}` |
+
+### Backup parameters
+
+| Name | Description | Value |
+| ------------------------ | ---------------------------------------------- | ------------------------------------------------------ |
+| `backup.enabled` | Enable pereiodic backups | `false` |
+| `backup.s3Region` | The AWS S3 region where backups are stored | `us-east-1` |
+| `backup.s3Bucket` | The S3 bucket used for storing backups | `s3.example.org/postgres-backups` |
+| `backup.schedule` | Cron schedule for automated backups | `0 2 * * *` |
+| `backup.cleanupStrategy` | The strategy for cleaning up old backups | `--keep-last=3 --keep-daily=3 --keep-within-weekly=1m` |
+| `backup.s3AccessKey` | The access key for S3, used for authentication | `oobaiRus9pah8PhohL1ThaeTa4UVa7gu` |
+| `backup.s3SecretKey` | The secret key for S3, used for authentication | `ju3eum4dekeich9ahM1te8waeGai0oog` |
+| `backup.resticPassword` | The password for Restic backup encryption | `ChaXoveekoh6eigh4siesheeda2quai0` |
+
+
diff --git a/packages/apps/ferretdb/ferretdb.svg b/packages/apps/ferretdb/ferretdb.svg
new file mode 100644
index 00000000..196871e1
--- /dev/null
+++ b/packages/apps/ferretdb/ferretdb.svg
@@ -0,0 +1,54 @@
+
+
+
+
diff --git a/packages/apps/ferretdb/templates/.gitkeep b/packages/apps/ferretdb/templates/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/apps/ferretdb/templates/backup-cronjob.yaml b/packages/apps/ferretdb/templates/backup-cronjob.yaml
new file mode 100644
index 00000000..84a6aee7
--- /dev/null
+++ b/packages/apps/ferretdb/templates/backup-cronjob.yaml
@@ -0,0 +1,99 @@
+{{- if .Values.backup.enabled }}
+{{ $image := .Files.Get "images/backup.json" | fromJson }}
+
+apiVersion: batch/v1
+kind: CronJob
+metadata:
+ name: {{ .Release.Name }}-backup
+spec:
+ schedule: "{{ .Values.backup.schedule }}"
+ concurrencyPolicy: Forbid
+ successfulJobsHistoryLimit: 3
+ failedJobsHistoryLimit: 3
+ jobTemplate:
+ spec:
+ backoffLimit: 2
+ template:
+ spec:
+ restartPolicy: OnFailure
+ template:
+ metadata:
+ annotations:
+ checksum/config: {{ include (print $.Template.BasePath "/backup-script.yaml") . | sha256sum }}
+ checksum/secret: {{ include (print $.Template.BasePath "/backup-secret.yaml") . | sha256sum }}
+ spec:
+ restartPolicy: Never
+ containers:
+ - name: mysqldump
+ image: "{{ index $image "image.name" }}@{{ index $image "containerimage.digest" }}"
+ command:
+ - /bin/sh
+ - /scripts/backup.sh
+ env:
+ - name: REPO_PREFIX
+ value: {{ required "s3Bucket is not specified!" .Values.backup.s3Bucket | quote }}
+ - name: CLEANUP_STRATEGY
+ value: {{ required "cleanupStrategy is not specified!" .Values.backup.cleanupStrategy | quote }}
+ - name: PGUSER
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Release.Name }}-postgres-superuser
+ key: username
+ - name: PGPASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Release.Name }}-postgres-superuser
+ key: password
+ - name: PGHOST
+ value: {{ .Release.Name }}-postgres-rw
+ - name: PGPORT
+ value: "5432"
+ - name: PGDATABASE
+ value: postgres
+ - name: AWS_ACCESS_KEY_ID
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Release.Name }}-backup
+ key: s3AccessKey
+ - name: AWS_SECRET_ACCESS_KEY
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Release.Name }}-backup
+ key: s3SecretKey
+ - name: AWS_DEFAULT_REGION
+ value: {{ .Values.backup.s3Region }}
+ - name: RESTIC_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Release.Name }}-backup
+ key: resticPassword
+ volumeMounts:
+ - mountPath: /scripts
+ name: scripts
+ - mountPath: /tmp
+ name: tmp
+ - mountPath: /.cache
+ name: cache
+ securityContext:
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop:
+ - ALL
+ privileged: false
+ readOnlyRootFilesystem: true
+ runAsNonRoot: true
+ volumes:
+ - name: scripts
+ secret:
+ secretName: {{ .Release.Name }}-backup-script
+ - name: tmp
+ emptyDir: {}
+ - name: cache
+ emptyDir: {}
+ securityContext:
+ runAsNonRoot: true
+ runAsUser: 9000
+ runAsGroup: 9000
+ seccompProfile:
+ type: RuntimeDefault
+{{- end }}
diff --git a/packages/apps/ferretdb/templates/backup-script.yaml b/packages/apps/ferretdb/templates/backup-script.yaml
new file mode 100644
index 00000000..362bdc01
--- /dev/null
+++ b/packages/apps/ferretdb/templates/backup-script.yaml
@@ -0,0 +1,50 @@
+{{- if .Values.backup.enabled }}
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {{ .Release.Name }}-backup-script
+stringData:
+ backup.sh: |
+ #!/bin/sh
+ set -e
+ set -o pipefail
+
+ JOB_ID="job-$(uuidgen|cut -f1 -d-)"
+ DB_LIST=$(psql -Atq -c 'SELECT datname FROM pg_catalog.pg_database;' | grep -v '^\(postgres\|app\|template.*\)$')
+ echo DB_LIST=$(echo "$DB_LIST" | shuf) # shuffle list
+ echo "Job ID: $JOB_ID"
+ echo "Target repo: $REPO_PREFIX"
+ echo "Cleanup strategy: $CLEANUP_STRATEGY"
+ echo "Start backup for:"
+ echo "$DB_LIST"
+ echo
+ echo "Backup started at `date +%Y-%m-%d\ %H:%M:%S`"
+ for db in $DB_LIST; do
+ (
+ set -x
+ restic -r "s3:${REPO_PREFIX}/$db" cat config >/dev/null 2>&1 || \
+ restic -r "s3:${REPO_PREFIX}/$db" init --repository-version 2
+ restic -r "s3:${REPO_PREFIX}/$db" unlock --remove-all >/dev/null 2>&1 || true # no locks, k8s takes care of it
+ pg_dump -Z0 -Ft -d "$db" | \
+ restic -r "s3:${REPO_PREFIX}/$db" backup --tag "$JOB_ID" --stdin --stdin-filename dump.tar
+ restic -r "s3:${REPO_PREFIX}/$db" tag --tag "$JOB_ID" --set "completed"
+ )
+ done
+ echo "Backup finished at `date +%Y-%m-%d\ %H:%M:%S`"
+
+ echo
+ echo "Run cleanup:"
+ echo
+
+ echo "Cleanup started at `date +%Y-%m-%d\ %H:%M:%S`"
+ for db in $DB_LIST; do
+ (
+ set -x
+ restic forget -r "s3:${REPO_PREFIX}/$db" --group-by=tags --keep-tag "completed" # keep completed snapshots only
+ restic forget -r "s3:${REPO_PREFIX}/$db" --group-by=tags $CLEANUP_STRATEGY
+ restic prune -r "s3:${REPO_PREFIX}/$db"
+ )
+ done
+ echo "Cleanup finished at `date +%Y-%m-%d\ %H:%M:%S`"
+{{- end }}
diff --git a/packages/apps/ferretdb/templates/backup-secret.yaml b/packages/apps/ferretdb/templates/backup-secret.yaml
new file mode 100644
index 00000000..be221e2f
--- /dev/null
+++ b/packages/apps/ferretdb/templates/backup-secret.yaml
@@ -0,0 +1,11 @@
+{{- if .Values.backup.enabled }}
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {{ .Release.Name }}-backup
+stringData:
+ s3AccessKey: {{ required "s3AccessKey is not specified!" .Values.backup.s3AccessKey }}
+ s3SecretKey: {{ required "s3SecretKey is not specified!" .Values.backup.s3SecretKey }}
+ resticPassword: {{ required "resticPassword is not specified!" .Values.backup.resticPassword }}
+{{- end }}
diff --git a/packages/apps/ferretdb/templates/external-svc.yaml b/packages/apps/ferretdb/templates/external-svc.yaml
new file mode 100644
index 00000000..2e0501c8
--- /dev/null
+++ b/packages/apps/ferretdb/templates/external-svc.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ .Release.Name }}
+spec:
+ type: {{ ternary "LoadBalancer" "ClusterIP" .Values.external }}
+ {{- if .Values.external }}
+ externalTrafficPolicy: Local
+ allocateLoadBalancerNodePorts: false
+ {{- end }}
+ ports:
+ - name: ferretdb
+ port: 27017
+ selector:
+ app: {{ .Release.Name }}
diff --git a/packages/apps/ferretdb/templates/ferretdb.yaml b/packages/apps/ferretdb/templates/ferretdb.yaml
new file mode 100644
index 00000000..8d72787b
--- /dev/null
+++ b/packages/apps/ferretdb/templates/ferretdb.yaml
@@ -0,0 +1,26 @@
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ .Release.Name }}
+spec:
+ replicas: {{ .Values.replicas }}
+ selector:
+ matchLabels:
+ app: {{ .Release.Name }}
+ template:
+ metadata:
+ labels:
+ app: {{ .Release.Name }}
+ spec:
+ containers:
+ - name: ferretdb
+ image: ghcr.io/ferretdb/ferretdb:1.22.0
+ ports:
+ - containerPort: 27017
+ env:
+ - name: FERRETDB_POSTGRESQL_URL
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Release.Name }}-postgres-app
+ key: uri
diff --git a/packages/apps/ferretdb/templates/init-job.yaml b/packages/apps/ferretdb/templates/init-job.yaml
new file mode 100644
index 00000000..b7b03133
--- /dev/null
+++ b/packages/apps/ferretdb/templates/init-job.yaml
@@ -0,0 +1,66 @@
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: {{ .Release.Name }}-init-job
+ annotations:
+ "helm.sh/hook": post-install,post-upgrade
+ "helm.sh/hook-weight": "-5"
+ "helm.sh/hook-delete-policy": before-hook-creation
+spec:
+ template:
+ metadata:
+ name: {{ .Release.Name }}-init-job
+ annotations:
+ checksum/config: {{ include (print $.Template.BasePath "/init-script.yaml") . | sha256sum }}
+ spec:
+ restartPolicy: Never
+ containers:
+ - name: postgres
+ image: ghcr.io/cloudnative-pg/postgresql:15.3
+ command:
+ - bash
+ - /scripts/init.sh
+ env:
+ - name: PGUSER
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Release.Name }}-postgres-superuser
+ key: username
+ - name: PGPASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Release.Name }}-postgres-superuser
+ key: password
+ - name: PGHOST
+ value: {{ .Release.Name }}-postgres-rw
+ - name: PGPORT
+ value: "5432"
+ - name: PGDATABASE
+ value: postgres
+ securityContext:
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop:
+ - ALL
+ privileged: false
+ readOnlyRootFilesystem: true
+ runAsNonRoot: true
+ volumeMounts:
+ - mountPath: /etc/secret
+ name: secret
+ - mountPath: /scripts
+ name: scripts
+ securityContext:
+ fsGroup: 26
+ runAsGroup: 26
+ runAsNonRoot: true
+ runAsUser: 26
+ seccompProfile:
+ type: RuntimeDefault
+ volumes:
+ - name: secret
+ secret:
+ secretName: {{ .Release.Name }}-postgres-superuser
+ - name: scripts
+ secret:
+ secretName: {{ .Release.Name }}-init-script
diff --git a/packages/apps/ferretdb/templates/init-script.yaml b/packages/apps/ferretdb/templates/init-script.yaml
new file mode 100644
index 00000000..3917f8fd
--- /dev/null
+++ b/packages/apps/ferretdb/templates/init-script.yaml
@@ -0,0 +1,104 @@
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {{ .Release.Name }}-init-script
+stringData:
+ init.sh: |
+ #!/bin/bash
+ set -e
+ echo "== create users"
+ {{- if .Values.users }}
+ psql -v ON_ERROR_STOP=1 <<\EOT
+ {{- range $user, $u := .Values.users }}
+ SELECT 'CREATE ROLE {{ $user }} LOGIN INHERIT;'
+ WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '{{ $user }}')\gexec
+ ALTER ROLE {{ $user }} WITH PASSWORD '{{ $u.password }}' LOGIN INHERIT {{ ternary "REPLICATION" "NOREPLICATION" (default false $u.replication) }};
+ COMMENT ON ROLE {{ $user }} IS 'user managed by helm';
+ {{- end }}
+ EOT
+ {{- end }}
+
+ echo "== delete users"
+ MANAGED_USERS=$(echo '\du+' | psql | awk -F'|' '$4 == " user managed by helm" {print $1}' | awk NF=NF RS= OFS=' ')
+ DEFINED_USERS="{{ join " " (keys .Values.users) }}"
+ DELETE_USERS=$(for user in $MANAGED_USERS; do case " $DEFINED_USERS " in *" $user "*) :;; *) echo $user;; esac; done)
+
+ echo "users to delete: $DELETE_USERS"
+ for user in $DELETE_USERS; do
+ # https://stackoverflow.com/a/51257346/2931267
+ psql -v ON_ERROR_STOP=1 --echo-all <