Skip to content

About

A helm chart to deploy https://stalw.art/

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Stalwart Helm chart

Deploys Stalwart with optional Bulwark webmail. Defaults to one Stalwart replica, external PostgreSQL and S3, and no persistent volume. Database, bucket, Secrets, DNS, and public routing must be supplied separately.

This is an initial 0.1 release. Template, schema, policy, and container startup checks have been performed; a complete Kubernetes deployment, mail delivery, and OAuth login remain unverified. See operating notes before production use.

Install

The Common dependency is vendored in charts/; a fresh checkout needs no dependency download. Copy examples/postgresql-s3.yaml to a private values file and set your database host, bucket, and Secret references. PostgreSQL TLS and certificate validation are enabled by default.

Create the namespace and these Secrets there before installing:

Example Secret Required keys
stalwart-postgresql password: database password
stalwart-s3 accessKey, secretKey: bucket credentials
stalwart-admin password: provisioning administrator password; STALWART_RECOVERY_ADMIN: admin:<same password> for initial setup

The example references stalwart-admin both for server environment variables and for CLI provisioning. A bare install without configured storage and credentials fails validation.

helm upgrade --install stalwart . --namespace stalwart \
  -f my-values.yaml --set recoveryMode.enabled=true --timeout 15m
kubectl --namespace stalwart port-forward svc/stalwart-management 8080:8080

Open http://localhost:8080/admin with the recovery administrator credential. Configure domains, permanent accounts, listeners, certificates, and any search backend. Recovery mode suspends mail services. Once setup is complete:

helm upgrade stalwart . --namespace stalwart \
  -f my-values.yaml --set recoveryMode.enabled=false --timeout 15m

Replace the provisioning credential with a permanent administrator's password, remove STALWART_RECOVERY_ADMIN, and restart server pods to remove it from their environment. Keep provisioning credentials valid for future upgrades.

Images and credentials

Server, CLI, and Bulwark images are pinned by digest in values.yaml. Changing a tag alone does not change the image: update the corresponding image.digest, or explicitly clear it to use the tag. This applies to image, provisioning.image, and webmail.image. A nonempty global.imageRegistry overrides their registries; mirrors must contain all enabled images.

Renovate checks the pinned images, Bitnami Common, the CI tools and GitHub Actions weekly and opens PRs that change tag and digest together; for the main image the PR also moves appVersion in Chart.yaml. Image blocks in values.yaml must keep the order registry, repository, tag, digest for Renovate to match them. Install the Renovate GitHub App on the repository to enable it.

Prefer existing Secrets. Plain credential values are stored in Helm release history even when rendered into Kubernetes Secrets.

Credential Existing Secret settings
Recovery administrator recoveryAdmin.enabled, .existingSecret, .secretKey (value is username:password)
PostgreSQL dataStore.postgresql.existingSecret, .passwordSecretKey
S3 blobStore.s3.existingSecret, .accessKeySecretKey, .secretKeySecretKey
Provisioning administrator provisioning.existingSecret, .passwordSecretKey (password only; username is separate)
Metrics metrics.auth.existingSecret, .passwordSecretKey
NATS coordinator.nats.existingSecret, .passwordSecretKey
Bulwark session/admin/OAuth webmail.sessionSecret, webmail.adminPassword, webmail.oauth
Image pulls global.imagePullSecrets or each image's pullSecrets

Top-level existingSecret supplies server environment variables through envFrom. Use it for recovery credentials as in the example, or use recoveryAdmin explicitly. extraEnvVars supports individual secretKeyRef entries; extraEnv contains plain values.

Existing Secret changes require a pod restart for environment variables to refresh. Helm cannot verify Secret contents or credentials at render time.

Storage and provisioning

dataStore.backend selects postgresql or rocksdb. A nonempty config object overrides that selection with raw startup configuration. Local durable stores, including local search storage, need persistence.enabled: true; RocksDB/SQLite cannot run with multiple replicas. With persistence disabled, local writes are lost on pod replacement.

blobStore.backend: s3 applies a BlobStore configuration through a post-install/post-upgrade CLI Job. Use Stalwart region names such as EuCentral1; for an S3-compatible provider, set endpoint and customRegion. The chart exposes permanent access keys, not session tokens or workload identity. Set blobStore.backend: existing to leave an externally configured BlobStore untouched.

The same Job handles optional server settings:

Values Action
metrics.enabled: true, metrics.provision: true Configure the Prometheus exporter
email.provision: true Apply encryptAtRest and encryptOnAppend
oidc.enabled: true Declare an OIDC Directory; oidc.activate separately enables it
Nonempty coordinator.type Configure cluster coordination

The ServiceMonitor and its authentication Secrets always live in the Helm release namespace. Configure Prometheus's serviceMonitorNamespaceSelector to discover that namespace and its serviceMonitorSelector to match the monitor's labels; Prometheus itself can stay in a separate monitoring namespace. The former metrics.serviceMonitor.namespace option has been removed; remove it from existing values files before upgrading.

All provisioning requires an administrator already accepted by Stalwart. The Job uses the internal management Service over HTTP. Successful Jobs are deleted; failed Jobs remain until the next attempt:

kubectl --namespace stalwart logs job/stalwart-provisioning

Use a Helm timeout longer than provisioning.activeDeadlineSeconds. Upgrades reapply enabled settings; rollback and uninstall do not undo changes in Stalwart's database. Changing a BlobStore does not migrate mail. Disabling a provisioning option does not revert its previous configuration.

Networking

Set service.type: LoadBalancer for external mail traffic. Ingress handles HTTP/HTTPS only; SMTP, IMAP, POP3, and ManageSieve need TCP exposure. Management stays on a separate <fullname>-management ClusterIP Service.

service.ports.<name>.port is the Service port and .containerPort is the application port. Configure matching listeners in Stalwart: declaring Kubernetes ports does not make the application listen. Ingress hosts, TLS, and upstream protocol settings must match your controller and listener configuration.

externalTrafficPolicy: Local is the default for external Services; use Cluster if cross-node forwarding is required. service.ipFamilyPolicy and service.ipFamilies configure mail Service IP families; equivalent fields exist under webmail.service.

Gateway API

Routes attach to existing Gateways; install compatible CRDs and a controller separately.

gatewayAPI:
  httpRoute:
    enabled: true
    parentRefs:
      - name: web-gateway
        namespace: infrastructure
        sectionName: https
    hostnames: [mail.example.org]
  tcpRoutes:
    smtp:
      parentRefs:
        - name: mail-gateway
          namespace: infrastructure
          sectionName: smtp

HTTPRoute defaults to plain HTTP upstream with client TLS terminated at the Gateway. TCPRoutes require Gateway TCP listeners and preserve the mail protocol's TLS stream. Configure mail certificates in Stalwart. Cross-namespace Gateway listeners must allow routes from the release namespace.

TCPRoute defaults to gateway.networking.k8s.io/v1; set gatewayAPI.tcpRouteApiVersion to the version installed in your cluster. Check route Accepted and ResolvedRefs status after installation. Management is not exposed by these routes.

Monitoring and authentication

metrics.enabled: true configures the exporter; metrics.serviceMonitor.enabled: true additionally creates a ServiceMonitor and requires Prometheus Operator CRDs. Set metrics.provision: false when managing exporter configuration outside Helm. Scraping targets the internal management Service at /metrics/prometheus. Configure metrics.auth for Basic authentication; internal reachability alone is not access control.

email.provision: true applies message encryption settings. Encryption still requires each account's OpenPGP key or S/MIME certificate; these settings do not configure S3 bucket encryption. Configure bucket encryption separately.

oidc.enabled declares a Stalwart token-validation Directory. oidc.activate: true switches the active authentication directory; treat this as a server-wide authentication change, not a webmail-only toggle. Matching accounts must already exist. Test administrator access and mail-client authentication before activation. The chart does not provision identity-provider clients or user accounts.

Webmail (Bulwark)

Bulwark is disabled by default. Enable it with a browser-reachable JMAP URL and a shared session secret:

webmail:
  enabled: true
  jmapServerUrl: https://mail.example.org
  sessionSecret:
    existingSecret: bulwark-session  # key: SESSION_SECRET, at least 32 characters
  adminPassword:
    existingSecret: bulwark-admin    # key: ADMIN_PASSWORD
  ingress:
    enabled: true
    host: webmail.example.org

Configure TLS on the Ingress for deployment. webmail.gatewayAPI.httpRoute is available instead. Without an explicit JMAP URL, the chart uses the internal mail Service, which is unsuitable for browser access outside the cluster. Serve the published Bulwark image on a dedicated host; its base path is a build-time setting.

adminPassword bootstraps Bulwark's local dashboard, separate from Stalwart accounts. Set it explicitly when using that dashboard. sessionSecret is required for dashboard sessions and other encrypted session features; generate one with openssl rand -base64 32.

For multiple replicas, including webmail.autoscaling.maxReplicas > 1:

  • Provide both a pinned admin password and the same session secret on every replica. Inline session secrets must be at least 32 characters; ensure existing Secret contents meet that requirement too.
  • Use ReadWriteMany persistence, or disable persistence and settings sync. With ephemeral storage, dashboard edits and audit data remain local to each replica and disappear on pod replacement. Shared-volume application behavior still needs deployment testing.
  • Leave adminConfigReadonly: false unless you supply a pre-seeded admin.json; read-only configuration prevents password bootstrap.

webmail.oauth controls Bulwark's browser login flow and needs its own identity-provider client ID and secret. It is separate from Stalwart's oidc token validation. The issuer defaults to oidc.issuerUrl when that integration is enabled. OAuth login has not been verified end to end.

webmail.telemetry.enabled: false disables Bulwark telemetry, not all outbound traffic. Additional application settings can be supplied through webmail.extraEnv and webmail.extraEnvVars.

Scaling and coordination

Multiple Stalwart replicas need shared external stores and cluster coordination. The chart rejects local DataStores with multiple replicas but does not enforce coordinator setup, since it may be configured outside Helm.

replicaCount: 3
podAntiAffinityPreset: soft
coordinator:
  type: nats
  nats:
    addresses: [nats.messaging.svc.cluster.local:4222]
    existingSecret: stalwart-nats   # key: password

Provision the coordinator separately. Supported chart options are nats, redis, kafka, and zenoh; see values.yaml for their fields. Check backend support and discovery in the chosen Stalwart build. Avoid embedding Redis passwords in URLs stored as configuration.

autoscaling.enabled creates an HPA and omits the workload's static replica count. Configure CPU/memory requests for utilization targets, or supply autoscaling.metrics. role and pushShard apply to every replica in a release; use separate releases for different roles. Bulwark has independent webmail.autoscaling settings.

podAntiAffinityPreset accepts soft or hard; explicit affinity takes precedence. Hard anti-affinity needs enough nodes. podManagementPolicy defaults to OrderedReady; changing it on an existing StatefulSet requires recreation. The chart does not install a PodDisruptionBudget.

Operating notes

Stalwart runs as UID/GID 2000 with a read-only root filesystem, RuntimeDefault seccomp, no privilege escalation, and all capabilities dropped except NET_BIND_SERVICE. Additional writable paths need explicit volumes. ServiceAccount token automounting is disabled; the chart grants no Kubernetes RBAC permissions.

Restrict management access with deployment-specific NetworkPolicies: a ClusterIP Service alone does not isolate callers. Configure external storage access controls, TLS, backups, and restore procedures separately. Check PVC retention before uninstalling or recreating workloads, and keep release names and fullnameOverride stable to preserve resource naming.

Stalwart's requests of 100m CPU and 512Mi memory are starting points. Tune resources, probes, placement, and disruption budgets from measured workloads; startup probes allow up to ten minutes. The chart does not install NetworkPolicies or PodDisruptionBudgets.

Before production use, test a fresh installation and upgrade with your actual infrastructure. Verify provisioning, mail send/receive, TLS, administrator access, and backup restoration. Test OAuth, cross-replica sessions, and failover when enabled; rendering and container health checks do not establish these behaviors.

GitOps and validation

The provisioning Job has Helm post-install/post-upgrade and Argo CD PostSync hooks. Ensure your controller executes hooks; plain helm template only renders resources. Install Gateway API, ServiceMonitor, and other required CRDs before enabling their resources.

Run local checks with Helm, Python, and Docker:

pip install --require-hashes -r ci/requirements.txt
scripts/render-matrix.sh
scripts/validate-manifests.sh
python3 tests/render.py
yamllint -c .yamllint.yaml Chart.yaml values.yaml ci examples policies .github

CI also checks workflow syntax and the values schema. Optional local hooks are defined in .pre-commit-config.yaml. These checks do not establish runtime correctness or production readiness.

policies/kyverno/ contains optional ValidatingPolicy resources for pod hardening, image digests, and resource requests. They are not installed by the chart. They default to Deny and have no namespace restriction; scope them or choose audit actions before applying to a shared cluster. Use a Kyverno version supporting this policy API.

Release

Common is vendored and included in the packaged chart. Renovate opens PRs that update it together with its archive. For a manual dependency upgrade, update Chart.yaml, run helm dependency update ., and commit Chart.lock and the replacement archive together. Review extracted dependency changes. helm dependency build . restores dependencies from the existing lock file.

Push a vX.Y.Z tag matching Chart.yaml to run CI, package the chart, publish it to oci://ghcr.io/<owner>/charts/stalwart, and sign its digest with cosign. GitHub publishing and signing require a successful hosted run. Consumers can install a published version with their configured values:

helm upgrade --install stalwart oci://ghcr.io/<owner>/charts/stalwart \
  --version <X.Y.Z> --namespace stalwart -f my-values.yaml --timeout 15m

For a fresh deployment, follow the recovery-mode setup above. Verify published signatures against the expected repository's release workflow identity and GitHub's OIDC issuer (https://token.actions.githubusercontent.com).

Values

The table below is generated from values.yaml by helm-docs. Edit the # -- comments in values.yaml, or README.md.gotmpl, then run scripts/generate-docs.sh. Do not edit README.md directly; CI fails if it is out of date.

Configuration values and defaults
Key Type Default Description
additionalAnnotations object {}
additionalLabels object {} Metadata is merged without changing immutable selector labels.
affinity object {} Explicit affinity always wins over podAntiAffinityPreset below.
autoscaling.behavior object {}
autoscaling.enabled bool false
autoscaling.maxReplicas int 5
autoscaling.metrics list [] Advanced: raw autoscaling/v2 metric entries, used instead of the two convenience targets above when non-empty.
autoscaling.minReplicas int 1
autoscaling.targetCPUUtilizationPercentage int 80
autoscaling.targetMemoryUtilizationPercentage string ""
blobStore.backend string "s3"
blobStore.s3.accessKey string "" Fallback only, stored in a chart-managed Secret and visible in Helm's release history. Prefer existingSecret. Both are required together when existingSecret is not set.
blobStore.s3.accessKeySecretKey string "accessKey"
blobStore.s3.allowInvalidCerts bool false
blobStore.s3.bucket string ""
blobStore.s3.customRegion string "us-east-1"
blobStore.s3.endpoint string ""
blobStore.s3.existingSecret string ""
blobStore.s3.keyPrefix string ""
blobStore.s3.region string "EuCentral1" Stalwart region variant, e.g. EuCentral1. An endpoint selects Custom instead.
blobStore.s3.secretKey string ""
blobStore.s3.secretKeySecretKey string "secretKey"
blobStore.s3.verifyAfterWrite bool true
config object {} Advanced raw DataStore object. A nonempty object overrides dataStore selection.
containerSecurityContext.allowPrivilegeEscalation bool false
containerSecurityContext.capabilities.add[0] string "NET_BIND_SERVICE"
containerSecurityContext.capabilities.drop[0] string "ALL"
containerSecurityContext.enabled bool true
containerSecurityContext.privileged bool false
containerSecurityContext.readOnlyRootFilesystem bool true
containerSecurityContext.runAsNonRoot bool true
coordinator.kafka.brokers list []
coordinator.kafka.groupId string "stalwart"
coordinator.nats.addresses list []
coordinator.nats.authUsername string "stalwart"
coordinator.nats.existingSecret string ""
coordinator.nats.password string "" Fallback only, stored in a chart-managed Secret and visible in Helm's release history. Prefer existingSecret. Leave both empty for no auth.
coordinator.nats.passwordSecretKey string "password"
coordinator.nats.useTls bool false
coordinator.redis.url string ""
coordinator.type string ""
coordinator.zenoh.config string ""
dataStore.backend string "postgresql"
dataStore.postgresql.allowInvalidCerts bool false
dataStore.postgresql.database string "stalwart"
dataStore.postgresql.existingSecret string "" Existing Secret containing the database password. Falls back to top-level existingSecret, then to the plain password below.
dataStore.postgresql.host string ""
dataStore.postgresql.password string "" Fallback only, stored in a chart-managed Secret and visible in Helm's release history. Prefer existingSecret.
dataStore.postgresql.passwordSecretKey string "STALWART_DB_PASSWORD"
dataStore.postgresql.poolMaxConnections int 10
dataStore.postgresql.port int 5432
dataStore.postgresql.useTls bool true
dataStore.postgresql.username string "stalwart"
dataStore.rocksdb.path string "/var/lib/stalwart"
domain string "" Convenience base domain: when set and ingress.hosts / webmail.ingress.host are left at their defaults, they derive mail. and webmail. respectively. Set either explicitly to override this, or leave domain empty and set both explicitly yourself.
email.encryptAtRest bool true Encrypts messages delivered via SMTP/LMTP before they're written to disk, for any recipient with a registered key.
email.encryptOnAppend bool false Also encrypt client-appended (IMAP/JMAP) messages server-wide, overriding any per-account preference. Off by default so clients keep control, matching Stalwart's own default.
email.provision bool false Off by default: flipping it on reapplies the Email singleton via the same CLI apply Job used for blobStore/metrics, which requires provisioning credentials even if nothing else needs the Job.
existingSecret string "" Existing Secret containing environment variables; also accepts STALWART_RECOVERY_ADMIN.
extraEnv object {} Extra environment variables injected into the container, for store credentials that live in plain text.
extraEnvVars list [] Extra environment variables for the mail container, as full Kubernetes env entries (for example valueFrom.secretKeyRef).
extraSecretEnv object {} Extra environment variables injected from the managed Secret. Use this for anything sensitive (database passwords, S3 keys, etc.).
extraVolumeMounts list [] Mounts for extraVolumes in the mail container.
extraVolumes list [] Extra volumes for the mail pod, for example a mounted certificate or a writable path for custom logging.
fullnameOverride string "" Overrides the full resource name. Use it to keep existing resource names when upgrading from another chart.
gatewayAPI.httpRoute.annotations object {}
gatewayAPI.httpRoute.backendPortName string "http" Plain HTTP upstream; terminate client TLS on the Gateway's HTTPS listener.
gatewayAPI.httpRoute.enabled bool false
gatewayAPI.httpRoute.hostnames list []
gatewayAPI.httpRoute.parentRefs list [] Each reference names a Gateway; namespace and sectionName are optional.
gatewayAPI.httpRoute.paths[0].type string "PathPrefix"
gatewayAPI.httpRoute.paths[0].value string "/"
gatewayAPI.tcpRouteApiVersion string "gateway.networking.k8s.io/v1" Use v1alpha2 for older experimental-channel TCPRoute CRDs.
gatewayAPI.tcpRoutes object {} Keys refer to service.ports entries, with a Gateway TCP listener per mail port. tcpRoutes: smtp: parentRefs: - name: mail-gateway sectionName: smtp annotations: {}
global.defaultStorageClass string "" Default storage class when persistence.storageClass is unset.
global.imagePullSecrets list []
global.imageRegistry string ""
image.digest string pinned, see values.yaml Multi-arch index digest of the tag. Takes precedence over the tag; clear it to float on the tag instead.
image.pullPolicy string "Always"
image.pullSecrets list []
image.registry string "docker.io"
image.repository string "stalwartlabs/stalwart"
image.tag string pinned, see values.yaml Image tag, kept in sync with the digest by Renovate.
ingress.annotations object {}
ingress.className string ""
ingress.enabled bool false
ingress.hosts list [] Defaults to a single "mail." host (HTTPS, path /) when empty and top-level domain is set. Set this explicitly to override that default or to declare more than one host.
ingress.tls list []
lifecycleHooks object {} Lifecycle hooks for the mail container.
livenessProbe.enabled bool true
livenessProbe.failureThreshold int 3
livenessProbe.initialDelaySeconds int 0
livenessProbe.periodSeconds int 10
livenessProbe.successThreshold int 1
livenessProbe.timeoutSeconds int 5
management.annotations object {}
management.port int 8080
management.servicePort int 8080
metrics.auth.existingSecret string ""
metrics.auth.password string "" Fallback only, stored in a chart-managed Secret and visible in Helm's release history. Prefer existingSecret.
metrics.auth.passwordSecretKey string "password"
metrics.auth.username string ""
metrics.enabled bool false
metrics.path string "/metrics/prometheus"
metrics.provision bool true Reapplies the Metrics singleton on every install/upgrade via the same CLI apply Job used for S3 provisioning. Disable if the Prometheus exporter is already configured through the WebUI/CLI.
metrics.serviceMonitor.additionalLabels object {}
metrics.serviceMonitor.annotations object {}
metrics.serviceMonitor.enabled bool false
metrics.serviceMonitor.honorLabels bool false
metrics.serviceMonitor.interval string "30s"
metrics.serviceMonitor.metricRelabelings list []
metrics.serviceMonitor.relabelings list []
metrics.serviceMonitor.scrapeTimeout string ""
nameOverride string "" Overrides the chart name used in resource names.
nodeSelector object {} Node selector for the mail StatefulSet.
oidc.activate bool false Activates this directory server-wide via the Authentication singleton's directoryId, replacing the internal directory for every protocol (IMAP, POP3, SMTP, JMAP) -- not only WebUI/JMAP -- since Stalwart has no per-protocol override. Stalwart's documentation does not confirm whether local/admin login keeps a fallback once this is set. Verify WebUI/admin access still works in a non-production environment before enabling this; leaving it false only declares the Directory object without switching any authentication over to it.
oidc.claimGroups string ""
oidc.claimName string "name"
oidc.claimUsername string "preferred_username"
oidc.enabled bool false
oidc.issuerUrl string ""
oidc.name string "keycloak" Short label. Also used as the Directory object's natural key (its "description" field), so re-applying doesn't create duplicates, and as the CLI-apply cross-reference for activate.
oidc.requireAudience string "stalwart"
oidc.requireScopes list ["openid","email"] A list here; rendered into Stalwart's object-of-true Set encoding.
oidc.usernameDomain string "" Domain appended to claimUsername values that don't already contain "@".
persistence.accessMode string "ReadWriteOnce"
persistence.enabled bool false Disabled for external PostgreSQL/S3; enable for local durable stores.
persistence.size string "20Gi"
persistence.storageClass string "" Empty uses the cluster default; "-" explicitly requests no storage class.
podAnnotations object {} Extra annotations on the mail pods. checksum/config and checksum/secret are reserved.
podAntiAffinityPreset string "" Spreads replicas across nodes when affinity is left empty: "", "soft" (preferred, default topologyKey kubernetes.io/hostname), or "hard" (required -- extra unschedulable replicas rather than co-located ones).
podLabels object {} Extra labels on the mail pods. Selector labels are reserved and cannot be overridden.
podManagementPolicy string "OrderedReady" OrderedReady starts/stops pods one at a time, each waiting for readiness (the startup probe allows up to ten minutes), so an HPA scale-up is serial. Parallel starts them together; with an external DataStore and no per-replica local state that is safe and much faster to scale. Immutable on an existing StatefulSet -- changing it on a live release means deleting the StatefulSet (e.g. --cascade=orphan) and upgrading.
podSecurityContext.enabled bool true
podSecurityContext.fsGroup int 2000
podSecurityContext.fsGroupChangePolicy string "OnRootMismatch"
podSecurityContext.runAsGroup int 2000
podSecurityContext.runAsNonRoot bool true
podSecurityContext.runAsUser int 2000
podSecurityContext.seccompProfile.type string "RuntimeDefault"
provisioning.activeDeadlineSeconds int 600
provisioning.backoffLimit int 6
provisioning.existingSecret string "" Password for an administrator already recognized by Stalwart.
provisioning.image.digest string pinned, see values.yaml Multi-arch index digest of the tag. Takes precedence over the tag; clear it to float on the tag instead.
provisioning.image.pullPolicy string "Always"
provisioning.image.pullSecrets list []
provisioning.image.registry string "ghcr.io"
provisioning.image.repository string "stalwartlabs/cli"
provisioning.image.tag string pinned, see values.yaml Image tag, kept in sync with the digest by Renovate.
provisioning.password string "" Fallback only, stored in a chart-managed Secret and visible in Helm's release history. Prefer existingSecret.
provisioning.passwordSecretKey string "password"
provisioning.resources.requests.cpu string "50m"
provisioning.resources.requests.memory string "64Mi"
provisioning.username string "admin"
pushShard string "" Maps to STALWART_PUSH_SHARD. Only set on nodes whose role delivers push notifications. Leave empty on single-node installs.
readinessProbe.enabled bool true
readinessProbe.failureThreshold int 3
readinessProbe.initialDelaySeconds int 0
readinessProbe.periodSeconds int 10
readinessProbe.successThreshold int 1
readinessProbe.timeoutSeconds int 5
recoveryAdmin.enabled bool false
recoveryAdmin.existingSecret string "" Preferred: key containing the complete username:password credential.
recoveryAdmin.password string ""
recoveryAdmin.secretKey string "STALWART_RECOVERY_ADMIN"
recoveryAdmin.username string "admin"
recoveryMode.enabled bool false
recoveryMode.logLevel string "info"
recoveryMode.port int 8080
replicaCount int 1 Number of pods in the StatefulSet. Single-node deployments keep this at 1. For a clustered deployment, raise this value and set role (and pushShard where applicable) to a value that makes sense for the whole StatefulSet, or install multiple releases of this chart with different role values.
resources.requests.cpu string "100m"
resources.requests.memory string "512Mi"
role string "" Maps to STALWART_ROLE. Names a ClusterRole defined in the database. Leave empty on single-node installs to run every task and listener. See /docs/cluster/configuration/roles for the role model.
service.annotations object {}
service.enabled bool true
service.externalTrafficPolicy string "Local" Preserve source IPs when supported by the load balancer.
service.ipFamilies list []
service.ipFamilyPolicy string "" Dual-stack: SingleStack, PreferDualStack or RequireDualStack. Empty leaves the cluster default. ipFamilies (IPv4/IPv6, in order) with two entries requires a *DualStack policy.
service.loadBalancerSourceRanges list []
service.ports.http.containerPort int 80
service.ports.http.port int 80
service.ports.http.protocol string "TCP"
service.ports.https.containerPort int 443
service.ports.https.port int 443
service.ports.https.protocol string "TCP"
service.ports.imap.containerPort int 143
service.ports.imap.port int 143
service.ports.imap.protocol string "TCP"
service.ports.imaps.containerPort int 993
service.ports.imaps.port int 993
service.ports.imaps.protocol string "TCP"
service.ports.pop3.containerPort int 110
service.ports.pop3.port int 110
service.ports.pop3.protocol string "TCP"
service.ports.pop3s.containerPort int 995
service.ports.pop3s.port int 995
service.ports.pop3s.protocol string "TCP"
service.ports.sieve.containerPort int 4190
service.ports.sieve.port int 4190
service.ports.sieve.protocol string "TCP"
service.ports.smtp.containerPort int 25
service.ports.smtp.port int 25
service.ports.smtp.protocol string "TCP"
service.ports.smtps.containerPort int 465
service.ports.smtps.port int 465
service.ports.smtps.protocol string "TCP"
service.ports.submission.containerPort int 587
service.ports.submission.port int 587
service.ports.submission.protocol string "TCP"
service.type string "ClusterIP"
serviceAccount.annotations object {}
serviceAccount.automountServiceAccountToken bool false
serviceAccount.create bool true
serviceAccount.labels object {}
serviceAccount.name string ""
startupProbe.enabled bool true
startupProbe.failureThreshold int 60
startupProbe.initialDelaySeconds int 0
startupProbe.periodSeconds int 10
startupProbe.successThreshold int 1
startupProbe.timeoutSeconds int 5
terminationGracePeriodSeconds int 60 Seconds the pod gets to shut down cleanly. Stalwart needs time to drain connections.
tolerations list [] Tolerations for the mail StatefulSet.
topologySpreadConstraints list [] Topology spread constraints for the mail StatefulSet. Use for zone-level spreading; podAntiAffinityPreset only covers nodes.
updateStrategy.type string "RollingUpdate"
webmail.additionalAnnotations object {}
webmail.additionalLabels object {}
webmail.adminConfigReadonly bool false Prevent admin config writes. Requires a pre-seeded admin.json; otherwise Bulwark cannot bootstrap the dashboard password.
webmail.adminPassword.existingSecret string ""
webmail.adminPassword.secretKey string "ADMIN_PASSWORD"
webmail.adminPassword.value string ""
webmail.affinity object {} Explicit affinity always wins over podAntiAffinityPreset below.
webmail.appName string ""
webmail.autoscaling.behavior object {}
webmail.autoscaling.enabled bool false
webmail.autoscaling.maxReplicas int 5
webmail.autoscaling.metrics list []
webmail.autoscaling.minReplicas int 1
webmail.autoscaling.targetCPUUtilizationPercentage int 80
webmail.autoscaling.targetMemoryUtilizationPercentage string ""
webmail.containerSecurityContext.allowPrivilegeEscalation bool false
webmail.containerSecurityContext.capabilities.drop[0] string "ALL"
webmail.containerSecurityContext.enabled bool true
webmail.containerSecurityContext.privileged bool false
webmail.containerSecurityContext.readOnlyRootFilesystem bool true
webmail.containerSecurityContext.runAsNonRoot bool true
webmail.enabled bool false
webmail.extraEnv object {} Extra environment variables for anything not exposed as its own value above (branding, PWA icons, cookie/CSP settings, logging, locale, etc.) -- see bulwarkmail.org's environment reference.
webmail.extraEnvVars list []
webmail.extraVolumeMounts list []
webmail.extraVolumes list []
webmail.gatewayAPI.httpRoute.annotations object {}
webmail.gatewayAPI.httpRoute.enabled bool false
webmail.gatewayAPI.httpRoute.hostnames list [] Defaults to a single "webmail." hostname when empty and top-level domain is set; required explicitly otherwise.
webmail.gatewayAPI.httpRoute.parentRefs list [] Each reference names a Gateway; namespace and sectionName are optional.
webmail.image.digest string pinned, see values.yaml Multi-arch index digest of the tag. Takes precedence over the tag; clear it to float on the tag instead.
webmail.image.pullPolicy string "IfNotPresent"
webmail.image.pullSecrets list []
webmail.image.registry string "ghcr.io"
webmail.image.repository string "bulwarkmail/webmail"
webmail.image.tag string pinned, see values.yaml Image tag, kept in sync with the digest by Renovate.
webmail.ingress.annotations object {}
webmail.ingress.className string ""
webmail.ingress.enabled bool false
webmail.ingress.host string "" Defaults to "webmail." when empty and top-level domain is set; required explicitly otherwise.
webmail.ingress.tls list []
webmail.jmapServerUrl string "" Browser-reachable JMAP URL. Empty falls back to the internal mail Service; set a public URL for access from outside the cluster.
webmail.livenessProbe.enabled bool true
webmail.livenessProbe.failureThreshold int 3
webmail.livenessProbe.initialDelaySeconds int 0
webmail.livenessProbe.periodSeconds int 15
webmail.livenessProbe.successThreshold int 1
webmail.livenessProbe.timeoutSeconds int 5
webmail.nodeSelector object {}
webmail.oauth.autoSso bool false Starts the OAuth redirect automatically with no user interaction. Requires only: true.
webmail.oauth.clientId string ""
webmail.oauth.clientSecret string "" Fallback only, stored in a chart-managed Secret. Prefer existingSecret.
webmail.oauth.clientSecretKey string "OAUTH_CLIENT_SECRET"
webmail.oauth.enabled bool false
webmail.oauth.existingSecret string ""
webmail.oauth.extraScopes string ""
webmail.oauth.issuerUrl string "" Defaults to the top-level oidc.issuerUrl when unset and oidc.enabled.
webmail.oauth.only bool false Hides the username/password form, making OAuth the only login method.
webmail.oauth.scopes string ""
webmail.persistence.accessMode string "ReadWriteOnce"
webmail.persistence.enabled bool false
webmail.persistence.size string "2Gi"
webmail.persistence.storageClass string ""
webmail.podAnnotations object {}
webmail.podAntiAffinityPreset string "" Spreads replicas across nodes when affinity is left empty: "", "soft", or "hard" -- see the top-level podAntiAffinityPreset for details.
webmail.podLabels object {}
webmail.podSecurityContext.enabled bool true
webmail.podSecurityContext.fsGroup int 1000 The published image's non-root user; adjust if a custom image uses a different UID/GID.
webmail.podSecurityContext.fsGroupChangePolicy string "OnRootMismatch"
webmail.podSecurityContext.runAsGroup int 1000
webmail.podSecurityContext.runAsNonRoot bool true
webmail.podSecurityContext.runAsUser int 1000
webmail.podSecurityContext.seccompProfile.type string "RuntimeDefault"
webmail.readinessProbe.enabled bool true
webmail.readinessProbe.failureThreshold int 3
webmail.readinessProbe.initialDelaySeconds int 0
webmail.readinessProbe.periodSeconds int 10
webmail.readinessProbe.successThreshold int 1
webmail.readinessProbe.timeoutSeconds int 5
webmail.replicaCount int 1 Multiple replicas require a pinned adminPassword and shared sessionSecret. Use ReadWriteMany persistence, or disable persistence and settingsSync. Without persistence, config and audit data are local to each replica.
webmail.resources.requests.cpu string "100m"
webmail.resources.requests.memory string "256Mi"
webmail.service.annotations object {}
webmail.service.ipFamilies list []
webmail.service.ipFamilyPolicy string "" See the top-level service.ipFamilyPolicy.
webmail.service.port int 3000
webmail.service.type string "ClusterIP"
webmail.serviceAccount.annotations object {}
webmail.serviceAccount.automountServiceAccountToken bool false
webmail.serviceAccount.create bool true
webmail.serviceAccount.name string ""
webmail.sessionSecret.existingSecret string ""
webmail.sessionSecret.secretKey string "SESSION_SECRET"
webmail.sessionSecret.value string "" Fallback only, stored in a chart-managed Secret and visible in Helm's release history. Prefer existingSecret.
webmail.settingsSync.enabled bool false
webmail.stalwartFeatures bool true Enables Stalwart-specific WebUI features (password change, Sieve management, vacation responder, account security, API keys, admin dashboard). Matches Bulwark's own default.
webmail.startupProbe.enabled bool true
webmail.startupProbe.failureThreshold int 30
webmail.startupProbe.initialDelaySeconds int 0
webmail.startupProbe.periodSeconds int 5
webmail.startupProbe.successThreshold int 1
webmail.startupProbe.timeoutSeconds int 5
webmail.telemetry.enabled bool true
webmail.tolerations list []

License

Copyright 2026 Thomas Kaltenstein (Sovereign Systems). Licensed under the Apache License, Version 2.0. Bundled third-party components are listed in NOTICE. Stalwart and Bulwark images have their own licences.

About

A helm chart to deploy https://stalw.art/

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages