diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index aa1c105f0ebd4e7d46a45759c4b3fecdf4038a03..d617e555c2b93e3c22365a35f74309fb9cb17ad2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -7,7 +7,7 @@ stages:
   - lint
   - release
 include:
-  - /actual/pipeline.yml
+  - /fdroid-repo/pipeline.yml
   - /flood/pipeline.yml
   - /imghost/pipeline.yml
   - /jellyfin/pipeline.yml
@@ -17,6 +17,7 @@ include:
   - /powerdns/pipeline.yml
   - /postgresql/pipeline.yml
   - /quassel/pipeline.yml
+  - /quassel-search/pipeline.yml
   - /restic/pipeline.yml
   - /rtorrent/pipeline.yml
   - /seafile/pipeline.yml
diff --git a/fdroid-repo/Chart.yaml b/fdroid-repo/Chart.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..984ee3c004cff6fab9b67e02e74b635ce9129cd3
--- /dev/null
+++ b/fdroid-repo/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v2
+name: fdroid-repo
+description: Helm Chart for fdroid-repo
+type: application
+version: 1.0.0
+appVersion: "0.2.0"
diff --git a/fdroid-repo/pipeline.yml b/fdroid-repo/pipeline.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6d9c52d3fcd26bbbd7ba02caa2819333c193e872
--- /dev/null
+++ b/fdroid-repo/pipeline.yml
@@ -0,0 +1,21 @@
+lint-fdroid-repo:
+  stage: lint
+  rules:
+    - changes:
+        - fdroid-repo/**/*
+  script:
+    - helm lint fdroid-repo
+
+release-fdroid-repo:
+  stage: release
+  needs:
+    - lint-fdroid-repo
+  rules:
+    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
+      changes:
+        - fdroid-repo/**/*
+  script:
+    - apk add --no-cache git
+    - helm plugin install https://github.com/chartmuseum/helm-push.git
+    - helm repo add --username gitlab-ci-token --password $CI_JOB_TOKEN repo ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/helm/stable
+    - helm cm-push fdroid-repo repo
diff --git a/fdroid-repo/templates/_helpers.tpl b/fdroid-repo/templates/_helpers.tpl
new file mode 100644
index 0000000000000000000000000000000000000000..a39419bfb9eaec5140178fe7a0dc690203c3db98
--- /dev/null
+++ b/fdroid-repo/templates/_helpers.tpl
@@ -0,0 +1,56 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "fdroid-repo-helm.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "fdroid-repo-helm.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "fdroid-repo-helm.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "fdroid-repo-helm.labels" -}}
+helm.sh/chart: {{ include "fdroid-repo-helm.chart" . }}
+{{ include "fdroid-repo-helm.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "fdroid-repo-helm.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "fdroid-repo-helm.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+
+{{- define "fdroid-repo-helm.sslPath" -}}
+/certs
+{{- end }}
diff --git a/fdroid-repo/templates/cronjob.yaml b/fdroid-repo/templates/cronjob.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d42956f5a3ae6e9ec9ba480be8652ad24090966d
--- /dev/null
+++ b/fdroid-repo/templates/cronjob.yaml
@@ -0,0 +1,126 @@
+apiVersion: batch/v1
+kind: CronJob
+metadata:
+  name: {{ include "fdroid-repo-helm.fullname" . }}
+  labels:
+    {{- include "fdroid-repo-helm.labels" . | nindent 4 }}
+spec:
+  concurrencyPolicy: Forbid
+  schedule: "{{ .Values.schedule }}"
+  jobTemplate:
+    metadata:
+      {{- with .Values.jobAnnotations }}
+      annotations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      labels:
+        {{- include "fdroid-repo-helm.labels" . | nindent 8 }}
+    spec:
+      template:
+        metadata:
+          {{- with .Values.podAnnotations }}
+          annotations:
+            {{- toYaml . | nindent 12 }}
+          {{- end }}
+          labels:
+            {{- include "fdroid-repo-helm.labels" . | nindent 12 }}
+        spec:
+          restartPolicy: OnFailure
+          {{- with .Values.imagePullSecrets }}
+          imagePullSecrets:
+            {{- toYaml . | nindent 12 }}
+          {{- end }}
+          securityContext:
+            {{- toYaml .Values.podSecurityContext | nindent 12 }}
+          volumes:
+            - name: secret
+              secret:
+                secretName: "{{ include "fdroid-repo-helm.fullname" . }}"
+                defaultMode: 0600
+            - name: metadata
+              emptyDir: {}
+            - name: builds
+              emptyDir: {}
+            - name: repo
+              {{- toYaml .Values.volume | nindent 14 }}
+            - name: tmp
+              emptyDir: {}
+            - name: workdir
+              emptyDir: {}
+          initContainers:
+            - name: git
+              securityContext:
+                {{- toYaml .Values.jobSecurityContext | nindent 16 }}
+              image: "{{ .Values.git.repository }}:{{ .Values.git.tag | default "latest" }}"
+              imagePullPolicy: {{ .Values.git.pullPolicy }}
+              volumeMounts:
+                - mountPath: /metadata
+                  name: metadata
+              args:
+                - "clone"
+                - "{{ .Values.fdroid.metadata }}"
+                - "/metadata"
+              resources:
+                {{- toYaml .Values.resources | nindent 16 }}
+            - name: s3
+              securityContext:
+                {{- toYaml .Values.securityContext | nindent 16 }}
+              image: "{{ .Values.s3.repository }}:{{ .Values.s3.tag | default "latest" }}"
+              imagePullPolicy: {{ .Values.s3.pullPolicy }}
+              env:
+                - name: MC_HOST_s3
+                  valueFrom:
+                    secretKeyRef:
+                      name: {{ include "fdroid-repo-helm.fullname" . }}
+                      key: storage
+              volumeMounts:
+                - mountPath: /builds
+                  name: builds
+                - mountPath: /.mc
+                  name: tmp
+              args:
+                - "mirror"
+                - "{{ .Values.fdroid.builds.path }}"
+                - "/builds"
+                - "--preserve"
+                {{ if .Values.fdroid.builds.exclude }}
+                - "--exclude={{ .Values.fdroid.builds.exclude }}"
+                {{ end }}
+              resources:
+                {{- toYaml .Values.resources | nindent 16 }}
+          containers:
+            - name: build
+              securityContext:
+                {{- toYaml .Values.securityContext | nindent 16 }}
+              image: "{{ .Values.build.repository }}:{{ .Values.build.tag | default .Chart.AppVersion }}"
+              imagePullPolicy: {{ .Values.build.pullPolicy }}
+              workingDir: "/workdir"
+              resources:
+                {{- toYaml .Values.resources | nindent 16 }}
+              volumeMounts:
+                - mountPath: "/metadata"
+                  name: metadata
+                - mountPath: "/builds"
+                  name: builds
+                - mountPath: "/fdroid" # permission error
+                  name: repo
+                - mountPath: "/workdir"
+                  name: workdir # failed to set times
+                - mountPath: "/workdir/keystore.bks"
+                  name: secret
+                  subPath: keystore
+                - mountPath: "/workdir/config.yml"
+                  name: secret
+                  subPath: config # should be 0600
+          {{- with .Values.nodeSelector }}
+          nodeSelector:
+            {{- toYaml . | nindent 12 }}
+          {{- end }}
+          {{- with .Values.affinity }}
+          affinity:
+            {{- toYaml . | nindent 12 }}
+          {{- end }}
+          {{- with .Values.tolerations }}
+          tolerations:
+            {{- toYaml . | nindent 12 }}
+          {{- end }}
diff --git a/fdroid-repo/templates/deployment.yaml b/fdroid-repo/templates/deployment.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..7dd84ab5ef1fefc4287063013818ed454211f185
--- /dev/null
+++ b/fdroid-repo/templates/deployment.yaml
@@ -0,0 +1,68 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "fdroid-repo-helm.fullname" . }}
+  labels:
+    {{- include "fdroid-repo-helm.labels" . | nindent 4 }}
+spec:
+  replicas: {{ .Values.replicaCount }}
+  selector:
+    matchLabels:
+      {{- include "fdroid-repo-helm.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+      {{- with .Values.podAnnotations }}
+      annotations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      labels:
+        {{- include "fdroid-repo-helm.selectorLabels" . | nindent 8 }}
+    spec:
+      {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      securityContext:
+        {{- toYaml .Values.podSecurityContext | nindent 8 }}
+      volumes:
+        - name: repo
+          {{- toYaml .Values.volume | nindent 10 }}
+      containers:
+        - name: {{ .Chart.Name }}
+          securityContext:
+            {{- toYaml .Values.securityContext | nindent 12 }}
+          image: "{{ .Values.nginx.repository }}:{{ .Values.nginx.tag | default "latest" }}"
+          imagePullPolicy: {{ .Values.nginx.pullPolicy }}
+          ports:
+            - name: http
+              containerPort: 80
+              protocol: TCP
+          startupProbe:
+            httpGet:
+              port: http
+              path: /fdroid/repo/index.xml
+          livenessProbe:
+            httpGet:
+              port: http
+              path: /fdroid/repo/index.xml
+          readinessProbe:
+            httpGet:
+              port: http
+              path: /fdroid/repo/index.xml
+          resources:
+            {{- toYaml .Values.resources | nindent 12 }}
+          volumeMounts:
+            - mountPath: "/usr/share/nginx/html/fdroid"
+              name: repo
+      {{- with .Values.nodeSelector }}
+      nodeSelector:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.affinity }}
+      affinity:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.tolerations }}
+      tolerations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
diff --git a/fdroid-repo/templates/ingress.yaml b/fdroid-repo/templates/ingress.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..610625e008a1a681f1d09cf8c05eac97cdcc90a5
--- /dev/null
+++ b/fdroid-repo/templates/ingress.yaml
@@ -0,0 +1,28 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: {{ include "fdroid-repo-helm.fullname" . }}
+  labels:
+    {{- include "fdroid-repo-helm.labels" . | nindent 4 }}
+  annotations:
+    {{- .Values.ingress.annotations | toYaml | nindent 4 }}
+spec:
+  ingressClassName: {{ .Values.ingress.class }}
+  rules:
+    - host: "{{ .Values.ingress.host }}"
+      http:
+        paths:
+          - backend:
+              service:
+                name: {{ include "fdroid-repo-helm.fullname" . }}
+                port:
+                  name: http
+            path: "{{ .Values.ingress.path }}(.*)"
+            pathType: ImplementationSpecific
+          - backend:
+              service:
+                name: {{ include "fdroid-repo-helm.fullname" . }}
+                port:
+                  name: http
+            path: "{{ .Values.ingress.path }}(?:fdroid/repo|fdroid|repo|archive)(/.*|$)"
+            pathType: ImplementationSpecific
diff --git a/fdroid-repo/templates/secret.yaml b/fdroid-repo/templates/secret.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..f012e8ff0f6b67804fe228951ce544605b28d3b0
--- /dev/null
+++ b/fdroid-repo/templates/secret.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: {{ include "fdroid-repo-helm.fullname" . }}
+  labels:
+    {{- include "fdroid-repo-helm.labels" . | nindent 4 }}
+data:
+  keystore: "{{ .Values.fdroid.keystore }}"
+stringData:
+  storage: "https://{{ .Values.fdroid.builds.access_key}}:{{.Values.fdroid.builds.secret_key}}@{{.Values.fdroid.builds.host}}"
+  config: |-
+    {{- toYaml .Values.fdroid.config | nindent 4 }}
+    local_copy_dir: /fdroid/
+    keystore: /workdir/keystore.bks
diff --git a/fdroid-repo/templates/service.yaml b/fdroid-repo/templates/service.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..313ca52e369d3e38c450890b1edadef5aa7da03f
--- /dev/null
+++ b/fdroid-repo/templates/service.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "fdroid-repo-helm.fullname" . }}
+  labels:
+    {{- include "fdroid-repo-helm.labels" . | nindent 4 }}
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+    - port: 80
+      targetPort: http
+      protocol: TCP
+      name: http
+  selector:
+    {{- include "fdroid-repo-helm.selectorLabels" . | nindent 4 }}
diff --git a/fdroid-repo/values.yaml b/fdroid-repo/values.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..6c4208e4b27981324b20ddff14cfc5bfa0c716c8
--- /dev/null
+++ b/fdroid-repo/values.yaml
@@ -0,0 +1,92 @@
+replicaCount: 1
+
+nginx:
+  repository: nginx
+  pullPolicy: IfNotPresent
+  tag: ""
+
+git:
+  repository: alpine/git
+  pullPolicy: IfNotPresent
+  tag: ""
+
+s3:
+  repository: minio/mc
+  pullPolicy: IfNotPresent
+  tag: ""
+
+build:
+  repository: k8r.eu/justjanne/fdroid-repo
+  pullPolicy: Always
+  tag: ""
+
+schedule: "*/15 * * * *"
+
+fdroid:
+  metadata: https://github.com/example/example
+  config:
+      repo_url: "https://repo.example.tld/fdroid/repo"
+      repo_name: "Example F-Droid Repo"
+      repo_icon: "fdroid-icon.png"
+      repo_description: "Example repository"
+
+      archive_url: "https://repo.example.tld/fdroid/archive"
+      archive_name: "Example F-Droid Repo Archive"
+      archive_icon: "fdroid-icon.png"
+      archive_description: "Example repository archive"
+      archive_older: 10
+
+      repo_keyalias: "repo.example.tld"
+      keydname: "CN=repo.example.tld"
+      keypass: "pass"
+      keystorepass: "pass"
+  builds:
+    path: "s3/bucket/folder"
+    host: "s3.example.tld"
+    access_key: "username"
+    secret_key: "token"
+    exclude: ""
+  keystore: "" # base64-encoded keystore
+
+volume: {}
+  #emptyDir: {}
+
+imagePullSecrets: [ ]
+nameOverride: ""
+fullnameOverride: ""
+
+service:
+  type: ClusterIP
+
+ingress:
+  host: "repo.example.tld"
+  path: "/"
+  origins: [ ]
+  class: "nginx"
+  annotations: {}
+
+jobAnnotations: { }
+
+podAnnotations: { }
+
+podSecurityContext:
+  fsGroup: 2000
+
+securityContext: {}
+
+jobSecurityContext:
+  capabilities:
+    drop:
+      - ALL
+  runAsNonRoot: true
+  runAsUser: 1000
+
+resources:
+  limits: {}
+  requests: {}
+
+nodeSelector: { }
+
+tolerations: [ ]
+
+affinity: { }