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:
| Tool | Version | Purpose |
|---|---|---|
| Terraform | >= 1.5.0 | Infrastructure provisioning |
| AWS CLI | >= 2.x | AWS authentication |
| kubectl | >= 1.28 | Kubernetes management |
| Helm | >= 3.x | Render the Realm9 chart |
EKS Cluster
Realm9 is deployed on Amazon EKS. Your cluster should meet the following minimum requirements:
| Setting | Requirement |
|---|---|
| Kubernetes version | 1.28 or newer (1.33 tested) |
| Region | Any AWS region where your data may reside |
| Networking | VPC with public + private subnets across at least 2 AZs |
| API endpoint | Public, private, or both — must be reachable from your kubectl/helm client |
| Node OS | Any supported EKS AMI (Amazon Linux 2023 or Bottlerocket; ARM/Graviton recommended for cost) |
| Instance sizing | Sized for your workload — start with 2× general-purpose nodes (e.g. m7g.medium or m6i.large) |
| Ingress | AWS Load Balancer Controller (ALB) or NGINX Ingress |
| DNS | ExternalDNS + Route53, or any DNS solution that creates records from ingress hostnames |
| TLS | ACM certificate (with ALB) or cert-manager + Let's Encrypt (with NGINX) |
| Encryption | KMS-backed envelope encryption for EKS secrets (recommended) |
| Monitoring | CloudWatch 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; overriderealm9-postgresql.postgresql.volume.storageClassandredis-stack.master.persistence.storageClassif 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
kubectlandhelmclients can reach the cluster API endpoint before continuing. Runaws eks update-kubeconfig --name <cluster> --region <region>and verify withkubectl get nodes.
AWS Resources
Provision these in the same account/region as your EKS cluster before installing Realm9:
| Resource | Details |
|---|---|
| Route53 hosted zone | For your Realm9 domain (e.g. realm9.example.com) |
| ACM certificate | Issued for your Realm9 domain, validated via Route53 |
| S3 bucket — application data | User uploads, supporting documents (e.g. realm9-data) |
| S3 bucket — Terraform state | For Terraform runs initiated from inside Realm9 (e.g. realm9-terraform-state) |
| DynamoDB table | Terraform state locking (e.g. terraform-state-locks) |
| IAM role for IRSA | Allows Realm9 pods to access S3, DynamoDB, SES, etc. |
| AWS SES | Verified domain or email for outbound notifications |
External Services
| Service | Purpose |
|---|---|
| OpenAI API key | Required 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 upgradesredis-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-identityworks) - EKS cluster reachable from your client (
kubectl get nodesworks) - 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.yamlprepared (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.
| Section | What it configures |
|---|---|
realm9.realm9Domain | The hostname Realm9 is served on |
realm9.database | PostgreSQL connection (in-cluster Zalando or external RDS) |
realm9.postgresqlEnabled | Must match realm9-postgresql.enabled below |
realm9.externalSecrets | Pull RDS credentials from AWS Secrets Manager via External Secrets Operator |
realm9.nextAuth.secret | NextAuth signing secret — auto-generated and preserved on upgrades if empty |
realm9.openAi.apiKey | OpenAI API key for the AI assistant |
realm9.aws | Application S3 bucket and region |
realm9.ses | Sender email for notifications |
realm9.terraform | State bucket, lock table, and LSP settings |
realm9.realm9 | AWS account ID, IRSA role name, EKS cluster name, encryption key |
realm9.redisConnection | Redis host and password — password auto-generated and preserved on upgrades if empty |
realm9.ingress | Hostname, ingress class, cert-manager issuer, rate limiting, CORS |
realm9-postgresql | Bundled Zalando PostgreSQL (HA, connection pooler) — toggle via enabled |
redis-stack | Bundled 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.postgresqlEnabledandrealm9-postgresql.enabledmust agree. Set both totruefor in-cluster PostgreSQL, or both tofalsefor 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.docluster CR with 2 instances and PgBouncer - A pre-install Job (
init-db) that:- Waits for the Zalando-managed
pooleruser to exist - Creates the
realm9database owned bypooler - Installs extensions:
uuid-ossp,pgcrypto,pg_trgm - Grants schema/object/default privileges to
pooler
- Waits for the Zalando-managed
- 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 aspostgres. - 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-scmust exist in the cluster (gp3 EBS is recommended). If your cluster uses a different name, overriderealm9-postgresql.postgresql.volume.storageClass.
Option B — External AWS RDS (recommended for production)
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
| Setting | Recommended value | Notes |
|---|---|---|
| Engine | PostgreSQL 15 | Match the in-cluster default |
| Instance class | db.t4g.medium minimum | Scale up for production load |
| Storage | gp3, 100 GB starting | Enable autoscaling up to a sensible cap |
| Storage encryption | Enabled, KMS-backed | Use a customer-managed key for compliance |
| Multi-AZ | Enabled (production) | Provides automatic failover |
| VPC | Same VPC as the EKS cluster | Required for private connectivity |
| DB subnet group | Private subnets only | At least 2 AZs |
| Public access | Disabled | The DB must not be reachable from the internet |
| Backup retention | 7+ days (prod), 35 days max | Enables PITR |
| Backup window | Off-hours (e.g. 02:00–04:00 UTC) | |
| Maintenance window | Off-hours, different from backup | |
| Deletion protection | Enabled (production) | |
| Parameter group | Custom, with rds.force_ssl = 1 | Enforces TLS on every connection |
| Performance Insights | Enabled | 7-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
realm9database 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 skippg_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
ClusterSecretStorenamedaws-secrets-manager(or override viaexternalSecrets.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 fromrds-credentials. To make ESO work end-to-end, also setdatabase.existingSecretto 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:
- 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_superuserso the init-db Job can create the database on first install. - Stop the application to prevent writes during the dump:
kubectl scale deployment -n realm9-prod realm9 --replicas=0 - Dump the in-cluster database from the Zalando primary:
Thekubectl 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.dumppostgressuperuser password is in the secretpostgres.<release>-postgresql.credentials.postgresql.acid.zalan.do. - Pre-create an empty
realm9database on RDS sopg_restorehas 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 - Update
values.yamlper Option B (set bothrealm9.postgresqlEnabledandrealm9-postgresql.enabledtofalse). - Re-render and re-apply the chart (see Upgrading). The
init-db-externalJob will be a no-op because the database and extensions already exist. - Verify the app connects to RDS, then delete the leftover Zalando
postgresql.acid.zalan.docluster 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:
- Set up your first environment
- Connect your cloud accounts (optional)
- 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:
- Visit our GitHub repository
- Contact support: sales@realm9.app
