diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..46478c2b0dc2e0c4e06fb7fc303b2e9fb3a3edc1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,76 @@
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+/.idea/*
+!/.idea/copyright/
+
+### Linux template
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### Windows template
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+### macOS template
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..cd8159dc6b15a2fd45351d4a644a11d5257bb937
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,11 @@
+image:
+  name: alpine/helm:3.8.2
+  entrypoint: [ "/bin/sh", "-c" ]
+variables:
+  HELM_EXPERIMENTAL_OCI: 1
+stages:
+  - lint
+  - release
+include:
+- mailu/pipeline.yml
+- quassel/pipeline.yml
diff --git a/.helmignore b/.helmignore
new file mode 100644
index 0000000000000000000000000000000000000000..0e8a0eb36f4ca2c939201c0d54b5d82a1ea34778
--- /dev/null
+++ b/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/mailu/Chart.yaml b/mailu/Chart.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..ef7e1a5dc8cf4547ceae0968411d3dfc4c3e5581
--- /dev/null
+++ b/mailu/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v2
+name: mailu
+description: Helm Chart for Mailu
+type: application
+version: 1.3.2
+appVersion: "1.9.26"
diff --git a/mailu/pipeline.yml b/mailu/pipeline.yml
new file mode 100644
index 0000000000000000000000000000000000000000..48a68a8036fe1372917c6f70c619fff1ab197cd3
--- /dev/null
+++ b/mailu/pipeline.yml
@@ -0,0 +1,12 @@
+lint-mailu:
+  stage: lint
+  script:
+    - helm lint mailu
+
+release-mailu:
+  stage: release
+  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 mailu repo
diff --git a/mailu/templates/_helpers.tpl b/mailu/templates/_helpers.tpl
new file mode 100644
index 0000000000000000000000000000000000000000..aa230576c1cb88099ce3f53bbab128ad26515fd0
--- /dev/null
+++ b/mailu/templates/_helpers.tpl
@@ -0,0 +1,51 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "mailu-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 "mailu-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 "mailu-helm.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "mailu-helm.labels" -}}
+helm.sh/chart: {{ include "mailu-helm.chart" . }}
+{{ include "mailu-helm.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "mailu-helm.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "mailu-helm.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
diff --git a/mailu/templates/certificate.yaml b/mailu/templates/certificate.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d44186e7d698be877ee5666498e0599f76e70bb5
--- /dev/null
+++ b/mailu/templates/certificate.yaml
@@ -0,0 +1,12 @@
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-tls
+spec:
+  commonName: {{ .Values.certificate.commonName }}
+  dnsNames:
+    {{- toYaml .Values.certificate.hostnames | nindent 4 }}
+  issuerRef:
+    kind: ClusterIssuer
+    name: {{ .Values.certificate.issuer }}
+  secretName: {{ include "mailu-helm.fullname" . }}-tls
diff --git a/mailu/templates/configmap-admin.yaml b/mailu/templates/configmap-admin.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..ef42db6d2e54e8be74b29475e6e0ca63ce6c4d47
--- /dev/null
+++ b/mailu/templates/configmap-admin.yaml
@@ -0,0 +1,71 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-admin
+  labels:
+    component: admin
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+data:
+  start.py: |-
+    #!/usr/bin/python3
+
+    import os
+    import logging as log
+    import sys
+
+    log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "INFO"))
+
+    os.system("flask db upgrade")
+
+    account = os.environ.get("INITIAL_ADMIN_ACCOUNT")
+    domain = os.environ.get("INITIAL_ADMIN_DOMAIN")
+    password = os.environ.get("INITIAL_ADMIN_PW")
+
+    if account is not None and domain is not None and password is not None:
+        mode = os.environ.get("INITIAL_ADMIN_MODE", default="ifmissing")
+        log.info("Creating initial admin accout %s@%s with mode %s", account, domain, mode)
+        os.system("flask mailu admin %s %s '%s' --mode %s" % (account, domain, password, mode))
+
+
+    def test_DNS():
+        import dns.resolver
+        import dns.exception
+        import dns.flags
+        import dns.rdtypes
+        import dns.rdatatype
+        import dns.rdataclass
+        import time
+        # DNS stub configured to do DNSSEC enabled queries
+        resolver = dns.resolver.Resolver()
+        resolver.use_edns(0, dns.flags.DO, 1232)
+        resolver.flags = dns.flags.AD | dns.flags.RD
+        nameservers = resolver.nameservers
+        for ns in nameservers:
+            resolver.nameservers = [ns]
+            while True:
+                try:
+                    result = resolver.resolve('example.org', dns.rdatatype.A, dns.rdataclass.IN, lifetime=10)
+                except Exception as e:
+                    log.critical(
+                        "Your DNS resolver at %s is not working (%s). Please see https://mailu.io/master/faq.html#the-admin-container-won-t-start-and-its-log-says-critical-your-dns-resolver-isn-t-doing-dnssec-validation",
+                        ns, e);
+                else:
+                    if result.response.flags & dns.flags.AD:
+                        break
+                    log.critical(
+                        "Your DNS resolver at %s isn't doing DNSSEC validation; Please see https://mailu.io/master/faq.html#the-admin-container-won-t-start-and-its-log-says-critical-your-dns-resolver-isn-t-doing-dnssec-validation.",
+                        ns)
+                time.sleep(5)
+
+
+    test_DNS()
+
+    start_command = "".join([
+        "gunicorn --threads ", str(os.cpu_count()),
+        " -b :80 ",
+        "--access-logfile - " if (log.root.level <= log.INFO) else "",
+        "--error-logfile - ",
+        "--preload ",
+        "'mailu:create_app()'"])
+
+    os.system(start_command)
diff --git a/mailu/templates/configmap-autodiscover.yaml b/mailu/templates/configmap-autodiscover.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..64e70a210bd474787b31ca4809568a9fa5ee2df9
--- /dev/null
+++ b/mailu/templates/configmap-autodiscover.yaml
@@ -0,0 +1,83 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-autodiscover
+  labels:
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+    component: autodiscover
+data:
+  config-v1.1.xml: |-
+    <?xml version="1.0" encoding="UTF-8"?>
+    <clientConfig version="1.1">
+        <emailProvider id="{{ .Values.config.domain }}">
+            <domain>{{ .Values.config.domain }}</domain>
+            <displayName>{{ .Values.config.siteName }}</displayName>
+            <displayShortName>{{ .Values.config.domain }}</displayShortName>
+            <incomingServer type="imap">
+                <hostname>{{ .Values.config.domain }}</hostname>
+                <port>993</port>
+                <socketType>SSL</socketType>
+                <authentication>password-cleartext</authentication>
+                <username>%EMAILADDRESS%</username>
+            </incomingServer>
+            <outgoingServer type="smtp">
+                <hostname>kuschku.de</hostname>
+                <port>465</port>
+                <socketType>SSL</socketType>
+                <authentication>password-cleartext</authentication>
+                <username>%EMAILADDRESS%</username>
+            </outgoingServer>
+            <documentation url="{{ .Values.admin.host }}{{ .Values.admin.path }}/ui/client">
+                <descr lang="en">Configure your email client</descr>
+            </documentation>
+        </emailProvider>
+    </clientConfig>
+  autodiscover.xml: |-
+    <?xml version="1.0" encoding="utf-8" ?>
+    <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
+        <Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
+            <User>
+              <DisplayName>{{ .Values.config.siteName }}</DisplayName>
+            </User>
+            <Account>
+                <AccountType>email</AccountType>
+                <Action>settings</Action>
+                <ServiceHome>{{ .Values.admin.host }}{{ .Values.admin.path }}</ServiceHome>
+                <Protocol>
+                    <Type>IMAP</Type>
+                    <Server>{{ .Values.config.domain }}</Server>
+                    <Port>993</Port>
+                    <DomainRequired>true</DomainRequired>
+                    <SPA>off</SPA>
+                    <SSL>on</SSL>
+                    <AuthRequired>on</AuthRequired>
+                    <SMTPLast>off</SMTPLast>
+                </Protocol>
+                <Protocol>
+                    <Type>SMTP</Type>
+                    <Server>{{ .Values.config.domain }}</Server>
+                    <Port>465</Port>
+                    <DomainRequired>true</DomainRequired>
+                    <SPA>off</SPA>
+                    <SSL>on</SSL>
+                    <AuthRequired>on</AuthRequired>
+                    <SMTPLast>off</SMTPLast>
+                </Protocol>
+                <Protocol>
+                    <Type>DAV</Type>
+                    <Server>{{ .Values.webdav.host }}{{ .Values.webdav.path }}</Server>
+                    <DomainRequired>true</DomainRequired>
+                    <SPA>off</SPA>
+                    <SSL>on</SSL>
+                    <AuthRequired>on</AuthRequired>
+                </Protocol>
+            </Account>
+        </Response>
+    </Autodiscover>
+  mta-sts.txt: |-
+    version: STSv1
+    mode: enforce
+    {{ range .Values.config.hostnames -}}
+    mx: {{ . }}
+    {{ end -}}
+    max_age: 604800
diff --git a/mailu/templates/configmap-front.yaml b/mailu/templates/configmap-front.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..7b2fac9abd23f3b8528472e0721074b30c6a1502
--- /dev/null
+++ b/mailu/templates/configmap-front.yaml
@@ -0,0 +1,152 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-front
+  labels:
+    component: front
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+data:
+  nginx.conf: |-
+    # Basic configuration
+    user nginx;
+    worker_processes auto;
+    error_log /dev/stderr notice;
+    pid /var/run/nginx.pid;
+    load_module "modules/ngx_mail_module.so";
+
+    events {
+        worker_connections 1024;
+    }
+
+    http {
+        # Standard HTTP configuration with slight hardening
+        include /etc/nginx/mime.types;
+        default_type application/octet-stream;
+        access_log /dev/stdout;
+        sendfile on;
+        keepalive_timeout 65;
+        server_tokens off;
+        absolute_redirect off;
+        resolver {{ "{{" }} RESOLVER }} valid=30s;
+
+        {% if REAL_IP_HEADER %}
+        real_ip_header {{ "{{" }} REAL_IP_HEADER }};
+        {% endif %}
+
+        {% if REAL_IP_FROM %}{% for from_ip in REAL_IP_FROM.split(',') %}
+        set_real_ip_from {{ "{{" }} from_ip }};
+        {% endfor %}{% endif %}
+
+        # Header maps
+        map $http_x_forwarded_proto $proxy_x_forwarded_proto {
+          default $http_x_forwarded_proto;
+          ''      $scheme;
+        }
+    }
+
+    mail {
+        server_name "{{ .Values.config.domain }}";
+        auth_http http://{{ include "mailu-helm.fullname" . }}-admin.{{ .Release.Namespace }}.svc.{{ .Values.clusterSuffix }}/internal/auth/email;
+        proxy_pass_error_message on;
+        resolver {{ "{{" }} RESOLVER }} valid=30s;
+
+        {% if TLS and not TLS_ERROR %}
+        include /etc/nginx/tls.conf;
+        ssl_session_cache shared:SSLMAIL:50m;
+        {% endif %}
+
+        # Advertise real capabilites of backends (postfix/dovecot)
+        smtp_capabilities PIPELINING SIZE {{ "{{" }} MESSAGE_SIZE_LIMIT }} ETRN ENHANCEDSTATUSCODES 8BITMIME DSN;
+        pop3_capabilities TOP UIDL RESP-CODES PIPELINING AUTH-RESP-CODE USER;
+        imap_capabilities IMAP4 IMAP4rev1 UIDPLUS SASL-IR LOGIN-REFERRALS ID ENABLE IDLE LITERAL+;
+
+        # Default SMTP server for the webmail (no encryption, but authentication)
+        server {
+          listen 10025;
+          protocol smtp;
+          smtp_auth plain;
+          auth_http_header Auth-Port 10025;
+        }
+
+        # Default IMAP server for the webmail (no encryption, but authentication)
+        server {
+          listen 10143;
+          protocol imap;
+          smtp_auth plain;
+          auth_http_header Auth-Port 10143;
+        }
+
+        # All other protocols are disabled if TLS is failing
+        {% if not TLS_ERROR %}
+        server {
+          listen 143;
+          listen [::]:143;
+          {% if TLS %}
+          starttls only;
+          {% endif %}
+          protocol imap;
+          imap_auth plain;
+          auth_http_header Auth-Port 143;
+        }
+
+        server {
+          listen 110;
+          listen [::]:110;
+          {% if TLS %}
+          starttls only;
+          {% endif %}
+          protocol pop3;
+          pop3_auth plain;
+          auth_http_header Auth-Port 110;
+        }
+
+        server {
+          listen 587;
+          listen [::]:587;
+          {% if TLS %}
+          starttls only;
+          {% endif %}
+          protocol smtp;
+          smtp_auth plain login;
+          auth_http_header Auth-Port 587;
+        }
+
+        {% if TLS %}
+        server {
+          listen 465 ssl;
+          listen [::]:465 ssl;
+          protocol smtp;
+          smtp_auth plain login;
+          auth_http_header Auth-Port 465;
+        }
+
+        server {
+          listen 993 ssl;
+          listen [::]:993 ssl;
+          protocol imap;
+          imap_auth plain;
+          auth_http_header Auth-Port 993;
+        }
+
+        server {
+          listen 995 ssl;
+          listen [::]:995 ssl;
+          protocol pop3;
+          pop3_auth plain;
+          auth_http_header Auth-Port 995;
+        }
+        {% endif %}
+        {% endif %}
+    }
+  tls.conf: |-
+    ssl_protocols TLSv1.3 TLSv1.2;
+    ssl_prefer_server_ciphers on;
+    ssl_ciphers ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:EECDH+AESGCM:EDH+AESGCM:DHE-RSA-CHACHA20-POLY1305:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS:!RC4:!SEED:!CAMELLIA;
+    ssl_ecdh_curve secp384r1;
+    ssl_session_timeout  10m;
+    ssl_session_cache shared:SSL:10m;
+    ssl_session_tickets off;
+
+    ssl_certificate {{ "{{" }} TLS[0] }};
+    ssl_certificate_key {{ "{{" }} TLS[1] }};
+    ssl_dhparam /conf/dhparam.pem;
diff --git a/mailu/templates/configmap-global.yaml b/mailu/templates/configmap-global.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..76ba6e94ab6d993b0a1e010de06e94ad38dc5abd
--- /dev/null
+++ b/mailu/templates/configmap-global.yaml
@@ -0,0 +1,69 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}
+  labels:
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+data:
+  ADMIN: "{{ .Values.admin.enabled }}"
+  ANTIVIRUS: "none"
+  AUTH_RATELIMIT: "10/minute;1000/hour"
+  BIND_ADDRESS4: "127.0.0.1"
+  BIND_ADDRESS6: "::1"
+  DB_FLAVOR: "{{ .Values.database.flavor }}"
+  DB_HOST: "{{ .Values.database.host }}"
+  DB_NAME: "{{ .Values.database.database }}"
+  DMARC_RUA: "{{ .Values.dmarc.rua }}"
+  DMARC_RUF: "{{ .Values.dmarc.ruf }}"
+  DOMAIN: "{{ .Values.config.domain }}"
+  FETCHMAIL_DELAY: "600"
+  HOSTNAMES: "{{ join "," .Values.config.hostnames }}"
+  KUBERNETES_INGRESS: "true"
+  MESSAGE_SIZE_LIMIT: "{{ .Values.config.messageSizeLimit }}"
+  PASSWORD_SCHEME: "{{ .Values.config.passwordScheme }}"
+  POD_ADDRESS_RANGE: "{{ .Values.config.realIpFrom }}"
+  POSTMASTER: "{{ .Values.config.postmaster }}"
+  QUOTA_STORAGE_URL: "redis://{{ .Values.redis.host }}/1"
+  RATELIMIT_STORAGE_URL: "redis://{{ .Values.redis.host }}/2"
+  REAL_IP_FROM: "{{ .Values.config.realIpFrom }}"
+  REAL_IP_HEADER: "{{ .Values.config.realIpHeader }}"
+  RECIPIENT_DELIMITER: "{{ .Values.config.recipientDelimiter }}"
+  SITENAME: "{{ .Values.config.siteName }}"
+  SUBNET: "{{ .Values.config.subnet }}"
+  SUBNET_EXTERNAL: "{{ .Values.config.subnet_external }}"
+  TLS_FLAVOR: "mail"
+  VERSION: "{{ .Values.image.tag | default .Chart.AppVersion }}"
+  WEBDAV: "radicale"
+  WEBMAIL: "roundcube"
+  WEBSITE: "https://{{ .Values.config.domain }}/"
+  WEB_ADMIN: "{{ .Values.admin.path }}"
+  WEB_WEBMAIL: "{{ .Values.webmail.path }}"
+  WELCOME: "{{ .Values.welcome.enabled }}"
+  WELCOME_SUBJECT: "{{ .Values.welcome.subject }}"
+  WELCOME_BODY: "{{ .Values.welcome.body }}"
+
+  HOST_ADMIN: "{{ include "mailu-helm.fullname" . }}-admin.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}"
+  ADMIN_ADDRESS: "{{ include "mailu-helm.fullname" . }}-admin.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}"
+  HOST_FRONT: "{{ include "mailu-helm.fullname" . }}-front.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}"
+  FRONT_ADDRESS: "{{ include "mailu-helm.fullname" . }}-front.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}"
+  HOST_ANTISPAM_MILTER: "{{ include "mailu-helm.fullname" . }}-antispam.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}"
+  ANTISPAM_MILTER_ADDRESS: "{{ include "mailu-helm.fullname" . }}-antispam.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}:11332"
+  HOST_ANTISPAM_WEBUI: "{{ include "mailu-helm.fullname" . }}-antispam.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}"
+  ANTISPAM_WEBUI_ADDRESS: "{{ include "mailu-helm.fullname" . }}-antispam.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}:11334"
+  HOST_ANTIVIRUS: "{{ include "mailu-helm.fullname" . }}-antivirus.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}"
+  ADDRESS_ANTIVIRUS: "{{ include "mailu-helm.fullname" . }}-antivirus.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}:3310"
+  HOST_AUTHSMTP: "{{ include "mailu-helm.fullname" . }}-smtp.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}"
+  AUTHSMTP_ADDRESS: "{{ include "mailu-helm.fullname" . }}-smtp.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}"
+  HOST_IMAP: "{{ include "mailu-helm.fullname" . }}-imap.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}"
+  IMAP_ADDRESS: "{{ include "mailu-helm.fullname" . }}-imap.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}"
+  LMTP_ADDRESS: "{{ include "mailu-helm.fullname" . }}-imap.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}:2525"
+  HOST_POP3: "{{ include "mailu-helm.fullname" . }}-imap.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}"
+  POP3_ADDRESS: "{{ include "mailu-helm.fullname" . }}-imap.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}"
+  HOST_REDIS: "{{ .Values.redis.host }}"
+  REDIS_ADDRESS: "{{ .Values.redis.host }}"
+  HOST_SMTP: "{{ include "mailu-helm.fullname" . }}-smtp.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}"
+  SMTP_ADDRESS: "{{ include "mailu-helm.fullname" . }}-smtp.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}"
+  HOST_WEBDAV: "{{ include "mailu-helm.fullname" . }}-webdav.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}:5232"
+  WEBDAV_ADDRESS: "{{ include "mailu-helm.fullname" . }}-webmail.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}:5232"
+  HOST_WEBMAIL: "{{ include "mailu-helm.fullname" . }}-webmail.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}"
+  WEBMAIL_ADDRESS: "{{ include "mailu-helm.fullname" . }}-webmail.{{.Release.Namespace}}.svc.{{.Values.clusterSuffix}}"
diff --git a/mailu/templates/configmap-smtp.yaml b/mailu/templates/configmap-smtp.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..fc35d02c62219831918142751d74cf316d7515f8
--- /dev/null
+++ b/mailu/templates/configmap-smtp.yaml
@@ -0,0 +1,43 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-smtp
+  labels:
+    component: smtp
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+data:
+  postfix.cf: |-
+    # General TLS configuration
+    tls_high_cipherlist=ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:EECDH+AESGCM:EDH+AESGCM:DHE-RSA-CHACHA20-POLY1305:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS:!RC4:!SEED:!CAMELLIA
+    tls_preempt_cipherlist=yes
+    tls_ssl_options=NO_COMPRESSION
+
+    # Outgoing TLS is more flexible because 1. not all receiving servers will
+    # support TLS, 2. not all will have and up-to-date TLS stack.
+    smtp_tls_security_level=may
+    smtp_tls_mandatory_protocols=!SSLv2,!SSLv3,!TLSv1
+    smtp_tls_protocols=!SSLv2,!SSLv3,!TLSv1
+    smtpd_tls_security_level=may
+    smtpd_tls_mandatory_protocols=!SSLv2,!SSLv3,!TLSv1
+    smtpd_tls_protocols=!SSLv2,!SSLv3,!TLSv1
+    lmtp_tls_ciphers = high
+    lmtp_tls_mandatory_ciphers = high
+    smtp_tls_ciphers = high
+    smtp_tls_mandatory_ciphers = high
+    smtpd_tls_ciphers = high
+    smtpd_tls_mandatory_ciphers = high
+    # Relayed networks
+    mynetworks=127.0.0.1/32 [::1]/128 {{ .Values.config.subnet }} {{ .Values.config.subnet_external }}/32
+    smtpd_authorized_xclient_hosts={{ .Values.config.subnet }} {{ .Values.config.subnet_external }}/32
+
+    postscreen_upstream_proxy_protocol = haproxy
+    postscreen_upstream_proxy_protocol = haproxy
+    smtpd_tls_key_file=/certs/tls.key
+    smtpd_tls_cert_file=/certs/tls.crt
+    smtpd_use_tls = yes
+    smtp_use_tls = yes
+  postfix.master: |-
+    # expose proxy protocol support
+    10024/inet=10024     inet  n       -       n       -       1       postscreen
+    smtpd/pass=smtpd     pass  -       -       n       -       -       smtpd
+
diff --git a/mailu/templates/daemonset-front.yaml b/mailu/templates/daemonset-front.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..8af3f65e79580c15c67450f294da009ff2229f2b
--- /dev/null
+++ b/mailu/templates/daemonset-front.yaml
@@ -0,0 +1,103 @@
+apiVersion: apps/v1
+kind: DaemonSet
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-front
+  labels:
+    component: front
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+spec:
+  selector:
+    matchLabels:
+      component: front
+      {{- include "mailu-helm.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+      {{- with .Values.podAnnotations }}
+      annotations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      labels:
+        component: front
+        {{- include "mailu-helm.selectorLabels" . | nindent 8 }}
+    spec:
+      {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      # securityContext: Not supported by mailu-nginx for now
+      volumes:
+        - name: tls
+          secret:
+            secretName: {{ include "mailu-helm.fullname" . }}-tls
+            items:
+              - key: tls.crt
+                path: cert.pem
+              - key: tls.key
+                path: key.pem
+        - name: config
+          configMap:
+            name: {{ include "mailu-helm.fullname" . }}-front
+      containers:
+        - name: front
+          # securityContext: Not supported by mailu-nginx for now
+          image: "mailu/nginx:{{ .Values.image.tag | default .Chart.AppVersion }}"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          envFrom:
+            - configMapRef:
+                name: {{ include "mailu-helm.fullname" . }}
+            - secretRef:
+                name: {{ include "mailu-helm.fullname" . }}
+          ports:
+            - containerPort: 110
+              hostPort: 110
+              name: pop3
+              protocol: "TCP"
+            - containerPort: 995
+              hostPort: 995
+              name: pop3s
+              protocol: "TCP"
+            - containerPort: 143
+              hostPort: 143
+              name: imap
+              protocol: "TCP"
+            - containerPort: 993
+              hostPort: 993
+              name: imaps
+              protocol: "TCP"
+            - containerPort: 25
+              hostPort: 25
+              name: smtp
+              protocol: "TCP"
+            - containerPort: 10025
+              hostPort: 10025
+              name: smtp-auth
+              protocol: "TCP"
+            - containerPort: 10143
+              hostPort: 10143
+              name: imap-auth
+              protocol: "TCP"
+            - containerPort: 465
+              hostPort: 465
+              name: smtps
+              protocol: "TCP"
+            - containerPort: 587
+              hostPort: 587
+              name: smtpd
+              protocol: "TCP"
+            - containerPort: 8000
+              hostPort: 8000
+              name: auth
+              protocol: "TCP"
+          resources:
+            {{- toYaml .Values.front.resources | nindent 12 }}
+          volumeMounts:
+            - name: tls
+              mountPath: "/certs"
+            - name: config
+              mountPath: /conf/tls.conf
+              subPath: tls.conf
+            - name: config
+              mountPath: /conf/nginx.conf
+              subPath: nginx.conf
+      hostNetwork: true
+      dnsPolicy: ClusterFirstWithHostNet
diff --git a/mailu/templates/deploy-admin.yaml b/mailu/templates/deploy-admin.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5708112fe753ab9d0fd1950634b3d0e860ae19c2
--- /dev/null
+++ b/mailu/templates/deploy-admin.yaml
@@ -0,0 +1,90 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-admin
+  labels:
+    component: admin
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+spec:
+  replicas: {{ .Values.replicaCount }}
+  selector:
+    matchLabels:
+      component: admin
+      {{- include "mailu-helm.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+      {{- with .Values.podAnnotations }}
+      annotations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      labels:
+        component: admin
+        {{- include "mailu-helm.selectorLabels" . | nindent 8 }}
+    spec:
+      {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      securityContext:
+        {{- toYaml .Values.podSecurityContext | nindent 8 }}
+      volumes:
+        - name: data
+          {{- .Values.volumes.data | nindent 10 }}
+        - name: dkim
+          {{- .Values.volumes.dkim | nindent 10 }}
+        - name: config
+          configMap:
+            name: {{ include "mailu-helm.fullname" . }}-admin
+            defaultMode: 0755
+      containers:
+        - name: admin
+          securityContext:
+            {{- toYaml .Values.securityContext | nindent 12 }}
+          image: "mailu/admin:{{ .Values.image.tag | default .Chart.AppVersion }}"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          envFrom:
+            - configMapRef:
+                name: {{ include "mailu-helm.fullname" . }}
+            - secretRef:
+                name: {{ include "mailu-helm.fullname" . }}
+          env:
+            - name: SUBNET
+              valueFrom:
+                configMapKeyRef:
+                  name: {{ include "mailu-helm.fullname" . }}
+                  key: SUBNET_EXTERNAL
+          ports:
+            - name: "http"
+              containerPort: 80
+              protocol: "TCP"
+          resources:
+            {{- toYaml .Values.admin.resources | nindent 12 }}
+          volumeMounts:
+            - name: data
+              mountPath: "/data"
+            - name: dkim
+              mountPath: "/dkim"
+            - name: config
+              mountPath: "/start.py"
+              subPath: "start.py"
+          startupProbe:
+            httpGet:
+              path: /sso/login
+              port: http
+            periodSeconds: 10
+            failureThreshold: 30
+            timeoutSeconds: 5
+          livenessProbe:
+            httpGet:
+              path: /sso/login
+              port: http
+            periodSeconds: 10
+            failureThreshold: 3
+            timeoutSeconds: 5
+          readinessProbe:
+            httpGet:
+              path: /sso/login
+              port: http
+            periodSeconds: 10
+            failureThreshold: 1
+            timeoutSeconds: 5
diff --git a/mailu/templates/deploy-antispam.yaml b/mailu/templates/deploy-antispam.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..e5b4cafe94e8c78e237b82dee3cac36145bb5d0a
--- /dev/null
+++ b/mailu/templates/deploy-antispam.yaml
@@ -0,0 +1,84 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-antispam
+  labels:
+    component: antispam
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+spec:
+  replicas: {{ .Values.replicaCount }}
+  selector:
+    matchLabels:
+      component: antispam
+      {{- include "mailu-helm.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+      {{- with .Values.podAnnotations }}
+      annotations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      labels:
+        component: antispam
+        {{- include "mailu-helm.selectorLabels" . | nindent 8 }}
+    spec:
+      {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      securityContext:
+        {{- toYaml .Values.podSecurityContext | nindent 8 }}
+      volumes:
+        - name: filter
+          {{- .Values.volumes.filter | nindent 10 }}
+        - name: dkim
+          {{- .Values.volumes.dkim | nindent 10 }}
+        - name: local-config
+          emptyDir: {}
+      containers:
+        - name: antispam
+          securityContext:
+            {{- toYaml .Values.securityContext | nindent 12 }}
+          image: "mailu/rspamd:{{ .Values.image.tag | default .Chart.AppVersion }}"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          envFrom:
+            - configMapRef:
+                name: {{ include "mailu-helm.fullname" . }}
+            - secretRef:
+                name: {{ include "mailu-helm.fullname" . }}
+          ports:
+            - name: antispam
+              containerPort: 11332
+              protocol: "TCP"
+            - name: antispam-http
+              containerPort: 11334
+              protocol: "TCP"
+          resources:
+            {{- toYaml .Values.antispam.resources | nindent 12 }}
+          volumeMounts:
+            - name: filter
+              mountPath: "/var/lib/rspamd"
+            - name: dkim
+              mountPath: "/dkim"
+            - name: local-config
+              mountPath: "/etc/rspamd/local.d"
+          startupProbe:
+            httpGet:
+              path: /
+              port: antispam-http
+            periodSeconds:  10
+            failureThreshold: 90
+            timeoutSeconds: 5
+          livenessProbe:
+            httpGet:
+              path: /
+              port: antispam-http
+            periodSeconds:  10
+            failureThreshold: 90
+            timeoutSeconds: 5
+          readinessProbe:
+            httpGet:
+              path: /
+              port: antispam-http
+            periodSeconds:  10
+            failureThreshold: 90
+            timeoutSeconds: 5
diff --git a/mailu/templates/deploy-autodiscover.yaml b/mailu/templates/deploy-autodiscover.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..9bfa810b37979f7370006b295c9ec9bfb5617733
--- /dev/null
+++ b/mailu/templates/deploy-autodiscover.yaml
@@ -0,0 +1,76 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-autodiscover
+  labels:
+    component: autodiscover
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+spec:
+  replicas: {{ .Values.replicaCount }}
+  selector:
+    matchLabels:
+      component: autodiscover
+      {{- include "mailu-helm.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+      {{- with .Values.podAnnotations }}
+      annotations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      labels:
+        component: autodiscover
+        {{- include "mailu-helm.selectorLabels" . | nindent 8 }}
+    spec:
+      {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      securityContext:
+        {{- toYaml .Values.podSecurityContext | nindent 8 }}
+      volumes:
+        - name: config
+          configMap:
+            name: {{ include "mailu-helm.fullname" . }}-autodiscover
+            items:
+              - key: mta-sts.txt
+                path: ".well-known/mta-sts.txt"
+              - key: config-v1.1.xml
+                path: config-v1.1.xml
+              - key: autodiscover.xml
+                path: autodiscover.xml
+      containers:
+        - name: autodiscover
+          securityContext:
+            {{- toYaml .Values.securityContext | nindent 12 }}
+          image: "nginx:stable-alpine"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          ports:
+            - name: "http"
+              containerPort: 80
+              protocol: "TCP"
+          resources:
+            {{- toYaml .Values.admin.resources | nindent 12 }}
+          volumeMounts:
+            - name: config
+              mountPath: "/usr/share/nginx/html"
+          startupProbe:
+            httpGet:
+              path: /config-v1.1.xml
+              port: http
+            periodSeconds: 10
+            failureThreshold: 30
+            timeoutSeconds: 5
+          livenessProbe:
+            httpGet:
+              path: /config-v1.1.xml
+              port: http
+            periodSeconds: 10
+            failureThreshold: 3
+            timeoutSeconds: 5
+          readinessProbe:
+            httpGet:
+              path: /config-v1.1.xml
+              port: http
+            periodSeconds: 10
+            failureThreshold: 1
+            timeoutSeconds: 5
diff --git a/mailu/templates/deploy-imap.yaml b/mailu/templates/deploy-imap.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..c5df9fa88a25121a77d507e57cf903b1e8c1bfd0
--- /dev/null
+++ b/mailu/templates/deploy-imap.yaml
@@ -0,0 +1,95 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-imap
+  labels:
+    component: imap
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+spec:
+  replicas: {{ .Values.replicaCount }}
+  selector:
+    matchLabels:
+      component: imap
+      {{- include "mailu-helm.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+      {{- with .Values.podAnnotations }}
+      annotations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      labels:
+        component: imap
+        {{- include "mailu-helm.selectorLabels" . | nindent 8 }}
+    spec:
+      {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      securityContext:
+        {{- toYaml .Values.podSecurityContext | nindent 8 }}
+      volumes:
+        - name: data
+          {{- .Values.volumes.data | nindent 10 }}
+        - name: mail
+          {{- .Values.volumes.mail | nindent 10 }}
+      containers:
+        - name: imap
+          securityContext:
+            {{- toYaml .Values.securityContext | nindent 12 }}
+          image: "mailu/dovecot:{{ .Values.image.tag | default .Chart.AppVersion }}"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          envFrom:
+            - configMapRef:
+                name: {{ include "mailu-helm.fullname" . }}
+            - secretRef:
+                name: {{ include "mailu-helm.fullname" . }}
+          ports:
+            - name: imap-auth
+              containerPort: 2102
+              protocol: "TCP"
+            - name: imap-transport
+              containerPort: 2525
+              protocol: "TCP"
+            - name: pop3
+              containerPort: 110
+              protocol: "TCP"
+            - name: imap-default
+              containerPort: 143
+              protocol: "TCP"
+            - name: sieve
+              containerPort: 4190
+              protocol: "TCP"
+          resources:
+            {{- toYaml .Values.imap.resources | nindent 12 }}
+          volumeMounts:
+            - name: data
+              mountPath: "/data"
+            - name: mail
+              mountPath: "/mail"
+          startupProbe:
+            exec:
+              command:
+                - sh
+                - -c
+                - 'echo QUIT|nc localhost 110|grep "Dovecot ready."'
+            periodSeconds:  10
+            failureThreshold: 30
+            timeoutSeconds: 5
+          livenessProbe:
+            exec:
+              command:
+                - sh
+                - -c
+                - 'echo QUIT|nc localhost 110|grep "Dovecot ready."'
+            periodSeconds:  10
+            failureThreshold: 3
+            timeoutSeconds: 5
+          readinessProbe:
+            exec:
+              command:
+                - sh
+                - -c
+                - 'echo QUIT|nc localhost 110|grep "Dovecot ready."'
+            periodSeconds:  10
+            failureThreshold: 1
+            timeoutSeconds: 5
diff --git a/mailu/templates/deploy-smtp.yaml b/mailu/templates/deploy-smtp.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5e331dea830a57257b5a80feac65fb13f7325bcc
--- /dev/null
+++ b/mailu/templates/deploy-smtp.yaml
@@ -0,0 +1,101 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-smtp
+  labels:
+    component: smtp
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+spec:
+  replicas: {{ .Values.replicaCount }}
+  selector:
+    matchLabels:
+      component: smtp
+      {{- include "mailu-helm.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+      {{- with .Values.podAnnotations }}
+      annotations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      labels:
+        component: smtp
+        {{- include "mailu-helm.selectorLabels" . | nindent 8 }}
+    spec:
+      {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      securityContext:
+        {{- toYaml .Values.podSecurityContext | nindent 8 }}
+      volumes:
+        - name: data
+          {{- .Values.volumes.data | nindent 10 }}
+        - name: certs
+          secret:
+            secretName: {{ include "mailu-helm.fullname" . }}-tls
+        - name: config
+          configMap:
+            name: {{ include "mailu-helm.fullname" . }}-smtp
+      containers:
+        - name: smtp
+          securityContext:
+            {{- toYaml .Values.securityContext | nindent 12 }}
+          image: "mailu/postfix:{{ .Values.image.tag | default .Chart.AppVersion }}"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          envFrom:
+            - configMapRef:
+                name: {{ include "mailu-helm.fullname" . }}
+            - secretRef:
+                name: {{ include "mailu-helm.fullname" . }}
+          ports:
+          - name: smtp
+            containerPort: 25
+            protocol: "TCP"
+          - name: smtp-ssl
+            containerPort: 465
+            protocol: "TCP"
+          - name: smtp-starttls
+            containerPort: 587
+            protocol: "TCP"
+          - name: smtp-proxy
+            containerPort: 10024
+            protocol: "TCP"
+          - name: smtp-auth
+            containerPort: 10025
+            protocol: "TCP"
+          resources:
+            {{- toYaml .Values.smtp.resources | nindent 12 }}
+          volumeMounts:
+            - name: data
+              mountPath: "/data"
+            - name: certs
+              mountPath: "/certs"
+            - name: config
+              mountPath: "/overrides"
+          startupProbe:
+            exec:
+              command:
+                - sh
+                - -c
+                - 'echo QUIT|nc localhost 25|grep "220 .* ESMTP Postfix"'
+            periodSeconds:  10
+            failureThreshold: 30
+            timeoutSeconds: 5
+          livenessProbe:
+            exec:
+              command:
+                - sh
+                - -c
+                - 'echo QUIT|nc localhost 25|grep "220 .* ESMTP Postfix"'
+            periodSeconds:  10
+            failureThreshold: 3
+            timeoutSeconds: 5
+          readinessProbe:
+            exec:
+              command:
+                - sh
+                - -c
+                - 'echo QUIT|nc localhost 25|grep "220 .* ESMTP Postfix"'
+            periodSeconds:  10
+            failureThreshold: 1
+            timeoutSeconds: 5
diff --git a/mailu/templates/deploy-webdav.yaml b/mailu/templates/deploy-webdav.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..dc9710d1093acc814a2fe3d00acd23dd937e7a29
--- /dev/null
+++ b/mailu/templates/deploy-webdav.yaml
@@ -0,0 +1,73 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-webdav
+  labels:
+    component: webdav
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+spec:
+  replicas: {{ .Values.replicaCount }}
+  selector:
+    matchLabels:
+      component: webdav
+      {{- include "mailu-helm.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+      {{- with .Values.podAnnotations }}
+      annotations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      labels:
+        component: webdav
+        {{- include "mailu-helm.selectorLabels" . | nindent 8 }}
+    spec:
+      {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      securityContext:
+        {{- toYaml .Values.podSecurityContext | nindent 8 }}
+      volumes:
+        - name: webdav
+          {{- .Values.volumes.webdav | nindent 10 }}
+      containers:
+        - name: webdav
+          securityContext:
+            {{- toYaml .Values.securityContext | nindent 12 }}
+          image: "mailu/radicale:{{ .Values.image.tag | default .Chart.AppVersion }}"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          envFrom:
+            - configMapRef:
+                name: {{ include "mailu-helm.fullname" . }}
+            - secretRef:
+                name: {{ include "mailu-helm.fullname" . }}
+          ports:
+            - name: "http"
+              containerPort: 5232
+              protocol: "TCP"
+          resources:
+            {{- toYaml .Values.webdav.resources | nindent 12 }}
+          volumeMounts:
+            - name: webdav
+              mountPath: "/data"
+          startupProbe:
+            httpGet:
+              path: /
+              port: http
+            periodSeconds: 10
+            failureThreshold: 30
+            timeoutSeconds: 5
+          livenessProbe:
+            httpGet:
+              path: /
+              port: http
+            periodSeconds: 10
+            failureThreshold: 3
+            timeoutSeconds: 5
+          readinessProbe:
+            httpGet:
+              path: /
+              port: http
+            periodSeconds: 10
+            failureThreshold: 1
+            timeoutSeconds: 5
diff --git a/mailu/templates/deploy-webmail.yaml b/mailu/templates/deploy-webmail.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..8971f8644a13f38a8fa6778dbb53f4404f9997ee
--- /dev/null
+++ b/mailu/templates/deploy-webmail.yaml
@@ -0,0 +1,79 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-webmail
+  labels:
+    component: webmail
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+spec:
+  replicas: {{ .Values.replicaCount }}
+  selector:
+    matchLabels:
+      component: webmail
+      {{- include "mailu-helm.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+      {{- with .Values.podAnnotations }}
+      annotations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      labels:
+        component: webmail
+        {{- include "mailu-helm.selectorLabels" . | nindent 8 }}
+    spec:
+      {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      securityContext:
+        {{- toYaml .Values.podSecurityContext | nindent 8 }}
+      volumes:
+        - name: webmail
+          {{- .Values.volumes.webmail | nindent 10 }}
+      containers:
+        - name: webmail
+          securityContext:
+            {{- toYaml .Values.securityContext | nindent 12 }}
+          image: "k8r.eu/justjanne/mailu-snappymail:{{ .Values.webmail.tag | default .Chart.AppVersion }}"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          envFrom:
+            - configMapRef:
+                name: {{ include "mailu-helm.fullname" . }}
+            - secretRef:
+                name: {{ include "mailu-helm.fullname" . }}
+          env:
+            - name: HOST_FRONT
+              valueFrom:
+                configMapKeyRef:
+                  key: FRONT_ADDRESS
+                  name: {{ include "mailu-helm.fullname" . }}
+          ports:
+            - name: "http"
+              containerPort: 80
+              protocol: "TCP"
+          resources:
+            {{- toYaml .Values.webmail.resources | nindent 12 }}
+          volumeMounts:
+            - name: webmail
+              mountPath: "/data"
+          startupProbe:
+            httpGet:
+              path: /healthz
+              port: http
+            periodSeconds: 10
+            failureThreshold: 30
+            timeoutSeconds: 5
+          livenessProbe:
+            httpGet:
+              path: /healthz
+              port: http
+            periodSeconds: 10
+            failureThreshold: 3
+            timeoutSeconds: 5
+          readinessProbe:
+            httpGet:
+              path: /healthz
+              port: http
+            periodSeconds: 10
+            failureThreshold: 1
+            timeoutSeconds: 5
diff --git a/mailu/templates/ingress-admin.yaml b/mailu/templates/ingress-admin.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..72c3641f9a62ef07712776bd31ed482c9747e1c3
--- /dev/null
+++ b/mailu/templates/ingress-admin.yaml
@@ -0,0 +1,41 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-admin
+  labels:
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+    component: admin
+  annotations:
+    nginx.ingress.kubernetes.io/server-snippet: |-
+      location @login {
+        return 302 "/sso/login";
+      }
+    {{- range $key, $value := .Values.ingress.annotations }}
+    {{- printf "%s: %s" $key (tpl $value $ | quote) | nindent 4 }}
+    {{- end }}
+spec:
+  rules:
+    - host: "{{ .Values.admin.host }}"
+      http:
+        paths:
+          - path: "{{ .Values.admin.path }}"
+            backend:
+              service:
+                name: {{ include "mailu-helm.fullname" . }}-admin
+                port:
+                  name: http
+            pathType: Prefix
+          - path: "/sso/"
+            backend:
+              service:
+                name: {{ include "mailu-helm.fullname" . }}-admin
+                port:
+                  name: http
+            pathType: Prefix
+          - path: "/static"
+            backend:
+              service:
+                name: {{ include "mailu-helm.fullname" . }}-admin
+                port:
+                  name: http
+            pathType: Prefix
diff --git a/mailu/templates/ingress-antispam.yaml b/mailu/templates/ingress-antispam.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..930207c3ab7d9764ed544ae74fe8ac96866124c4
--- /dev/null
+++ b/mailu/templates/ingress-antispam.yaml
@@ -0,0 +1,29 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-antispam
+  labels:
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+    component: antispam
+  annotations:
+    nginx.ingress.kubernetes.io/auth-url:
+      "http://{{ include "mailu-helm.fullname" . }}-admin.{{ .Release.Namespace }}.svc.{{ .Values.clusterSuffix }}/internal/auth/admin"
+    nginx.ingress.kubernetes.io/configuration-snippet: |
+      proxy_set_header X-Real-IP "";
+      proxy_set_header X-Forwarded-For "";
+      proxy_set_header Password "mailu";
+    {{- range $key, $value := .Values.ingress.annotations }}
+    {{- printf "%s: %s" $key (tpl $value $ | quote) | nindent 4 }}
+    {{- end }}
+spec:
+  rules:
+    - host: "{{ .Values.admin.host }}"
+      http:
+        paths:
+          - path: "{{ .Values.admin.path }}/antispam($|/)(.*)"
+            backend:
+              service:
+                name: {{ include "mailu-helm.fullname" . }}-antispam
+                port:
+                  name: antispam-http
+            pathType: Prefix
diff --git a/mailu/templates/ingress-autodiscover.yaml b/mailu/templates/ingress-autodiscover.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..3aca552ca9679eba1dc3e1292681aaab10480fd6
--- /dev/null
+++ b/mailu/templates/ingress-autodiscover.yaml
@@ -0,0 +1,75 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-autodiscover
+  labels:
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+    component: autodiscover
+  annotations:
+    nginx.ingress.kubernetes.io/cache-enable: "true"
+    nginx.ingress.kubernetes.io/cache-generation: "2"
+    nginx.ingress.kubernetes.io/cache-whitelist-query-params: ""
+    nginx.ingress.kubernetes.io/configuration-snippet: |-
+      rewrite ^/.well-known/(carddav|caldav)$ "https://{{ .Values.webdav.host }}{{ .Values.webdav.path }}/.well-known/$1" permanent;
+      rewrite ^/mail/(.*) /$1 last;
+    {{- range $key, $value := .Values.ingress.annotations }}
+    {{- printf "%s: %s" $key (tpl $value $ | quote) | nindent 4 }}
+    {{- end }}
+spec:
+  rules:
+    {{ range .Values.config.hostnames }}
+    - host: {{ . }}
+      http:
+        paths:
+          - path: "/.well-known/carddav"
+            backend:
+              service:
+                name: {{ include "mailu-helm.fullname" $ }}-autodiscover
+                port:
+                  name: http
+            pathType: Prefix
+          - path: "/.well-known/caldav"
+            backend:
+              service:
+                name: {{ include "mailu-helm.fullname" $ }}-autodiscover
+                port:
+                  name: http
+            pathType: Prefix
+          - path: "/.well-known/mta-sts.txt"
+            backend:
+              service:
+                name: {{ include "mailu-helm.fullname" $ }}-autodiscover
+                port:
+                  name: http
+            pathType: Prefix
+    {{ end }}
+    - host: mta-sts.{{ .Values.config.domain }}
+      http:
+        paths:
+          - path: "/.well-known/mta-sts.txt"
+            backend:
+              service:
+                name: {{ include "mailu-helm.fullname" $ }}-autodiscover
+                port:
+                  name: http
+            pathType: Prefix
+    - host: "autodiscover.{{ .Values.config.domain }}"
+      http:
+        paths:
+          - path: "/"
+            backend:
+              service:
+                name: {{ include "mailu-helm.fullname" . }}-autodiscover
+                port:
+                  name: http
+            pathType: Prefix
+    - host: "autoconfig.{{ .Values.config.domain }}"
+      http:
+        paths:
+          - path: "/"
+            backend:
+              service:
+                name: {{ include "mailu-helm.fullname" . }}-autodiscover
+                port:
+                  name: http
+            pathType: Prefix
diff --git a/mailu/templates/ingress-webdav.yaml b/mailu/templates/ingress-webdav.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..040fb327545b45ffc5c103f10f316fb35faaf088
--- /dev/null
+++ b/mailu/templates/ingress-webdav.yaml
@@ -0,0 +1,30 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-webdav
+  labels:
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+    component: webdav
+  annotations:
+    nginx.ingress.kubernetes.io/auth-url:
+      "http://{{ include "mailu-helm.fullname" . }}-admin.{{ .Release.Namespace }}.svc.{{ .Values.clusterSuffix }}/internal/auth/basic"
+    nginx.ingress.kubernetes.io/configuration-snippet: |-
+      auth_request_set $user $upstream_http_x_user;
+      proxy_set_header X-Remote-User $user;
+      proxy_set_header X-Script-Name "{{ .Values.webdav.path }}";
+    nginx.ingress.kubernetes.io/rewrite-target: /$2
+    {{- range $key, $value := .Values.ingress.annotations }}
+    {{- printf "%s: %s" $key (tpl $value $ | quote) | nindent 4 }}
+    {{- end }}
+spec:
+  rules:
+    - host: "{{ .Values.webdav.host }}"
+      http:
+        paths:
+          - path: "{{ .Values.webdav.path }}(/|$)(.*)"
+            backend:
+              service:
+                name: {{ include "mailu-helm.fullname" . }}-webdav
+                port:
+                  name: http
+            pathType: Prefix
diff --git a/mailu/templates/ingress-webmail.yaml b/mailu/templates/ingress-webmail.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5bd1839e819b14a347ebeeedc5fafb14d385944d
--- /dev/null
+++ b/mailu/templates/ingress-webmail.yaml
@@ -0,0 +1,31 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-webmail
+  labels:
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+    component: webmail
+  annotations:
+    nginx.ingress.kubernetes.io/auth-url:
+      "http://{{ include "mailu-helm.fullname" . }}-admin.{{ .Release.Namespace }}.svc.{{ .Values.clusterSuffix }}/internal/auth/user"
+    nginx.ingress.kubernetes.io/configuration-snippet: |-
+      auth_request_set $user $upstream_http_x_user;
+      proxy_set_header 'X-Remote-User' $user;
+      auth_request_set $token $upstream_http_x_user_token;
+      proxy_set_header 'X-Remote-User-Token' $token;
+      error_page 403 @login;
+    {{- range $key, $value := .Values.ingress.annotations }}
+    {{- printf "%s: %s" $key (tpl $value $ | quote) | nindent 4 }}
+    {{- end }}
+spec:
+  rules:
+    - host: "{{ .Values.webmail.host }}"
+      http:
+        paths:
+          - path: "{{ .Values.webmail.path }}"
+            backend:
+              service:
+                name: {{ include "mailu-helm.fullname" . }}-webmail
+                port:
+                  name: http
+            pathType: Prefix
diff --git a/mailu/templates/secret-global.yaml b/mailu/templates/secret-global.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..be0561531497091f1594133684768e6e68cfbfbc
--- /dev/null
+++ b/mailu/templates/secret-global.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}
+  labels:
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+stringData:
+  SECRET_KEY: "{{ .Values.config.secretKey }}"
+  DB_PW: "{{ .Values.database.password }}"
+  DB_USER: "{{ .Values.database.username }}"
diff --git a/mailu/templates/service-admin.yaml b/mailu/templates/service-admin.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..c715a7dcc442585b5c0e054a1564a44ef9b1c68b
--- /dev/null
+++ b/mailu/templates/service-admin.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-admin
+  labels:
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+    component: admin
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+    - port: 80
+      targetPort: http
+      protocol: "TCP"
+      name: http
+  selector:
+    {{- include "mailu-helm.selectorLabels" . | nindent 4 }}
+    component: admin
diff --git a/mailu/templates/service-antispam.yaml b/mailu/templates/service-antispam.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a16fcd602cb83d4bbba7a3726c03ab53d62dabf8
--- /dev/null
+++ b/mailu/templates/service-antispam.yaml
@@ -0,0 +1,21 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-antispam
+  labels:
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+    component: antispam
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+    - port: 11332
+      targetPort: antispam
+      protocol: "TCP"
+      name: antispam
+    - port: 11334
+      targetPort: antispam-http
+      protocol: "TCP"
+      name: antispam-http
+  selector:
+    {{- include "mailu-helm.selectorLabels" . | nindent 4 }}
+    component: antispam
diff --git a/mailu/templates/service-autodiscover.yaml b/mailu/templates/service-autodiscover.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..7b7e33819d989bfd4f15d029fcb371d8b288096b
--- /dev/null
+++ b/mailu/templates/service-autodiscover.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-autodiscover
+  labels:
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+    component: autodiscover
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+    - port: 80
+      targetPort: http
+      protocol: "TCP"
+      name: http
+  selector:
+    {{- include "mailu-helm.selectorLabels" . | nindent 4 }}
+    component: autodiscover
diff --git a/mailu/templates/service-front.yaml b/mailu/templates/service-front.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..13022d1c1bf96078f164070514b02c7b2b1aab25
--- /dev/null
+++ b/mailu/templates/service-front.yaml
@@ -0,0 +1,49 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-front
+  labels:
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+    component: front
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+    - name: pop3
+      port: 110
+      protocol: TCP
+      targetPort: pop3
+    - name: pop3s
+      port: 995
+      protocol: TCP
+      targetPort: pop3s
+    - name: imap
+      port: 143
+      protocol: TCP
+      targetPort: imap
+    - name: imaps
+      port: 993
+      protocol: TCP
+      targetPort: imaps
+    - name: smtp
+      port: 25
+      protocol: TCP
+      targetPort: smtp
+    - name: smtps
+      port: 465
+      protocol: TCP
+      targetPort: smtps
+    - name: smtpd
+      port: 587
+      protocol: TCP
+      targetPort: smtpd
+    - name: smtp-auth
+      port: 10025
+      protocol: TCP
+      targetPort: smtp-auth
+    - name: imap-auth
+      port: 10143
+      protocol: TCP
+      targetPort: imap-auth
+  selector:
+    {{- include "mailu-helm.selectorLabels" . | nindent 4 }}
+    component: front
diff --git a/mailu/templates/service-imap.yaml b/mailu/templates/service-imap.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..87f92c55d9fa5649343a011fdecd1d6239dbaf65
--- /dev/null
+++ b/mailu/templates/service-imap.yaml
@@ -0,0 +1,33 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-imap
+  labels:
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+    component: imap
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+    - name: imap-auth
+      port: 2102
+      protocol: "TCP"
+      targetPort: imap-auth
+    - name: imap-transport
+      port: 2525
+      protocol: "TCP"
+      targetPort: imap-transport
+    - name: pop3
+      port: 110
+      protocol: "TCP"
+      targetPort: pop3
+    - name: imap-default
+      port: 143
+      protocol: "TCP"
+      targetPort: imap-default
+    - name: sieve
+      port: 4190
+      protocol: "TCP"
+      targetPort: sieve
+  selector:
+    {{- include "mailu-helm.selectorLabels" . | nindent 4 }}
+    component: imap
diff --git a/mailu/templates/service-smtp.yaml b/mailu/templates/service-smtp.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a8aa6cdd08e01499eb0c1595464331641b34bf76
--- /dev/null
+++ b/mailu/templates/service-smtp.yaml
@@ -0,0 +1,33 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-smtp
+  labels:
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+    component: smtp
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+    - name: smtp
+      port: 25
+      protocol: "TCP"
+      targetPort: smtp
+    - name: smtp-ssl
+      port: 465
+      protocol: "TCP"
+      targetPort: smtp-ssl
+    - name: smtp-starttls
+      port: 587
+      protocol: "TCP"
+      targetPort: smtp-starttls
+    - name: smtp-auth
+      port: 10025
+      protocol: "TCP"
+      targetPort: smtp-auth
+    - name: smtp-proxy
+      port: 10024
+      protocol: "TCP"
+      targetPort: smtp-proxy
+  selector:
+    {{- include "mailu-helm.selectorLabels" . | nindent 4 }}
+    component: smtp
diff --git a/mailu/templates/service-webdav.yaml b/mailu/templates/service-webdav.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..8c21ea81cba005bfafb0083b8e55b7cca0db060e
--- /dev/null
+++ b/mailu/templates/service-webdav.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-webdav
+  labels:
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+    component: webdav
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+    - name: http
+      port: 80
+      protocol: "TCP"
+      targetPort: http
+  selector:
+    {{- include "mailu-helm.selectorLabels" . | nindent 4 }}
+    component: webdav
diff --git a/mailu/templates/service-webmail.yaml b/mailu/templates/service-webmail.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..2c3c3dbb9af5f78b48c54c5d361b7cff4fa7b58f
--- /dev/null
+++ b/mailu/templates/service-webmail.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "mailu-helm.fullname" . }}-webmail
+  labels:
+    {{- include "mailu-helm.labels" . | nindent 4 }}
+    component: webmail
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+    - name: http
+      port: 80
+      protocol: "TCP"
+      targetPort: http
+  selector:
+    {{- include "mailu-helm.selectorLabels" . | nindent 4 }}
+    component: webmail
diff --git a/mailu/values.yaml b/mailu/values.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..683df70eb95334005af38bb953a33f01adacb35f
--- /dev/null
+++ b/mailu/values.yaml
@@ -0,0 +1,159 @@
+replicaCount: 1
+
+imagePullSecrets: [ ]
+nameOverride: ""
+fullnameOverride: ""
+clusterSuffix: "cluster.local"
+
+image:
+  pullPolicy: IfNotPresent
+  tag: ""
+
+config:
+  secretKey: "changeMe"
+  domain: "example.com"
+  hostnames:
+  - "example.com"
+  - "mail.example.com"
+  - "imap.example.com"
+  passwordScheme: "PBKDF2"
+  messageSizeLimit: "500000000"
+  realIpFrom: "0.0.0.0/0"
+  realIpHeader: "X-Forwarded-For"
+  postmaster: "postmaster"
+  recipientDelimiter: "+"
+  siteName: "Example.com Mail"
+  subnet: "10.42.0.0/16"
+  subnet_external: "1.2.3.4"
+
+welcome:
+  enabled: false
+  subject: "Welcome to your new email account"
+  body: "Welcome to your new email account, if you can read this, then it is configured properly!"
+
+dmarc:
+  rua: "dmarc"
+  ruf: "dmarc"
+
+database:
+  flavor: "sqlite"
+  host: "external-db-hostname"
+  database: "mailu"
+  username: "mailu"
+  password: "chang3m3!"
+
+redis:
+  host: "external-redis-hostname"
+
+certificate:
+  issuer: "letsencrypt"
+  commonName: "example.com"
+  hostnames:
+    - "example.com"
+    - "imap.example.com"
+    - "mail.example.com"
+
+volumes:
+  dkim: |-
+    emptyDir: {}
+  data: |-
+    emptyDir: {}
+  mail: |-
+    emptyDir: {}
+  filter: |-
+    emptyDir: {}
+  webdav: |-
+    emptyDir: {}
+  webmail: |-
+    emptyDir: {}
+
+front:
+  resources:
+    limits:
+      cpu: 200m
+      memory: 200Mi
+    requests:
+      cpu: 100m
+      memory: 100Mi
+admin:
+  enabled: true
+  host: "mail.example.com"
+  path: "/admin"
+  resources:
+    limits:
+      cpu: 500m
+      memory: 512Mi
+    requests:
+      cpu: 500m
+      memory: 128Mi
+imap:
+  resources:
+    limits:
+      cpu: 1
+      memory: 1Gi
+    requests:
+      cpu: 100m
+      memory: 500Mi
+smtp:
+  resources:
+    limits:
+      cpu: 500m
+      memory: 2Gi
+    requests:
+      cpu: 100m
+      memory: 1Gi
+antispam:
+  password: "chang3m3!"
+  resources:
+    limits:
+      cpu: 1
+      memory: 2Gi
+    requests:
+      cpu: 1
+      memory: 1Gi
+webmail:
+  host: "mail.example.com"
+  path: "/"
+  tag: ""
+  resources:
+    limits:
+      cpu: 1
+      memory: 1Gi
+    requests:
+      cpu: 100m
+      memory: 500Mi
+webdav:
+  host: "mail.example.com"
+  path: "/webdav"
+  resources:
+    limits:
+      cpu: 1
+      memory: 1Gi
+    requests:
+      cpu: 100m
+      memory: 500Mi
+
+podAnnotations: { }
+
+podSecurityContext: { }
+#  fsGroup: 2000
+
+securityContext: { }
+#  capabilities:
+#    drop:
+#      - ALL
+#  readOnlyRootFilesystem: true
+#  runAsNonRoot: true
+#  runAsUser: 1000
+
+service:
+  type: ClusterIP
+
+nodeSelector: { }
+
+tolerations: [ ]
+
+affinity: { }
+
+ingress:
+  annotations: {}
diff --git a/quassel/Chart.yaml b/quassel/Chart.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..45d5ee5eb39c8ec27d28ca34970f416422eb381f
--- /dev/null
+++ b/quassel/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v2
+name: quassel
+description: Helm Chart for Quassel-Core
+type: application
+version: 1.0.0
+appVersion: "v0.14.0"
diff --git a/quassel/README.md b/quassel/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..fa2c11f6288c7492ce13554b0557b902daa9f8e7
--- /dev/null
+++ b/quassel/README.md
@@ -0,0 +1,72 @@
+# Helm Chart for Quassel Core
+
+## TL;DR
+
+```console
+$ helm repo add justjanne https://git.kuschku.de/api/v4/projects/72/packages/helm/stable
+$ helm install -f values.yaml quassel-core --set fullnameOverride=quassel-core justjanne/quassel-helm
+```
+
+## Introduction
+
+This chart installs a [Quassel Core] deployment on a [Kubernetes] cluster using 
+the [Helm] package manager.
+
+It supports the following architectures: `x86-64`, `aarch64`, `armhf`.  
+For non-x86_64 deployments, use `--set image.tag=v0.14.0-aarch64` (or 
+`v0.14.0-armhf` accordingly)
+
+## Installing the Chart
+
+To install the chart with the release name `my-release`:
+
+```console
+$ helm install my-release justjanne/quassel-helm
+```
+
+The command deploys Quassel Core on the Kubernetes cluster in the default 
+configuration. See [values.yaml] for available configuration options.
+
+## Exposing the Chart
+
+You’ll have to configure your ingress controller, load balancer, or an
+equivalent resource to expose the quassel service from your cluster.
+
+With the [NGINX Ingress Controller], you’d  add the following line to your 
+[TCP service map]:
+
+```diff
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: tcp-services
+  namespace: ingress-nginx
+data:
+  9000: "default/example-go:8080"
++   4242: "default/quassel-helm-my-release:4242"
+```
+
+If using the proxy protocol, you’d add the "::PROXY" suffix.
+
+If using a different ingress controller, load balancer, etc., please follow
+their documentation for how to expose TCP services.
+
+## Uninstalling the Chart
+
+To uninstall/delete the `my-release` deployment:
+
+```console
+$ helm delete my-release
+```
+
+The command removes all the Kubernetes components associated with the chart and 
+deletes the release.
+
+---
+
+[Quassel Core]: https://quassel-irc.org/
+[Kubernetes]: https://kubernetes.io
+[Helm]: https://helm.sh
+[values.yaml]: https://git.kuschku.de/justJanne/quassel-helm/-/blob/main/values.yaml
+[NGINX Ingress Controller]: https://kubernetes.github.io/ingress-nginx/
+[TCP service map]: https://kubernetes.github.io/ingress-nginx/user-guide/exposing-tcp-udp-services/#exposing-tcp-and-udp-services
diff --git a/quassel/pipeline.yml b/quassel/pipeline.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3bff89aa29cd00fadb0578f262b0965360f6fe89
--- /dev/null
+++ b/quassel/pipeline.yml
@@ -0,0 +1,13 @@
+lint-quassel:
+  stage: lint
+  script:
+    - helm lint quassel
+
+release-quassel:
+  stage: release
+  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 quassel repo
+
diff --git a/quassel/templates/_helpers.tpl b/quassel/templates/_helpers.tpl
new file mode 100644
index 0000000000000000000000000000000000000000..1fbbfa292a8ab616a26dec3cc65c5cd88082e806
--- /dev/null
+++ b/quassel/templates/_helpers.tpl
@@ -0,0 +1,56 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "quassel-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 "quassel-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 "quassel-helm.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "quassel-helm.labels" -}}
+helm.sh/chart: {{ include "quassel-helm.chart" . }}
+{{ include "quassel-helm.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "quassel-helm.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "quassel-helm.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+
+{{- define "quassel-helm.sslPath" -}}
+/certs
+{{- end }}
diff --git a/quassel/templates/configmap.yaml b/quassel/templates/configmap.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..64c71632c59a80e530ef49809b76a1c6bacac198
--- /dev/null
+++ b/quassel/templates/configmap.yaml
@@ -0,0 +1,24 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ include "quassel-helm.fullname" . }}
+  labels:
+    {{- include "quassel-helm.labels" . | nindent 4 }}
+data:
+  NORESTORE: "{{ .Values.config.noRestore }}"
+  DB_BACKEND: {{ .Values.config.database }}
+  {{ if eq .Values.config.database "PostgreSQL" }}
+  DB_PGSQL_HOSTNAME: {{ .Values.config.postgres.hostname }}
+  DB_PGSQL_PORT: "{{ .Values.config.postgres.port }}"
+  DB_PGSQL_DATABASE: {{ .Values.config.postgres.database }}
+  DB_PGSQL_USERNAME: {{ .Values.config.postgres.username }}
+  {{ end }}
+  AUTH_AUTHENTICATOR: {{ .Values.config.authenticator }}
+  {{ if eq .Values.config.authenticator "Ldap" }}
+  AUTH_LDAP_HOSTNAME: {{ .Values.config.ldap.hostname }}
+  AUTH_LDAP_PORT: "{{ .Values.config.ldap.port }}"
+  AUTH_LDAP_BIND_DN: {{ .Values.config.ldap.bind_dn }}
+  AUTH_LDAP_BASE_DN: {{ .Values.config.ldap.base_dn }}
+  AUTH_LDAP_FILTER: {{ .Values.config.ldap.filter }}
+  AUTH_LDAP_UID_ATTRIBUTE: {{ .Values.config.ldap.uid_attribute }}
+  {{ end }}
diff --git a/quassel/templates/deployment.yaml b/quassel/templates/deployment.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..69b59f4554f739ed8f2e7072e0f8b26c57cef59a
--- /dev/null
+++ b/quassel/templates/deployment.yaml
@@ -0,0 +1,164 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "quassel-helm.fullname" . }}
+  labels:
+    {{- include "quassel-helm.labels" . | nindent 4 }}
+spec:
+  replicas: {{ .Values.replicaCount }}
+  selector:
+    matchLabels:
+      {{- include "quassel-helm.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+      {{- with .Values.podAnnotations }}
+      annotations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      labels:
+        {{- include "quassel-helm.selectorLabels" . | nindent 8 }}
+    spec:
+      {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      securityContext:
+        {{- toYaml .Values.podSecurityContext | nindent 8 }}
+      volumes:
+        - name: config
+          {{- toYaml .Values.volume | nindent 10 }}
+      {{ if .Values.ssl.enabled }}
+        - name: ssl
+          secret:
+            secretName: {{ .Values.ssl.secret.name }}
+      {{ end }}
+      containers:
+        {{ if and .Values.ssl.enabled .Values.ssl.certChecker.enabled }}
+        - name: cert-checker
+          securityContext:
+            {{- toYaml .Values.securityContext | nindent 12 }}
+          env:
+            - name: CONFIG_DIR
+              value: /certs
+          image: "{{ .Values.ssl.certChecker.repository }}:{{ .Values.ssl.certChecker.tag }}"
+          imagePullPolicy: {{ .Values.ssl.certChecker.pullPolicy }}
+          resources:
+            {{- toYaml .Values.resources | nindent 12 }}
+          volumeMounts:
+            - mountPath: /certs
+              name: ssl
+        {{ end }}
+        - name: {{ .Chart.Name }}
+          securityContext:
+            {{- toYaml .Values.securityContext | nindent 12 }}
+          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          env:
+            - name: CONFIG_DIR
+              value: "/config"
+          envFrom:
+            - secretRef:
+                name: {{ include "quassel-helm.fullname" . }}
+            - configMapRef:
+                name: {{ include "quassel-helm.fullname" . }}
+          args:
+            - "--config-from-environment"
+            - "--configdir"
+            - "/config"
+            - "--loglevel"
+            - "{{ .Values.config.loglevel }}"
+            {{ if .Values.proxy.enabled}}
+            - "--proxy-cidr"
+            - "{{ .Values.proxy.cidr | join "," }}"
+            {{ end }}
+            {{ if .Values.identd.enabled }}
+            - "--ident-daemon"
+            {{ if .Values.identd.strict }}
+            - "--strict-ident"
+            {{ end }}
+            - "--ident-port"
+            - "{{ .Values.identd.containerPort }}"
+            - "--ident-listen"
+            - "{{ .Values.identd.listen | join "," }}"
+            {{ end }}
+            {{ if .Values.metrics.enabled }}
+            - "--metrics-daemon"
+            - "--metrics-port"
+            - "{{ .Values.metrics.containerPort }}"
+            - "--metrics-listen"
+            - "{{ .Values.metrics.listen | join "," }}"
+            {{ end }}
+            {{ if .Values.ssl.enabled }}
+            {{ if .Values.ssl.required }}
+            - "--require-ssl"
+            {{ end }}
+            - "--ssl-cert"
+            - "/certs/{{ .Values.ssl.secret.certificate }}"
+            - "--ssl-key"
+            - "/certs/{{ .Values.ssl.secret.key }}"
+            {{ end }}
+            {{ if .Values.debug.enabled }}
+            - "--debug"
+            {{ end }}
+            {{ if .Values.debug.irc.enabled }}
+            {{ if .Values.debug.irc.network}}
+            - "--debug-irc-id"
+            - "{{ .Values.debug.irc.network }}"
+            {{ else }}
+            - "--debug-irc"
+            {{ end }}
+            {{ end }}
+            {{ if .Values.debug.parsed.enabled }}
+            {{ if .Values.debug.parsed.network}}
+            - "--debug-irc-parsed-id"
+            - "{{ .Values.debug.parsed.network }}"
+            {{ else }}
+            - "--debug-irc-parsed"
+            {{ end }}
+            {{ end }}
+          ports:
+            - name: bouncer
+              containerPort: {{ .Values.bouncer.containerPort }}
+              protocol: TCP
+          {{ if .Values.metrics.enabled }}
+            - name: identd
+              containerPort: {{ .Values.identd.containerPort }}
+          {{ end }}
+          {{ if .Values.metrics.enabled }}
+            - name: metrics
+              containerPort: {{ .Values.metrics.containerPort }}
+              protocol: TCP
+          startupProbe:
+            httpGet:
+              path: /healthz
+              port: metrics
+          livenessProbe:
+            httpGet:
+              path: /healthz
+              port: metrics
+          readinessProbe:
+            httpGet:
+              path: /healthz
+              port: metrics
+          {{ end }}
+          resources:
+            {{- toYaml .Values.resources | nindent 12 }}
+          volumeMounts:
+            - mountPath: /config
+              name: config
+          {{ if .Values.ssl.enabled }}
+            - mountPath: /certs
+              name: ssl
+          {{ end }}
+      {{- 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/quassel/templates/secret.yaml b/quassel/templates/secret.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..bea181d80809b181935797c671d73d8d543a4afe
--- /dev/null
+++ b/quassel/templates/secret.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: {{ include "quassel-helm.fullname" . }}
+  labels:
+    {{- include "quassel-helm.labels" . | nindent 4 }}
+stringData:
+  {{ if eq .Values.config.database "PostgreSQL" }}
+  DB_PGSQL_PASSWORD: {{ .Values.config.postgres.password }}
+  {{ end }}
+  {{ if eq .Values.config.authenticator "Ldap" }}
+  AUTH_LDAP_BIND_PASSWORD: {{ .Values.config.ldap.bind_password }}
+  {{ end }}
diff --git a/quassel/templates/service.yaml b/quassel/templates/service.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..774b837dc9e934b3daccde481638501e7bc6c0cc
--- /dev/null
+++ b/quassel/templates/service.yaml
@@ -0,0 +1,33 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "quassel-helm.fullname" . }}
+  {{ if .Values.metrics.enabled }}
+  annotations:
+    prometheus.io/path: "/metrics"
+    prometheus.io/port: "{{ .Values.metrics.port }}"
+    prometheus.io/scrape: "true"
+  {{ end }}
+  labels:
+    {{- include "quassel-helm.labels" . | nindent 4 }}
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+    - port: {{ .Values.bouncer.port }}
+      targetPort: bouncer
+      protocol: TCP
+      name: bouncer
+    {{ if .Values.identd.enabled }}
+    - port: {{ .Values.identd.port }}
+      targetPort: identd
+      protocol: TCP
+      name: identd
+    {{ end }}
+    {{ if .Values.metrics.enabled }}
+    - port: {{ .Values.metrics.port }}
+      targetPort: metrics
+      protocol: TCP
+      name: metrics
+    {{ end }}
+  selector:
+    {{- include "quassel-helm.selectorLabels" . | nindent 4 }}
diff --git a/quassel/values.yaml b/quassel/values.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a04592f36ef2a11fa2aa06dee6335f20e3631d20
--- /dev/null
+++ b/quassel/values.yaml
@@ -0,0 +1,120 @@
+replicaCount: 1
+
+image:
+  repository: k8r.eu/justjanne/quassel-docker
+  pullPolicy: IfNotPresent
+  tag: ""
+
+imagePullSecrets: [ ]
+nameOverride: ""
+fullnameOverride: ""
+
+podAnnotations: { }
+
+podSecurityContext:
+  fsGroup: 2000
+
+securityContext:
+  capabilities:
+    drop:
+      - ALL
+  readOnlyRootFilesystem: true
+  runAsNonRoot: true
+  runAsUser: 1000
+
+config:
+  # Disables restoring the previous state, useful for situations where this has
+  # been corrupted
+  noRestore: false
+  # Available options: Debug, Info, Warning, Error
+  loglevel: "Info"
+  # Available options: SQLite, PostgreSQL
+  database: "SQLite"
+  postgres:
+    hostname: "localhost"
+    port: 5432
+    database: "quassel"
+    username: "quassel"
+    password: "hunter2"
+
+  # Available options: Database, Ldap
+  authenticator: "Database"
+  ldap:
+    hostname: "ldap://localhost"
+    port: 389
+    bind_dn: ""
+    bind_password: "hunter2"
+    base_dn: ""
+    filter: ""
+    uid_attribute: "uid"
+
+ssl:
+  enabled: false
+  required: true
+  secret:
+    name: "quassel-tls"
+    certificate: "tls.crt"
+    key: "tls.key"
+  certChecker:
+    enabled: true
+    repository: k8r.eu/justjanne/fswatch
+    pullPolicy: IfNotPresent
+    tag: latest
+
+proxy:
+  enabled: false
+  cidr:
+    - "10.244.0.0/16"
+
+bouncer:
+  port: 4242
+  containerPort: 4242
+  listen:
+    - "::"
+    - "0.0.0.0"
+
+identd:
+  enabled: false
+  strict: true
+  port: 113
+  containerPort: 10113
+  listen:
+    - "::"
+    - "0.0.0.0"
+
+metrics:
+  enabled: true
+  port: 9090
+  containerPort: 9090
+  listen:
+    - "::"
+    - "0.0.0.0"
+
+service:
+  type: ClusterIP
+
+volume:
+  emptyDir: {}
+
+resources:
+  limits:
+    cpu: 500m
+    memory: 512Mi
+  requests:
+    cpu: 50m
+    memory: 128Mi
+
+nodeSelector: { }
+
+tolerations: [ ]
+
+affinity: { }
+
+debug:
+  enabled: false
+  irc:
+    enabled: false
+    network: ""
+  parsed:
+    enabled: false
+    network: ""