Installation With Helm
Overview
This guide documents the process for installing Monad into a Kubernetes cluster using Helm. The primary installation method uses the Kubernetes Gateway API for ingress. If your cluster uses a traditional ingress controller instead, see Alternative: Ingress Controller at the end of this document.
Phase 1: Prerequisites
These must be installed before the Monad Helm chart can be deployed.
Authentication Provider
Monad supports local authentication, Auth0, and AWS Cognito, with Auth0 being the default.
Required Operators/Controllers
-
Victoria Metrics Operator
- Required for internal metrics and dashboards, deployed by Custom Resources from within the chart. This does not replace an observability stack like Prometheus, and all components expose endpoints for scraping by an observability platform.
- Installation: https://docs.victoriametrics.com/operator/
-
CloudNativePG (CNPG) - PostgreSQL operator
- Required unless you're using an external PostgreSQL instance
- Installation: https://cloudnative-pg.io/documentation/current/installation_upgrade/
Gateway or Ingress Controller
- Monad recommends using the Kubernetes Gateway API for ingress and will create HTTPRoute and TCPRoute resources automatically. You need a Gateway API implementation installed in your cluster (e.g., Traefik, Istio, Envoy Gateway, kgateway, or any other conformant implementation).
- Your implementation must support the experimental Gateway API channel, which includes
TCPRoute. - Installation varies by implementation. Refer to your implementation's documentation.
- This guide assumes the use of Gateway API, though alternatives for using an ingress controller are provided at the end of the document.
Create Namespace
kubectl create namespace monad
Phase 2: Create Required Secrets
These secrets must exist before installing the Helm chart. All secrets are created in the monad namespace.
1. License Secret
The Monad license is a TLS certificate. You should have received a license.crt file from Monad.
kubectl create secret generic monad-license \
--from-file=license.crt=/path/to/license.crt \
-n monad
2. Encryption Key Secret
Used for encrypting sensitive data within Monad. This is a base64-encoded random 32-byte key.
# Generate the encryption key
ENCRYPTION_KEY=$(dd if=/dev/urandom bs=1 count=32 2>/dev/null | base64)
# Create the secret
kubectl create secret generic secret \
--from-literal=MONAD_ENCRYPTION_KEY="${ENCRYPTION_KEY}" \
-n monad
3. Image Pull Secret
You should have received credentials from Monad support for accessing the images in Docker Hub. Configure those here as default-pull-secret (required name).
kubectl create secret docker-registry default-pull-secret \
--docker-server=registry-1.docker.io \
--docker-username=monadinc \
--docker-password=<PERSONAL_ACCESS_TOKEN> \
-n monad
4. Authentication Secrets
Using External Secrets Operator
If you're using an external secrets store, you'll need to either save your secrets with the property names found in values.yaml or update the values.yaml to match your secret property names.
externalsecret:
enabled: true
data:
- secretKey: AUTH0_DOMAIN
remoteRef:
key: env-secrets
property: MONAD_AUTH_AUTH0_API_AUDIENCE # <-- update these to match your secret property names
- secretKey: AUTH0_API_AUDIENCE
remoteRef:
key: env-secrets
property: MONAD_AUTH_AUTH0_API_AUDIENCE
...
Not Using External Secrets Operator
If you're not using an External Secrets operator, you need to create secrets for authentication backend credentials. Below is an example file of key/value environment variable pairs that you can create a secret from.
Note: Some of these variables exist in two forms. Monad is migrating from variable names like AUTH0_CLIENT_ID to MONAD_AUTH_AUTH0_CLIENT_ID. The MONAD_-prefixed variables are the new names, and the old names will be deprecated in a future release. For current and future compatibility, use both until informed that the old names can be removed.
api.env
# Required
AUTH_SECRET=<AUTH_SECRET>
MONAD_AUTH_SECRET=<AUTH_SECRET>
JWT_SIGNING_KEY=<ANY RANDOM STRING>
# Required for AUTH0
MONAD_AUTH_TYPE=auth0
MONAD_AUTH_AUTH0_CLIENT_ID=<AUTH0_CLIENT_ID>
MONAD_AUTH_AUTH0_API_AUDIENCE=<AUTH0_API_AUDIENCE>
MONAD_AUTH_AUTH0_API_CLIENT_ID=<AUTH0_API_CLIENT_ID>
MONAD_AUTH_AUTH0_CLIENT_SECRET=<AUTH0_API_CLIENT_SECRET>
MONAD_AUTH_AUTH0_DOMAIN=<AUTH0_DOMAIN>
MONAD_AUTH_AUTH0_ISSUER=<AUTH0_ISSUER>
MONAD_AUTH_AUTH0_MACHINE_CLIENT_SECRET=<AUTH0_MACHINE_CLIENT_SECRET>
MONAD_AUTH_AUTH0_MACHINE_CLIENT_ID=<AUTH0_MACHINE_CLIENT_ID>
MONAD_AUTH_AUTH0_MACHINE_AUDIENCE=<AUTH0_MACHINE_AUDIENCE>
# Required for Cognito
MONAD_AUTH_TYPE=cognito
MONAD_AUTH_COGNITO_AWS_REGION=<COGNITO_AWS_REGION>
MONAD_AUTH_COGNITO_USER_POOL_ID=<COGNITO_USER_POOL_ID>
MONAD_AUTH_COGNITO_CLIENT_ID=<COGNITO_CLIENT_ID>
MONAD_AUTH_COGNITO_DOMAIN=<COGNITO_DOMAIN>
MONAD_AUTH_COGNITO_SECRET=<COGNITO_SECRET>
MONAD_AUTH_COGNITO_ISSUER_URL=<COGNITO_ISSUER_URL>
# Optional
GOOGLE_OAUTH_CLIENT_ID=<GOOGLE_OAUTH_CLIENT_ID>
GOOGLE_OAUTH_CLIENT_SECRET=<GOOGLE_OAUTH_CLIENT_SECRET>
ZOOM_OAUTH_CLIENT_ID=<ZOOM_OAUTH_CLIENT_ID>
ZOOM_OAUTH_CLIENT_SECRET=<ZOOM_OAUTH_CLIENT_SECRET>
Create the Secret api from the file:
kubectl create secret generic api \
--from-env-file=api.env \
-n monad
ui.env
# Required
AUTH_SECRET=<AUTH_SECRET>
# Required for Auth0
MONAD_AUTH_TYPE=auth0
AUTH_AUTH0_DOMAIN=<AUTH0_DOMAIN>
AUTH_AUTH0_CLIENT_ID=<AUTH0_CLIENT_ID>
AUTH_AUTH0_API_AUDIENCE=<AUTH0_API_AUDIENCE>
AUTH_AUTH0_CLIENT_SECRET=<AUTH0_CLIENT_SECRET>
AUTH_SECRET=<AUTH_SECRET>
# Required for Cognito
MONAD_AUTH_TYPE=cognito
AUTH_COGNITO_AWS_REGION=<COGNITO_AWS_REGION>
AUTH_COGNITO_USER_POOL_ID=<COGNITO_USER_POOL_ID>
AUTH_COGNITO_CLIENT_ID=<COGNITO_CLIENT_ID>
AUTH_COGNITO_DOMAIN=<COGNITO_DOMAIN>
AUTH_COGNITO_SECRET=<COGNITO_SECRET>
AUTH_COGNITO_ISSUER_URL=<COGNITO_ISSUER_URL>
Create the secret ui from the file:
kubectl create secret generic ui \
--from-env-file=ui.env \
-n monad
Phase 3: Configure TLS
For the remainder of this guide we will be using monad.example.com as our example domain.
Monad requires two TLS certificates:
1. Gateway Certificate
Used by the Gateway to terminate HTTPS for all web traffic. This is a standard certificate for your Monad hostname (e.g., monad.example.com).
The Secret must be created in the same namespace as your Gateway resource (not the monad namespace). This is a Gateway API requirement: certificate secrets must be co-located with the Gateway that references them.
See TLS with cert-manager for instructions on creating this certificate automatically.
2. HTTP Input TLS Certificate
Monad's HTTP Input workload handles inputs such as Syslog that require direct TLS termination at the Pod level. This certificate is separate from the Gateway certificate and must be named http-input-tls in the monad namespace.
Syslog and other TCP-terminated inputs use the hostname with SNI for routing, with names like cef85707-4b6e-405a-aea9-3237d520e805.l4.monad.example.com. It should answer to both l4.monad.example.com and the wildcard domain *.l4.monad.example.com. You will also need a DNS record pointing *.l4.monad.example.com to the load balancer address that handles TCP traffic into your cluster.
The full FQDN that you use for L4 traffic doesn't have to be connected to the hostname that you use for Monad itself (such as *.l4.monad.example.com and monad.example.com). As long as the FQDN you choose lands on the Gateway and is routed to the correct Service, the Pod that receives it performs a handshake, retrieves the requested FQDN from SNI, and then uses the hostname portion to route to the corresponding pipeline.
See TLS with cert-manager for instructions on creating this certificate automatically.
Configure Your Gateway
Your Gateway resource needs an HTTPS listener that references the Gateway certificate Secret. The exact configuration depends on your implementation, but the Gateway API spec looks like:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: <your-gateway>
namespace: <gateway-namespace>
spec:
gatewayClassName: <your-gateway-class>
listeners:
- name: web
port: 80
protocol: HTTP
allowedRoutes:
namespaces:
from: All
- name: websecure
port: 443
protocol: HTTPS
allowedRoutes:
namespaces:
from: All
tls:
mode: Terminate
certificateRefs:
- name: monad-example-com-crt
namespace: <gateway-namespace>
- name: tcp
port: 6514
protocol: TCP
allowedRoutes:
namespaces:
from: All
Refer to your Gateway implementation's documentation for how to configure listeners.
Phase 4: Configure values.yaml Overrides
Create a values-override.yaml file with the following configurations.
With Gateway API enabled, Monad's chart generates HTTPRoute resources automatically for all components. You do not need to configure ingress per-component — setting hostnames and routing at the top level is sufficient for all components to be reachable.
# The hostname(s) at which Monad will be accessible.
# All HTTPRoutes, backend URLs, and the UI origin are derived from this.
hostnames:
- monad.example.com
# Pull images from Docker Hub instead of GHCR
image:
repository: registry-1.docker.io/monadinc/
imagePullSecrets:
- name: default-pull-secret
# Disable the built-in Postgres cluster if using external Postgres
postgresql:
cnpg:
enabled: false
# Enable Gateway API routing
routing:
enabled: true
# Point all routes at your Gateway
routes:
default:
parentRefs:
- namespace: <gateway-namespace>
name: <your-gateway-name>
sectionName: websecure
otel:
parentRefs:
- namespace: <gateway-namespace>
name: <your-gateway-name>
sectionName: websecure
tcp:
parentRefs:
- namespace: <gateway-namespace>
name: <your-gateway-name>
sectionName: tcp
# Set this to your cluster's storageClassName (even if you have a default storage class, you have to override the setting in values.yaml)
nats:
config:
jetstream:
fileStore:
pvc:
storageClassName: <your-storage-class>
operator:
env:
MONAD_PIPELINE_IMAGE_REGISTRY:
value: registry-1.docker.io/monadinc/
The hostnames value replaces what previously required per-component BACKEND_URL environment variables and per-component ingress host configuration. Setting it once here propagates to all components automatically.
Phase 5: Install Monad
Log in to OCI Registry
Before you can pull the Helm chart, authenticate to the Docker registry:
helm registry login registry-1.docker.io --username monadinc
Password:
Login Succeeded
Note: Credentials are provided by Monad support. The login persists in ~/.docker/config.json.
Install with Custom Values
helm upgrade monad oci://registry-1.docker.io/monadinc/monad \
--install \
--namespace monad \
--values values-override.yaml \
--timeout 10m
You can perform upgrades of Monad using the same command above. Simply remove the --install directive to perform an upgrade.
Verify Installation
Some pods will initially come up in an Error state as they wait for the database to be ready. They should all be Running (or Completed) within a few minutes.
# Check all pods are running
kubectl get pods -n monad
# Check services
kubectl get svc -n monad
# Check Gateway API routes (replaces kubectl get ingress)
kubectl get httproute -n monad
kubectl get certificate -n monad
Phase 6: Post-Installation
At this point, Monad should be up and running. Access it at your designated hostname.
Alternative Installation Options
TLS with cert-manager
If you're using cert-manager for certificate management, create a ClusterIssuer and two Certificate resources as described below.
ClusterIssuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: your-email@example.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: traefik
Gateway Certificate
Create this in your Gateway's namespace (e.g., kube-system for Traefik on k3s):
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: monad-example-com-crt
namespace: <gateway-namespace>
spec:
secretName: monad-example-com-crt
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- monad.example.com
HTTP Input TLS Certificate
Create this in the monad namespace:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: http-input-tls
namespace: monad
spec:
secretName: http-input-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- l4.monad.example.com
- "*.l4.monad.example.com"
External Postgres
If you want to use an external Postgres instance, you can disable the CloudNativePG operator and provide the connection details in the monad-db-app Secret.
Below is an example file you can create a Secret from. It uses the following values:
dbname: monaduser: monadpassword: somereallylongandcomplexpasswordhost: monad-db-rw.postgres / monad-db-rw.postgres.svc.cluster.local (default CNPG service structure for a database in thepostgresnamespace)port: 5432
# db.env
dbname=monad
fqdn-jdbc-uri=jdbc:postgresql://monad-db-rw.postgres.svc.cluster.local:5432/monad?password=somereallylongandcomplexpassword&user=monad
fqdn-url=postgresql://monad:somereallylongandcomplexpassword@monad-db-rw.postgres.svc.cluster.local:5432/monad
host=monad-db-rw.postgres
jdbc-uri=jdbc:postgresql://monad-db-rw.postgres:5432/monad?password=somereallylongandcomplexpassword&user=monad
password=somereallylongandcomplexpassword
pgpass=monad-db-rw.postgres:5432:monad:monad:somereallylongandcomplexpassword
port=5432
uri=postgresql://monad:somereallylongandcomplexpassword@monad-db-rw.postgres:5432/monad
user=monad
username=monad
Create the Secret monad-db-app from the file:
kubectl create secret generic monad-db-app \
--from-env-file=db.env \
-n monad
Disable the CloudNativePG operator in your values-override.yaml:
postgresql:
cnpg:
enabled: false
Local Authentication
Local authentication creates an admin user of admin@monad.local and a random password. To activate this, remove the Auth0 and Cognito configuration from the api and ui secrets, and set MONAD_AUTH_TYPE to local for both components in your values-override.yaml:
ui:
env:
MONAD_AUTH_TYPE:
value: local
api:
env:
MONAD_AUTH_TYPE:
value: local
After completing the installation, retrieve the Secret with the username and password:
kubectl get secret app-bootstrap-admin -n monad \
-o go-template='{{range $k,$v := .data}}{{printf "%s: %s\n" $k ($v | base64decode)}}{{end}}'
This secret is only used to deliver credentials after installation and can be deleted after retrieval.
Local authentication does not allow the creation of more users than the local admin. If you wish to have multiple users, log in as the admin user and set up SSO from the Settings menu.
Ingress Controller
If your cluster uses a traditional Kubernetes Ingress resource rather than Gateway API, use the following values-override.yaml instead of the one in Phase 4. All other phases remain the same, except:
- The TLS certificate Secret should be created in the
monadnamespace instead of the Gateway namespace - Use
kubectl get ingress -n monadinstead ofkubectl get httproute -n monadto verify routing
Remove the routing and routes keys from the earlier example of values-override.yaml and add the following:
ingress:
className: "<your-ingress-class>" # e.g., traefik, nginx
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
tls:
- secretName: monad-example-com-crt
hosts:
- monad.example.com
api:
ingress:
enabled: true
docs:
ingress:
enabled: true
http-input:
ingress:
enabled: true
ui:
ingress:
enabled: true
The hostnames value at the top automatically configures backend URLs and the UI origin for all components. Per-component ingress.hosts configuration is handled by the chart defaults and does not need to be set explicitly.