Skip to content

HCL Domino

Latest chart version:

chartVersion: "1.13.2"

Custom chart for deploying an HCL Domino server cluster on Kubernetes using StatefulSets.

Architecture

Each Domino server is a separate StatefulSet with its own PVC and Service. Servers are grouped by role (type):

Type Role
admin First server — bootstraps the Domino domain, runs adminp/http/nomad
srvmgr Server manager — handles directory and replication routing
files File server — serves NSF databases over NRPC and HTTPS
mail Mail server — IMAP, DRAPI, Nomad. Optional split mailbox layout (see mailStorageA/B)
ontime OnTime Group Calendar integration server
smtp Outbound SMTP gateway
iq Dedicated Domino IQ server(s)
ldap LDAP proxy — NRPC + LDAPS (636)

Optionally, each pod runs a Grafana Alloy sidecar that ships notes.log, domino-keep.log, and Traveler logs (NTSAudit, NTSErrors, NTSUsage, NTSActivity) to Loki (enabled via alloy.enabled: true in environment values).

Each pod runs two init containers before Domino starts: - fix-data-dir — fixes PVC mount point ownership/permissions for uid 1000 (required for fresh volumes where the mount point is owned by root) - wait-for-admin — non-admin servers only; waits for dom-admin01a port 1352 to be available on first boot, then requires it to stay open continuously for 2 minutes before proceeding (dom-admin01a restarts itself internally after its own OTS bootstrap, briefly closing the port — a single successful check isn't enough, since a joining server's own registration attempt is one-shot and won't retry). Skipped entirely if notes.ini already exists.

Prerequisites

  • Kubernetes cluster with APISIX (routes managed via Admin API — no ingress controller required)
  • Custom dom-router Lua plugin installed on that APISIX cluster (per-user mail routing — not installed by this chart, see Default routes)
  • A StorageClass that supports ReadWriteOnce volumes
  • Image pull access to hclcr.io (secret: regcred)
  • (optional) Prometheus Operator with kube-prometheus-stack release label for ServiceMonitor discovery
  • (optional) Loki instance reachable from the cluster — required when alloy.enabled: true

Incremental deployment

Two per-server flags control deployment:

  • enabled (default true) — controls whether the StatefulSet, Service, and OTS config are created
  • register (default true) — controls whether the server is included in admin's autoRegister list (admin pre-creates the server ID during OTS)

The intended rollout order:

  1. dom-admin01a — bootstraps the domain from scratch (One-Touch Setup, type admin)
  2. dom-admin01b — joins the domain; the hook Job extracts its .id from dom-admin01a's PVC
  3. All remaining servers — same pattern

To enable the next server, set enabled: true and run helm upgrade.

Set register: true + enabled: false to have admin pre-register a server without starting it yet — the hook Job extracts its .id immediately, so the pod can start cleanly on the next deploy.

Startup behaviour

First boot (empty PVCs) — timing varies by server type, from a few minutes (admin/files/ldap) up to 15-20+ minutes for mail (larger storage, DAOS/mail-file initialization): 1. Non-admin pods stay Pending until the hook Job creates their server.id Secrets (Job waits for dom-admin01a to complete OTS and become Ready first) 2. wait-for-admin then waits ~2 more minutes to confirm dom-admin01a's port has stabilized past its own internal post-OTS restart (see Architecture) 3. Each non-admin server then runs its own OTS and restarts once internally as part of normal Domino setup

Subsequent boots — wait-for-admin is skipped (detects existing notes.ini), all pods up in ~2 minutes.

One-Touch Setup (OTS)

OTS configuration is stored in the ConfigMap domino-ots-setup with a separate key per server (DominoAutoConfig-<name>.json), mounted as /run/secrets/setup/DominoAutoConfig.json inside each container.

File structure

files/ots/
  first.json.tpl        # template for admin server (type: first)
  additional.json.tpl   # template for all other servers
  parts/                # JSON sections included by the templates above

The .tpl files are rendered as Helm templates. JSON sections in parts/ are included via $files.Get.

How values get into OTS

The final JSON is assembled entirely by Helm at render time: serverTasks, notesINI (merged from otsDefaults / otsTypes.<type> / servers.<name>.otsNotesINI), autoRegister.serverNames, IDVault, DAOSEnable (auto-set when daosStorage is defined on the server), the OIDC IdP Catalog (oidc.clients.*), and initial user registration (otsUsers.*).

Adding a new OTS section

Create a file in files/ots/parts/ and include it in the appropriate .tpl:

{{ tpl ($files.Get "files/ots/parts/daos.json") . | indent 6 | trim }},

If the section needs Helm values (.Values, .Name, .Server, etc.), wrap with tpl:

{{ tpl ($files.Get "files/ots/parts/daos.json") . | indent 6 | trim }},

Inclusion can be made conditional — for example, only for a specific server type:

{{- if eq $server.type "mail" }}, {{ $files.Get "files/ots/parts/daos.json" | indent 6 | trim }} {{- end }}

Server .id extraction (hook Job)

After each install/upgrade Helm runs a post-install,post-upgrade hook Job (domino-extract-ids) that:

  1. Waits for dom-admin01a-0 to be Ready
  2. For each non-admin server with enabled: true or register: true, waits until <name>.id appears on the dom-admin01a PVC
  3. Compares the file content with the existing Kubernetes Secret — creates or updates only if different
  4. The Job stays after completion for debugging; it is deleted before the next run (before-hook-creation)

Deploy

Inspect default values

helm registry login hclcr.io --username <username> --password <password>
helm show values oci://hclcr.io/hcl-domino-workspace-communicate/domino/charts/domino --version <version>

Available versions and chart contents are also browsable via the Harbor UI at hclcr.io.

Deployment via Helm

Download chart and create your own custom values file for your env if needed.

helm pull oci://hclcr.io/hcl-domino-workspace-communicate/domino/charts/domino --version <version>

Use your custom values file in installation command or ommit -f option to use default values.

helm upgrade --install domino \
  oci://hclcr.io/hcl-domino-workspace-communicate/domino/charts/domino \
  --version <version> \
  --create-namespace \
  --namespace <namespace> \
  -f <your-values.yml> \
  --timeout 30m

Values

Image

Key Default Description
image.repository <registry>/<path>/domino Domino container image
image.tag — Image tag
image.pullPolicy IfNotPresent Pull policy
imagePullSecrets [{name: regcred}] Registry credentials
kubectlImage alpine/k8s:1.35.5 Image used by the hook Job — pin to cluster server version
busyboxImage busybox:1.37 Image used by init-containers in the StatefulSet

Global Domino settings

Key Default Description
global.domainName — Domino domain name (overridable per server)
global.orgName — Domino org name (overridable per server)
global.adminServerName dom-admin01a CN of the admin server — FQDN is built automatically as <name>.<namespace>.svc.cluster.local
global.admin.firstName admin Admin user first name
global.admin.lastName Admin Admin user last name
global.admin.idFilePath /local/notesdata/admin.id Path to admin.id inside the container
global.admin.password "" Admin user password
global.certifierPassword "" Org certifier password
globalEnv {} Environment variables injected into every Domino container. Per-server extraEnv is merged on top — server wins on conflict.

OTS (One-Touch Setup)

Key Default Description
ots.firstConfigFile ots/first.json.tpl OTS template used for the admin (first) server
ots.additionalConfigFile ots/additional.json.tpl OTS template used for all other servers
otsDefaults.notesINI (see values.yaml) Lowest-priority notesINI layer — applied to all servers
otsTypes.<type>.serverTasks (per type) Domino server tasks string for that server type
otsTypes.<type>.notesINI {} Per-type notesINI overrides — merged above otsDefaults, below per-server
servers.<name>.otsNotesINI {} Per-server notesINI overrides — highest priority in the merge chain

Storage

Key Default Description
storageClassName "" Default StorageClass for all PVCs — must be set in environment values
servers.<name>.storageClassName (inherit) Per-server override — set to "" to use cluster default
servers.<name>.storage 50Gi Main data PVC size — chart default is 50Gi; srvmgr, files, ontime, smtp servers override to 20Gi in base values
servers.<name>.mailStorageA/B — Mail PVC sizes for split mailbox layout (/local/notesdata/maila and mailb) — type: mail only. When omitted, mail data stays on the main volume.
servers.<name>.daosStorage — DAOS PVC size (/local/daos). Works for any server type — set for mail servers with split storage and for files servers.
servers.<name>.nifnsfStorage 80Gi PVC size for /local/nifnsf — type: mail with split storage only.

Per-server options

Key Default Description
servers.<name>.enabled true Set to false to skip StatefulSet, Service, and OTS config
servers.<name>.register true Set to false to exclude from admin's autoRegister list (admin won't pre-create the server ID)
servers.<name>.type — Server role: admin, srvmgr, files, mail, ontime, smtp, iq, ldap
servers.<name>.nodeSelector {} Pin pod to a specific node (useful for storage data locality). When set, disables anti-affinity for that server.
servers.<name>.requireNodeLabels {} Restrict pod to nodes with these labels, e.g. {datacenter: dc-1}. Does not disable podAntiAffinity — pods still spread across matching nodes.
servers.<name>.avoidNodeLabels {} Node labels to avoid, e.g. {domino/exclude: "true"}.
servers.<name>.resources — Pod resource requests/limits
servers.<name>.terminationGracePeriodSeconds 300 Grace period; dom-admin01a defaults to 60
servers.<name>.domainName (global) Override domain for this server
servers.<name>.orgName (global) Override org for this server
servers.<name>.extraEnv {} Per-server environment variables — merged with globalEnv, server wins on conflict
servers.<name>.probesEnabled true Set to false to disable all probes (startup, liveness, readiness) for this server. Pod will not be restarted by Kubernetes regardless of health state.
servers.<name>.probes — Full override of the probe spec — replaces the type default. Type defaults: 30 min startupProbe (tolerates a slow first OTS bootstrap), fast livenessProbe/readinessProbe (normal failure detection once up).

Sequential rollout (hook Job)

Restarts StatefulSet pods in controlled batches on upgrade instead of all at once — each server is its own single-replica StatefulSet, so Kubernetes doesn't sequence restarts between them by default. Everything below only has an effect when sequentialRollout.enabled is true.

Key Default Description
sequentialRollout.enabled false When true, sets updateStrategy: OnDelete on affected StatefulSets and runs a post-upgrade hook Job (<release>-sequential-rollout) to restart them in batches.
sequentialRollout.groupOrder [] Batch order by the last character of each server name, e.g. ["b", "a"] = all *b servers, then all *a servers. Empty = one batch, alpha order. Unlisted suffixes go last.
sequentialRollout.parallelWithinGroup false true restarts a whole batch's changed servers together (still Ready-gated as a batch); false = one at a time within the batch.
sequentialRollout.podReadyTimeoutSeconds 1800 Per-server wait for Ready after restart.
sequentialRollout.jobActiveDeadlineSeconds 3600 Ceiling for the whole hook Job.

Servers whose StatefulSet revision didn't change are skipped, so a no-op upgrade restarts nothing. The Job is idempotent — a failed/re-run Job skips already-rolled-out servers.

Admin-specific bootstrap (type: admin)

Identity/password come from global.admin.* / global.certifierPassword. Remaining bootstrap parameters are passed via extraEnv on the admin server entry:

extraEnv key Description
DominoConfigRestartWaitTime Seconds to wait after OTS restart before checking the log
DominoConfigRestartWaitString String in the log that signals the server is ready after restart

OTS users

Key Default Description
otsUsers {} Map of users registered during admin OTS (files/ots/parts/reg-users.json.tpl) — arbitrary key, any number of entries, each with firstName/lastName/password/email. Empty by default. One-shot: only takes effect during dom-admin01a's initial bootstrap.

OIDC clients (IdP Catalog)

Key Default name Consuming app
oidc.clients.files file-server-oidc-client Files web (also apisix.filesOidcClientId)
oidc.clients.files_api file-api-oidc-client Files API (also apisix.filesApiOidcClientId)
oidc.clients.ontime ontime-web-oidc-client OnTime Group Calendar (internal to Domino)
oidc.clients.verse verse-web-oidc-client Mail, Calendar, and Contacts Web (internal to Domino)
oidc.clients.homepage landingpage dw-homepage
oidc.clients.grafana grafana Grafana
oidc.clients.huddoboards huddoboards Huddo Boards
oidc.clients.sametime sametime Chat and Meetings
oidc.clients.leap leap Forms
oidc.clients.search_api search-api-oidc-client Search API
oidc.clients.search_middleware search-middleware-oidc-client Search middleware (also apisix.oidc.clientId in the search chart)
oidc.clients.verse_mobile verse_mobile Mail, Calendar, and Contacts / Traveler mobile clients
oidc.clients.sametime_mobile sametime_mobile Chat and Meetings mobile client
oidc.clients.files_mobile files_mobile Files mobile client

Scheduling

Key Default Description
podAntiAffinity.enabled true Hard anti-affinity — prevents two pods of the same Domino role from landing on the same node. Requires enough nodes to satisfy the constraint, otherwise pods stay Pending. Automatically skipped when nodeSelector is set for a server.
servers.<name>.requireNodeLabels {} Per-server node label restriction (DC placement). Keeps podAntiAffinity active — pods spread across matching nodes.
servers.<name>.avoidNodeLabels {} Per-server node labels the pod must not land on.

Observability

Key Default Description
serviceMonitor.labels {release: kube-prometheus-stack} Labels for Prometheus discovery
serviceMonitor.interval 60s Scrape interval
serviceMonitor.scrapeTimeout 10s Scrape timeout
servers.<name>.serviceMonitor.enabled false Enable ServiceMonitor for this server — set to true in environment values
alloy.enabled false Enable Grafana Alloy sidecar for log forwarding to Loki — set to true in environment values
alloy.image — Alloy sidecar image
alloy.cluster "" Cluster label added to all log streams — required when enabled
alloy.loki.url "" Loki push endpoint — required when enabled
alloy.loki.tlsVerify true Set to false to skip Loki TLS verification
alloy.caBundleConfigMap "" Name of a ConfigMap containing the cluster CA bundle. When set, the ConfigMap is mounted and used as ca_file in the Alloy TLS config. Not needed if tlsVerify: false or Loki uses a publicly trusted certificate.
alloy.configMapName domino-log-alloy-config Name of the ConfigMap created for the Alloy sidecar config.
grafana.dashboards.enabled false Create Grafana dashboard ConfigMaps from grafana_dashboards/*.json
grafana.dashboards.labels {grafana_dashboard: "1"} Labels added to dashboard ConfigMaps for Grafana sidecar discovery

Keep config secret (keep-config-secret)

HCL Domino REST API (Keep) configuration is rendered into a Kubernetes Secret keep-config-secret from files/keep-config.json.

identitySecret is sensitive; the rest are plain config.

Key Description
keepConfig.tlsCertStoreName TLS cert store hostname. Leave empty to omit from config.
keepConfig.keepVersion Keep config version. Set to 0 to omit from config.
keepConfig.oidcProviderUrl OIDC provider URL
keepConfig.oidcAud OIDC audience (aud). List of allowed audience URLs.
keepConfig.oidcAdditionalClientIds Additional OIDC client IDs allowed to authenticate with KEEP. List, omitted if empty.
keepConfig.identitySecret Hashed secret for healthcheck/management/metrics API users
keepConfig.caldavActive Enable CalDAV support
keepConfig.carddavActive Enable CardDAV support

Secrets and extra objects

Key Description
serverSecrets Map of Secret name → data (base64).
extraObjects List of arbitrary Kubernetes resources to create (non-sensitive)

IMAP proxy

An optional Python asyncio TLS proxy that routes IMAP connections to the correct Domino mail server based on the user's MailServer LDAP attribute.

Key Default Description
imapProxy.enabled false Deploy the IMAP proxy
imapProxy.replicaCount 2 Number of replicas (soft anti-affinity spreads across nodes)
imapProxy.image.repository python Image repository
imapProxy.image.tag 3.12-alpine Image tag
imapProxy.ldap.host "" Required: LDAP server hostname
imapProxy.ldap.baseDn "" Required: base DN for user search
imapProxy.ldap.bindDn "" Bind DN for LDAP auth; leave empty for anonymous
imapProxy.ldap.bindPassword "" Chart creates the Secret automatically
imapProxy.ldap.bindPasswordSecret imap-auth-secret Name of the Secret containing the bind password
imapProxy.ldap.bindPasswordKey ldapBindPassword Key within the Secret
imapProxy.ldap.userFilter (mail=%s) LDAP filter to find user by email
imapProxy.ldap.mailServerAttr MailServer LDAP attribute with Domino server CN
imapProxy.ldap.cacheTtl 300 Seconds to cache username→server mapping
imapProxy.dominoNamespace "" Namespace where Domino mail servers run; defaults to Release.Namespace
imapProxy.imapPort 993 Port exposed by the Service and used to connect to backend Domino
imapProxy.listenPort 9993 Port the proxy listens on inside the container
imapProxy.defaultDomain "" Appended to bare usernames without @domain; defaults to top-level domain value
imapProxy.timeouts.auth 30 Seconds to wait for client LOGIN
imapProxy.timeouts.backend 10 Seconds to connect to Domino backend
imapProxy.timeouts.shutdown 30 Seconds to drain sessions on SIGTERM

APISIX Admin API integration

Routes, upstreams, and stream routes are defined in a dedicated file under charts/domino/files/ (default: apisix-routes.yaml). The file is a Go template — it supports {{ .Release.Namespace }}, {{ .Values.domain }}, {{ .Values.apisix.<key> }} for plain values, and {{ index .Values.apisix.routeSecrets "KEY" }} for sensitive values.

The APISIX admin key is set via apisix.adminKey — the chart creates the Secret automatically. Alternatively, create the Secret manually and reference it via apisix.secretName.

Key Default Description
domain "" Cluster base domain — used in route hostnames and OIDC endpoints. Required when apisix.enabled is true.
apisix.enabled false Enable APISIX Admin API integration
apisix.adminUrl "" APISIX Admin API URL (e.g. https://apisix-admin.example.com)
apisix.tlsVerify true Set to false to skip TLS certificate validation
apisix.secretName "" Name of the Secret containing the admin key
apisix.secretKey "key" Key within the Secret
apisix.configFile "apisix-routes.yaml" File from charts/domino/files/ to load routes/upstreams from
apisix.filesOidcClientId "file-server-oidc-client" OIDC client ID for the files-web route
apisix.filesApiOidcClientId "file-api-oidc-client" OIDC client ID for the files-api route
apisix.filesOidcScope "openid profile email offline_access" OIDC scopes — override to add custom scopes (e.g. vault scopes)
apisix.filesApiCorsOrigins "" Comma-separated allowed CORS origins for files-api.<domain>
apisix.filesApiCorsHeaders "Authorization,Content-Type,..." Allowed CORS request headers for files-api.<domain>
apisix.routeSecrets {filesOidcClientSecret: "changeme", filesApiOidcSessionSecret: "changeme", filesApiOidcClientSecret: "changeme"} Sensitive values referenced in the config file via {{ index .Values.apisix.routeSecrets "KEY" }}.
apisix.jobResources requests: 50m/32Mi, limits: 100m/64Mi Resources for the hook Job container
apisix.smtpIpRestriction.enabled false Enable ip-restriction plugin on the SMTP stream route
apisix.smtpIpRestriction.whitelist [] List of allowed IPs/CIDRs for SMTP access, e.g. ["10.0.0.1", "192.168.0.0/24"]
apisix.streamPorts.nrpc 1352 Port APISIX listens on for the domino-nrpc stream route
apisix.streamPorts.ldaps 636 Port APISIX listens on for the domino-ldaps stream route
apisix.streamPorts.smtp 25 Port APISIX listens on for the domino-smtp-lb stream route
apisix.streamPorts.imap 993 Port APISIX listens on for the domino-imap stream route

Route/upstream bodies map directly to the APISIX Admin API schema — see charts/domino/files/apisix-routes.yaml for the actual structure. routeSecrets holds the sensitive values, everything else is plain config.

routeSecrets reference:

  • filesOidcClientSecret — the OIDC client secret from your IdP for the file-server-oidc-client client (files-web).
  • filesApiOidcSessionSecret — a random string used to sign the files-api session cookies (e.g. openssl rand -hex 32).
  • filesApiOidcClientSecret — the OIDC client secret from your IdP for the file-api-oidc-client client (files-api).

Default routes (apisix-routes.yaml)

mail.<domain>, mobile.<domain> and drapi.<domain> routes require the dom-router plugin (see Prerequisites) for per-user mail server routing.

HTTP routes — all on <domain>, all with HTTP→HTTPS redirect:

Host Upstream Load balancing Notes
files.<domain> dom-files chash (remote_addr)
files-api.<domain> /picker/* dom-files chash (remote_addr) CORS only (public)
files-api.<domain> /* dom-files chash (remote_addr) CORS + OIDC
groupcal.<domain> dom-ontime chash (remote_addr)
dom-ontime01a.<domain> dom-ontime01a — direct access
dom-ontime01b.<domain> dom-ontime01b — direct access
mail.<domain> per-user, via dom-router — fallback: dom-advisor (roundrobin, all mail nodes)
mobile.<domain> /traveler* per-user, via dom-router — fallback: dom-mail-mobile-failover (chash, all mail nodes)
mobile.<domain> / — — redirects to /traveler
drapi.<domain> per-user, via dom-router — no static fallback upstream
dom-admin.<domain> dom-admin01a — port 80
auth.<domain> dom-admin01a — port 443
nomad.<domain> dom-admin01a — port 9443, WebSocket

dom-router picks a per-node upstream based on the user's mail server.

TCP stream routes (APISIX port defaults below — override via apisix.streamPorts.* if APISIX listens on different ports):

APISIX port (default) Destination Protocol
1352 (apisix.streamPorts.nrpc) dom-admin01a:1353 NRPC
636 (apisix.streamPorts.ldaps) dom-ldap (roundrobin) LDAPS
25 (apisix.streamPorts.smtp) dom-smtp (roundrobin) SMTP
993 (apisix.streamPorts.imap) imap-proxy:993 IMAPS (SNI: imap.<domain>)

To use different routes, create a new file in charts/domino/files/ and set apisix.configFile.

Useful commands

# Watch dom-admin01a startup
kubectl logs -f dom-admin01a-0 -n <namespace> -c domino

# Check hook Job result (server .id extraction)
kubectl logs -n <namespace> -l job-name=domino-extract-ids

# Check APISIX routes sync Job result
kubectl logs -n <namespace> -l job-name=domino-apisix-routes

# Verify a server.id Secret was created
kubectl get secret domino-secret-dom-admin01b -n <namespace> -o jsonpath='{.data.server\.id}' | base64 -d | xxd | head

# Port-forward dom-admin01a HTTPS for local testing
kubectl port-forward pod/dom-admin01a-0 8443:443 -n <namespace>

# Uninstall (PVCs are retained — delete manually if needed)
helm uninstall domino -n <namespace>

# List release revisions
helm history domino -n <namespace>

# Rollback to the previous revision
helm rollback domino -n <namespace>

# Rollback to a specific revision
helm rollback domino <REVISION> -n <namespace>