Realm9 Logo
Search documentation...

Installation Guide

Realm9 is deployed on Kubernetes using Helm. This guide will help you get Realm9 running in your cluster in just a few minutes.

Prerequisites

Required Tools

Install these on the workstation you'll deploy from:

ToolVersionPurpose
Terraform>= 1.5.0Infrastructure provisioning
AWS CLI>= 2.xAWS authentication
kubectl>= 1.28Kubernetes management
Helm>= 3.xRender the Realm9 chart

EKS Cluster

Realm9 is deployed on Amazon EKS. Your cluster should meet the following minimum requirements:

SettingRequirement
Kubernetes version1.28 or newer (1.33 tested)
RegionAny AWS region where your data may reside
NetworkingVPC with public + private subnets across at least 2 AZs
API endpointPublic, private, or both — must be reachable from your kubectl/helm client
Node OSAny supported EKS AMI (Amazon Linux 2023 or Bottlerocket; ARM/Graviton recommended for cost)
Instance sizingSized for your workload — start with 2× general-purpose nodes (e.g. m7g.medium or m6i.large)
IngressAWS Load Balancer Controller (ALB) or NGINX Ingress
DNSExternalDNS + Route53, or any DNS solution that creates records from ingress hostnames
TLSACM certificate (with ALB) or cert-manager + Let's Encrypt (with NGINX)
EncryptionKMS-backed envelope encryption for EKS secrets (recommended)
MonitoringCloudWatch Container Insights or any Prometheus/Grafana stack

Required cluster add-ons (must be installed before deploying Realm9):

  • An ingress controller — AWS Load Balancer Controller or NGINX Ingress
  • A DNS automation solution — ExternalDNS is recommended for Route53; manual DNS works too
  • EBS CSI driver (or another StorageClass) — persistent volumes for PostgreSQL and Redis. The chart defaults to a StorageClass named ebs-sc; override realm9-postgresql.postgresql.volume.storageClass and redis-stack.master.persistence.storageClass if your cluster uses a different name
  • Metrics Server — required for HPA
  • cert-manager — required if you use the default ingress issuer (letsencrypt-prod); skip if you terminate TLS at the ALB with ACM
  • Zalando Postgres Operator — required only if you deploy in-cluster PostgreSQL (see Database Configuration)
  • External Secrets Operator — required only if you pull RDS credentials from AWS Secrets Manager (see Database Configuration)

Note: Make sure your kubectl and helm clients can reach the cluster API endpoint before continuing. Run aws eks update-kubeconfig --name <cluster> --region <region> and verify with kubectl get nodes.

AWS Resources

Provision these in the same account/region as your EKS cluster before installing Realm9:

ResourceDetails
Route53 hosted zoneFor your Realm9 domain (e.g. realm9.example.com)
ACM certificateIssued for your Realm9 domain, validated via Route53
S3 bucket — application dataUser uploads, supporting documents (e.g. realm9-data)
S3 bucket — Terraform stateFor Terraform runs initiated from inside Realm9 (e.g. realm9-terraform-state)
DynamoDB tableTerraform state locking (e.g. terraform-state-locks)
IAM role for IRSAAllows Realm9 pods to access S3, DynamoDB, SES, etc.
AWS SESVerified domain or email for outbound notifications

External Services

ServicePurpose
OpenAI API keyRequired for the AI assistant — bring your own key from platform.openai.com

Generated Secrets

Most secrets are managed by the chart automatically:

  • realm9.nextAuth.secret — auto-generated on first install if empty, preserved on upgrades
  • redis-stack.auth.password — auto-generated on first install if empty, preserved on upgrades
  • In-cluster PostgreSQL password — managed by the Zalando Postgres Operator

The one secret you must generate yourself is the application encryption key. Generate it now and store it in your secret manager:

# Application encryption key (required — overrides the chart's insecure default)
openssl rand -base64 32

You'll set this as realm9.realm9.encryptionKey in values.yaml. If you choose external RDS with an inline password (Option B1 in Database Configuration), generate one for that as well.

Pre-install Checklist

  • Terraform, AWS CLI, kubectl, Helm installed
  • AWS credentials configured (aws sts get-caller-identity works)
  • EKS cluster reachable from your client (kubectl get nodes works)
  • AWS Load Balancer Controller, ExternalDNS, EBS CSI driver installed in the cluster
  • Route53 hosted zone + ACM certificate ready
  • S3 buckets and DynamoDB lock table created
  • IRSA role created with permissions for S3, DynamoDB, SES
  • SES domain/email verified
  • OpenAI API key ready
  • Application encryption key generated
  • values.yaml prepared (see Configuration below)

Configuration

Realm9 is a Helm umbrella chart with three top-level sections: the application (realm9), the bundled Zalando-operated PostgreSQL (realm9-postgresql), and the bundled Redis Stack (redis-stack). All of your configuration goes into a single values.yaml.

SectionWhat it configures
realm9.realm9DomainThe hostname Realm9 is served on
realm9.databasePostgreSQL connection (in-cluster Zalando or external RDS)
realm9.postgresqlEnabledMust match realm9-postgresql.enabled below
realm9.externalSecretsPull RDS credentials from AWS Secrets Manager via External Secrets Operator
realm9.nextAuth.secretNextAuth signing secret — auto-generated and preserved on upgrades if empty
realm9.openAi.apiKeyOpenAI API key for the AI assistant
realm9.awsApplication S3 bucket and region
realm9.sesSender email for notifications
realm9.terraformState bucket, lock table, and LSP settings
realm9.realm9AWS account ID, IRSA role name, EKS cluster name, encryption key
realm9.redisConnectionRedis host and password — password auto-generated and preserved on upgrades if empty
realm9.ingressHostname, ingress class, cert-manager issuer, rate limiting, CORS
realm9-postgresqlBundled Zalando PostgreSQL (HA, connection pooler) — toggle via enabled
redis-stackBundled Redis Stack (also used as the vector DB) — toggle via enabled

Values you must change from defaults: realm9.realm9Domain, realm9.openAi.apiKey, realm9.aws.s3BucketName, realm9.ses.fromEmail, realm9.terraform.stateBucket, realm9.realm9.awsAccount, realm9.realm9.irsaRoleName, realm9.realm9.encryptionKey, realm9.realm9.eksClusterName, and realm9.ingress.enabled.

Values you can leave empty to auto-generate: realm9.nextAuth.secret, realm9.redisConnection.password, and the in-cluster PostgreSQL password (managed by the Zalando operator). Auto-generated values are preserved across upgrades via Helm lookup.

Database Configuration

Realm9 needs a PostgreSQL 15 database. You have two supported options — pick one before rendering the chart.

Toggle pair: realm9.postgresqlEnabled and realm9-postgresql.enabled must agree. Set both to true for in-cluster PostgreSQL, or both to false for external RDS. The chart's pre-install hook will fail loudly if they don't match.

Option A — In-cluster PostgreSQL (Zalando Postgres Operator)

The chart bundles PostgreSQL via the Zalando Postgres Operator. You get HA out of the box: 2 instances by default, plus a connection pooler (PgBouncer) sitting in front. The operator manages backups via WAL-E/WAL-G if you configure object storage.

Use this for: evaluation, dev, UAT, and small production deployments where you don't want a separate managed database.

Prerequisite: the Zalando Postgres Operator must already be installed in the cluster. It is not part of this chart — install it once per cluster:

helm repo add postgres-operator-charts https://opensource.zalando.com/postgres-operator/charts/postgres-operator
helm install postgres-operator postgres-operator-charts/postgres-operator \
  --namespace postgres-operator --create-namespace

What the chart provisions:

  • A postgresql.acid.zalan.do cluster CR with 2 instances and PgBouncer
  • A pre-install Job (init-db) that:
    • Waits for the Zalando-managed pooler user to exist
    • Creates the realm9 database owned by pooler
    • Installs extensions: uuid-ossp, pgcrypto, pg_trgm
    • Grants schema/object/default privileges to pooler
  • Application connections are routed via the connection pooler

values.yaml — defaults are fine for most deployments:

realm9:
  postgresqlEnabled: true        # MUST match realm9-postgresql.enabled

  database:
    user: pooler                 # Zalando-managed user
    instance: realm9             # Database name
    port: '5432'
    ssl: 'true'
    host: ''                     # Leave empty — auto-constructed
    password: ''                 # Leave empty — sourced from Zalando-managed secret

realm9-postgresql:
  enabled: true                  # MUST match realm9.postgresqlEnabled

  postgresql:
    version: '15'
    numberOfInstances: 2
    enableConnectionPooler: true
    connectionPooler:
      numberOfInstances: 2       # Drop to 1 for dev to save resources

    volume:
      size: '20Gi'
      storageClass: 'ebs-sc'

    resources:
      requests:
        cpu: 300m
        memory: 512Mi
      limits:
        cpu: 2000m
        memory: 2048Mi

Important characteristics:

  • The application connects as pooler, which is not a Postgres superuser. Migrations that require superuser (e.g. COMMENT ON, SET session_replication_role, CREATE EXTENSION) will fail. Realm9's migrations are written to work within these constraints — extensions are pre-installed by the init-db Job running as postgres.
  • The PostgreSQL master password is generated by the Zalando operator and stored in a secret named postgres.<release>-postgresql.credentials.postgresql.acid.zalan.do. Do not set it manually.
  • Storage class ebs-sc must exist in the cluster (gp3 EBS is recommended). If your cluster uses a different name, override realm9-postgresql.postgresql.volume.storageClass.

For production, point Realm9 at a managed RDS PostgreSQL instance. Multi-AZ, automated backups, point-in-time recovery, and storage scaling are handled by AWS.

Use this for: staging, production.

1. Provision the RDS instance

SettingRecommended valueNotes
EnginePostgreSQL 15Match the in-cluster default
Instance classdb.t4g.medium minimumScale up for production load
Storagegp3, 100 GB startingEnable autoscaling up to a sensible cap
Storage encryptionEnabled, KMS-backedUse a customer-managed key for compliance
Multi-AZEnabled (production)Provides automatic failover
VPCSame VPC as the EKS clusterRequired for private connectivity
DB subnet groupPrivate subnets onlyAt least 2 AZs
Public accessDisabledThe DB must not be reachable from the internet
Backup retention7+ days (prod), 35 days maxEnables PITR
Backup windowOff-hours (e.g. 02:00–04:00 UTC)
Maintenance windowOff-hours, different from backup
Deletion protectionEnabled (production)
Parameter groupCustom, with rds.force_ssl = 1Enforces TLS on every connection
Performance InsightsEnabled7-day retention free tier

2. Configure networking

The RDS instance must be reachable from EKS pods on TCP/5432:

  • Place RDS in the same VPC as the EKS cluster
  • Create an RDS security group that allows inbound 5432 from the EKS node security group (or the cluster security group, depending on your CNI)
  • Do not open 5432 to 0.0.0.0/0

3. Database, role, and extensions

You don't need to run any SQL manually. The chart includes a Helm pre-install hook (init-db-external Job) that runs on every helm install / helm upgrade and:

  • Creates the realm9 database if it doesn't exist
  • Installs the required extensions: uuid-ossp, pgcrypto, pg_trgm

All you need to provide is a database user (via realm9.database.user + database.password, or via a Kubernetes Secret) that already exists on RDS and has enough privileges to CREATE DATABASE and CREATE EXTENSION. The simplest way to satisfy this is to point the chart at the RDS master credentials — the master user already has CREATEDB and rds_superuser, so the init-db Job runs without any extra setup. If you provisioned RDS via Terraform and stored the master credentials in AWS Secrets Manager, the External Secrets Operator approach (option B3 in the next step) wires this up automatically.

If you'd rather have the application run as a least-privilege role instead of the master user, see the Hardening with a dedicated application role note at the end of this section.

Note: All three extensions (uuid-ossp, pgcrypto, pg_trgm) are on the RDS allow-list. Do not skip pg_trgm — Realm9's search features depend on it.

4. Disable the in-cluster PostgreSQL and point at RDS

You have three ways to provide the database password to the chart. Pick one.

B1 — Inline password (simplest, but the password ends up in values.yaml):

realm9:
  postgresqlEnabled: false       # MUST match realm9-postgresql.enabled

  database:
    user: realm9
    password: '<rds-realm9-password>'
    host: realm9-prod.cluster-abc123xyz.eu-west-2.rds.amazonaws.com
    instance: realm9
    port: '5432'
    ssl: 'true'

realm9-postgresql:
  enabled: false                 # MUST match realm9.postgresqlEnabled

B2 — Existing Kubernetes Secret (good for GitOps without External Secrets Operator):

kubectl create secret generic realm9-db-credentials \
  --namespace realm9-prod \
  --from-literal=password='<rds-realm9-password>'
realm9:
  postgresqlEnabled: false

  database:
    user: realm9
    host: realm9-prod.cluster-abc123xyz.eu-west-2.rds.amazonaws.com
    instance: realm9
    port: '5432'
    ssl: 'true'
    existingSecret:
      enabled: true
      name: realm9-db-credentials
      key: password

realm9-postgresql:
  enabled: false

B3 — AWS Secrets Manager via External Secrets Operator (recommended for production):

The chart has built-in support for pulling the RDS credentials directly from AWS Secrets Manager. The operator is responsible for password rotation; Realm9 picks up the rotated password on its next refresh.

Prerequisites:

  • External Secrets Operator installed in the cluster
  • A ClusterSecretStore named aws-secrets-manager (or override via externalSecrets.storeName) wired to your AWS account
  • An AWS Secrets Manager secret containing the keys: password, username, host, port, dbname. RDS-managed secrets (created when you enable "Manage master credentials in AWS Secrets Manager" on the RDS instance) match this shape out of the box; for self-managed secrets, populate the same five keys.
realm9:
  postgresqlEnabled: false

  externalSecrets:
    enabled: true
    rdsSecretName: '<your-rds-secret-name>'   # e.g. realm9-rds-credentials
    storeName: 'aws-secrets-manager'          # default, optional
    refreshInterval: '1h'                     # default, optional

  database:
    ssl: 'true'
    # user, host, port, instance, password are all sourced from the
    # rds-credentials Kubernetes secret materialised by External Secrets

realm9-postgresql:
  enabled: false

When externalSecrets.enabled: true, the chart provisions an ExternalSecret resource that materialises a rds-credentials Kubernetes secret in the release namespace; the application reads its database connection from there.

Important: The pre-install init-db Job (and the prisma-migrate Job) read the database password from database.existingSecret, not from rds-credentials. To make ESO work end-to-end, also set database.existingSecret to point at the materialised secret:

realm9:
  database:
    user: realm9
    host: <rds-endpoint>
    instance: realm9
    port: '5432'
    ssl: 'true'
    existingSecret:
      enabled: true
      name: rds-credentials   # materialised by the ExternalSecret above
      key: password

Hardening with a dedicated application role

By default the chart connects to RDS as whatever user you provide — typically the master. If you'd prefer a least-privilege application role instead, pre-create one as the master user before installing:

CREATE ROLE realm9 WITH LOGIN PASSWORD '<strong-password>';
GRANT rds_superuser TO realm9;     -- needed by init-db to CREATE EXTENSION
ALTER ROLE realm9 CREATEDB;        -- needed by init-db to CREATE DATABASE

Point realm9.database.user and realm9.database.password (or your existing/external Secret) at this role. After the first install, you can revoke rds_superuser to leave only LOGIN for steady-state operation — but you must re-grant it before any chart upgrade that touches extensions.

TLS / SSL between Realm9 and PostgreSQL

Realm9 sets sslmode=require when database.ssl: "true". This is sufficient for most deployments. For stricter compliance, use verify-full and mount the AWS RDS CA bundle (https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem) into the Realm9 pod via a ConfigMap, then override the connection mode through pod environment.

Migrating from in-cluster PostgreSQL to RDS

If you started on Option A and want to move to RDS without losing data:

  1. Provision the RDS instance and configure networking (Option B steps 1–2 above), and ensure your chosen database user exists on RDS with CREATEDB + rds_superuser so the init-db Job can create the database on first install.
  2. Stop the application to prevent writes during the dump:
    kubectl scale deployment -n realm9-prod realm9 --replicas=0
    
  3. Dump the in-cluster database from the Zalando primary:
    kubectl exec -n realm9-prod <release>-postgresql-0 -- \
      pg_dump -U postgres -d realm9 -Fc -f /tmp/realm9.dump
    kubectl cp realm9-prod/<release>-postgresql-0:/tmp/realm9.dump ./realm9.dump
    
    The postgres superuser password is in the secret postgres.<release>-postgresql.credentials.postgresql.acid.zalan.do.
  4. Pre-create an empty realm9 database on RDS so pg_restore has a target (or run a first chart install pointing at RDS to let init-db create it), then restore:
    pg_restore -h <rds-endpoint> -U <db-user> -d realm9 --no-owner --no-acl realm9.dump
    
  5. Update values.yaml per Option B (set both realm9.postgresqlEnabled and realm9-postgresql.enabled to false).
  6. Re-render and re-apply the chart (see Upgrading). The init-db-external Job will be a no-op because the database and extensions already exist.
  7. Verify the app connects to RDS, then delete the leftover Zalando postgresql.acid.zalan.do cluster CR and its PVCs.

Install Realm9

Realm9 is published as an OCI Helm chart on AWS ECR Public. The supported install workflow renders the chart locally and applies it with kubectl — this keeps the deployed manifests reviewable, GitOps-friendly, and avoids storing Helm release state in the cluster.

1. Render the chart

helm template realm9 oci://public.ecr.aws/realm9/helm/realm9 \
  --version 1.0.31 \
  --namespace realm9-prod \
  -f values.yaml \
  > /tmp/realm9-manifests.yaml

Inspect /tmp/realm9-manifests.yaml before applying — this is the moment to review what's about to land in your cluster.

2. Create the namespace

kubectl create namespace realm9-prod --dry-run=client -o yaml | kubectl apply -f -

3. Apply the manifests

kubectl apply -f /tmp/realm9-manifests.yaml -n realm9-prod

Verification

After applying, watch the pods come up:

# Pod status
kubectl get pods -n realm9-prod

# Services
kubectl get svc -n realm9-prod

# Ingress (ALB hostname appears once provisioned)
kubectl get ingress -n realm9-prod

# Application logs
kubectl logs -n realm9-prod -l app.kubernetes.io/name=realm9 --tail=50 -f

All pods should reach Running within a few minutes. The ingress will take an extra minute or two to provision the ALB and create the Route53 record via ExternalDNS.

Access Realm9

Once the ALB is provisioned and DNS has propagated, navigate to your Realm9 domain:

https://realm9.example.com

The first user to sign up is automatically granted the Admin role and becomes the organisation owner. Use a corporate email address.

After signing in:

  1. Set up your first environment
  2. Connect your cloud accounts (optional)
  3. Configure SSO / SAML for your identity provider (optional)

Upgrading

To upgrade to a newer chart version, repeat the install workflow with the new version pinned:

helm template realm9 oci://public.ecr.aws/realm9/helm/realm9 \
  --version <new-version> \
  --namespace realm9-prod \
  -f values.yaml \
  > /tmp/realm9-manifests.yaml

kubectl apply -f /tmp/realm9-manifests.yaml -n realm9-prod

kubectl apply will reconcile only the resources that changed.

Uninstallation

To remove Realm9:

kubectl delete -f /tmp/realm9-manifests.yaml -n realm9-prod
kubectl delete namespace realm9-prod

Warning: Deleting the namespace removes the in-cluster PostgreSQL and Redis PersistentVolumeClaims along with their data. Back up your database before uninstalling if you intend to redeploy.

Troubleshooting

Pods not starting

kubectl describe pod -n realm9-prod <pod-name>
kubectl logs -n realm9-prod <pod-name> --previous

Database connection errors

In-cluster (Zalando): verify the PostgreSQL cluster CR is healthy and the connection pooler is running:

kubectl get postgresql -n realm9-prod
kubectl get pods -n realm9-prod -l application=spilo
kubectl get pods -n realm9-prod -l application=db-connection-pooler

# Logs from the primary
kubectl logs -n realm9-prod <release>-postgresql-0 --tail=100

# Check the init-db Job (should be Complete)
kubectl get jobs -n realm9-prod -l app.kubernetes.io/component=init-db
kubectl logs -n realm9-prod -l app.kubernetes.io/component=init-db --tail=200

External RDS: confirm the RDS security group allows traffic from the EKS node SG, the database user exists with the right privileges, and the password in values.yaml (or your secret) matches RDS. Inspect the init-db Job to see exactly where it failed:

# init-db Job status and logs
kubectl get jobs -n realm9-prod -l app.kubernetes.io/component=init-db-external
kubectl logs -n realm9-prod -l app.kubernetes.io/component=init-db-external --tail=200

If you're using externalSecrets.enabled: true, also check that the ExternalSecret materialised:

kubectl get externalsecret -n realm9-prod rds-credentials
kubectl get secret -n realm9-prod rds-credentials -o jsonpath='{.data.host}' | base64 -d

Cannot reach Realm9 via the domain

Check that the ALB was provisioned and ExternalDNS created the Route53 record:

kubectl get ingress -n realm9-prod
kubectl logs -n kube-system -l app.kubernetes.io/name=external-dns --tail=50

kubectl commands hang or time out

If kubectl get nodes also hangs, the issue is cluster connectivity, not Realm9. Re-run aws eks update-kubeconfig --name <cluster> --region <region> to refresh your kubeconfig and confirm your client has network access to the cluster API endpoint.

Next Steps

Support

For installation issues: