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-routerLua plugin installed on that APISIX cluster (per-user mail routing — not installed by this chart, see Default routes) - A StorageClass that supports
ReadWriteOncevolumes - Image pull access to
hclcr.io(secret:regcred) - (optional) Prometheus Operator with
kube-prometheus-stackrelease 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(defaulttrue) — controls whether the StatefulSet, Service, and OTS config are createdregister(defaulttrue) — controls whether the server is included in admin'sautoRegisterlist (admin pre-creates the server ID during OTS)
The intended rollout order:
dom-admin01a— bootstraps the domain from scratch (One-Touch Setup, typeadmin)dom-admin01b— joins the domain; the hook Job extracts its.idfrom dom-admin01a's PVC- 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:
- Waits for
dom-admin01a-0to be Ready - For each non-admin server with
enabled: trueorregister: true, waits until<name>.idappears on the dom-admin01a PVC - Compares the file content with the existing Kubernetes Secret — creates or updates only if different
- 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 thefile-server-oidc-clientclient (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 thefile-api-oidc-clientclient (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>