Skip to main content
By default the Enterprise stack runs Postgres (Nebula app + Hatchet) and MinIO inside the deploy boundary. For any production AWS deployment we recommend backing these onto RDS Postgres and real S3 instead — better durability, point-in-time recovery, IAM-driven credentials, and operational ownership by AWS. This page covers the AWS-side setup and the config knobs. The same managed-resource path works on both the Compose and EKS deploys.

RDS Postgres

Instance setup

  • Engine: PostgreSQL 16 (Aurora PostgreSQL 16 also works)
  • Databases: create the two logical databases (nebula and hatchet) before application bootstrap with nebula-enterprise postgres provision, unless your platform team already manages database/user creation through its own workflow. The application roles do not need CREATEDB.
  • Parameter group: vector, pg_partman, and pg_cron must be available to the master user; shared_preload_libraries must include pg_cron; cron.database_name must match the Nebula database name. Changing these settings may require a cluster reboot before bootstrap.
  • Network: private subnet in the same VPC as the compose host or EKS cluster. The host’s / cluster’s security group must be allowed inbound on :5432 from the application security group.
  • Storage: gp3, 100 GB minimum to start. Enable autoscaling up to ~1 TB; Nebula’s working set scales linearly with the number of collections + total ingested document volume.
  • Backups: RDS automatic backups, 7-day retention minimum. PITR is enabled by default.

AWS Terraform / OpenTofu

The Enterprise bundle includes a guided AWS workspace generator plus a focused RDS module at infra/aws-rds-postgres. The module creates the physical RDS PostgreSQL 16 instance, subnet group, security group, parameter group, RDS-managed master password, backups, CloudWatch Postgres log export, encryption, and Multi-AZ storage shape. It does not create Nebula/Hatchet roles or databases; run the logical bootstrap below after the instance is available. For first-time EKS installs, generate a workspace and run the emitted phase scripts:
./nebula-enterprise init aws \
  --output-dir nebula-prod-install \
  --eks-cluster-name <cluster-name> \
  --ecr-registry <account-id>.dkr.ecr.us-east-1.amazonaws.com \
  --vpc-id <vpc-id> \
  --subnet-id <private-subnet-a> \
  --subnet-id <private-subnet-b> \
  --allowed-security-group <eks-node-or-pod-sg> \
  --s3-bucket <bucket-name> \
  --service-account-role-arn <nebula-irsa-role-arn> \
  --eso-secret-path <secrets-manager-path> \
  --domain nebula.<your-domain> \
  --ingress-certificate-arn <acm-cert-arn>

./nebula-prod-install/scripts/00-seed-app-secret.sh
./nebula-prod-install/scripts/01-provision-rds.sh
./nebula-prod-install/scripts/02-install-nebula.sh
./nebula-prod-install/scripts/03-verify.sh
The scripts keep Terraform state in nebula-prod-install/terraform/aws-rds-postgres, write nebula-install.env with mode 0600, seed the AWS Secrets Manager app secret, fetch the RDS-managed admin password into PGPASSWORD, bootstrap and verify Postgres, run Helm, and verify the rollout. Export provider keys such as OPENAI_API_KEY before 00-seed-app-secret.sh; to update an existing secret, edit nebula-prod-install/app-secret.json and rerun with UPDATE_APP_SECRET=1. If you want to run Terraform manually instead, use the bundled module directly:
cd infra/aws-rds-postgres
cp terraform.tfvars.example terraform.tfvars
# Fill in aws_region, vpc_id, private subnet_ids, and the EKS/host security group allowed to reach RDS.
terraform init
terraform apply

terraform output -raw nebula_install_env >> ../../nebula-install.env
# Requires AWS CLI + jq.
export PGPASSWORD="$(
  aws secretsmanager get-secret-value \
    --secret-id "$(terraform output -raw master_user_secret_arn)" \
    --query SecretString \
    --output text | jq -r .password
)"
Keep the admin password out of nebula-install.env; prefer PGPASSWORD or .pgpass. If you do put any password value in the env file, run chmod 600 nebula-install.env before writing it.

Database / user provisioning

Use the bundle helper as the canonical external Postgres bootstrap. It creates the Nebula and Hatchet roles, databases, required Nebula DB extensions, and Kubernetes credential Secrets with the shapes the Helm chart expects:
./nebula-enterprise postgres provision \
  --namespace nebula \
  --admin-url "postgresql://postgres@<rds-endpoint>:5432/postgres?sslmode=require" \
  --nebula-database nebula \
  --nebula-user nebula \
  --nebula-secret nebula-postgres-credentials \
  --hatchet-database hatchet \
  --hatchet-user hatchet \
  --hatchet-secret hatchet-postgres-credentials
Set PGPASSWORD for the admin role. If you do not pass NEBULA_POSTGRES_PASSWORD / HATCHET_POSTGRES_PASSWORD in the environment, the helper generates stable random passwords, reusing existing Kubernetes Secrets on rerun. Pass --nebula-host / --hatchet-host only when application pods should connect through a hostname different from the --admin-url host. To assert an existing setup without changing Postgres or Kubernetes, run the verifier with the same arguments. It checks the database objects, required extensions, Secret shapes, and that the Secret credentials can authenticate with SELECT 1:
./nebula-enterprise postgres verify \
  --namespace nebula \
  --admin-url "postgresql://postgres@<rds-endpoint>:5432/postgres?sslmode=require" \
  --nebula-database nebula \
  --nebula-user nebula \
  --nebula-secret nebula-postgres-credentials \
  --hatchet-database hatchet \
  --hatchet-user hatchet \
  --hatchet-secret hatchet-postgres-credentials
For Compose installs, run the same helper with --skip-k8s-secrets and explicit NEBULA_POSTGRES_PASSWORD / HATCHET_POSTGRES_PASSWORD values in the environment, then place those same passwords in env/.env.enterprise.
NEBULA_POSTGRES_PASSWORD=<generate-32-bytes-random> \
HATCHET_POSTGRES_PASSWORD=<generate-32-bytes-random> \
PGPASSWORD=<admin-password> \
./nebula-enterprise postgres provision \
  --skip-k8s-secrets \
  --admin-url "postgresql://postgres@<rds-endpoint>:5432/postgres?sslmode=require" \
  --nebula-database nebula \
  --nebula-user nebula \
  --hatchet-database hatchet \
  --hatchet-user hatchet
Then assert the database-side contract:
PGPASSWORD=<admin-password> \
./nebula-enterprise postgres verify \
  --skip-k8s-secrets \
  --admin-url "postgresql://postgres@<rds-endpoint>:5432/postgres?sslmode=require" \
  --nebula-database nebula \
  --nebula-user nebula \
  --hatchet-database hatchet \
  --hatchet-user hatchet
For the EKS installer, set these values in nebula-install.env and run the normal install:
PROVISION_POSTGRES=1
POSTGRES_ADMIN_URL=postgresql://postgres@<rds-endpoint>:5432/postgres?sslmode=require
POSTGRES_HOST=<rds-endpoint>
HATCHET_POSTGRES_HOST=<rds-endpoint>
If you used infra/aws-rds-postgres, terraform output -raw nebula_install_env writes the Postgres values for this block. Fetch the admin password from the RDS-managed Secrets Manager secret as shown above. Before running the installer, set PGPASSWORD for the admin role or use .pgpass. If you set any password value in nebula-install.env, protect that file with chmod 600 before writing it. If your platform team provisions Postgres outside the bundle helper, mirror the same contract: distinct Nebula and Hatchet users, distinct logical databases, vector / pg_partman / pg_cron installed in the Nebula database, and a Hatchet database_url Secret that is already URL-encoded. Run nebula-enterprise postgres verify before Helm install to check that contract without granting the helper permission to mutate resources.

Compose: wire up via .env.enterprise

Append to env/.env.enterprise:
NEBULA_POSTGRES_HOST=<rds-endpoint>.us-east-1.rds.amazonaws.com
NEBULA_POSTGRES_PORT=5432
NEBULA_POSTGRES_USER=nebula
NEBULA_POSTGRES_DBNAME=nebula
NEBULA_POSTGRES_SSL_MODE=require
NEBULA_POSTGRES_PASSWORD=<same NEBULA_POSTGRES_PASSWORD passed to the helper>

HATCHET_POSTGRES_HOST=<rds-endpoint>.us-east-1.rds.amazonaws.com
HATCHET_POSTGRES_PORT=5432
HATCHET_POSTGRES_USER=hatchet
HATCHET_POSTGRES_DBNAME=hatchet
HATCHET_POSTGRES_SSL_MODE=require
HATCHET_POSTGRES_PASSWORD=<same HATCHET_POSTGRES_PASSWORD passed to the helper>
bootstrap.sh auto-detects the override and skips the in-stack postgres + hatchet-postgres containers via compose profiles. No other changes required.

EKS: wire up via Helm values

In your your-values.yaml (copied from helm/examples/eks/values.yaml):
postgres:
  mode: external
  host: <rds-endpoint>.us-east-1.rds.amazonaws.com
  port: 5432
  database: nebula
  credentialsSecret: nebula-postgres-credentials

hatchetPostgres:
  mode: external
  host: <rds-endpoint>.us-east-1.rds.amazonaws.com
  port: 5432
  database: hatchet
  credentialsSecret: hatchet-postgres-credentials
The provisioner creates those two Secrets for Kubernetes installs. If your platform workflow creates them instead, preserve the same shapes:
  • postgres.credentialsSecret (Nebula application DB): Kubernetes Secret with username and password keys (those exact lowercase key names — the chart reads them via secretKeyRef.key: username / .key: password).
  • hatchetPostgres.credentialsSecret (Hatchet orchestration DB): Kubernetes Secret with a single database_url key (override the key name via hatchetPostgres.databaseUrlKey) holding the full pre-encoded DSN, including ?sslmode=... and &sslrootcert=/etc/ssl/hatchet-postgres-ca.crt for TLS-strict Postgres. Hatchet v0.79 reads DATABASE_URL directly and does not URL-encode discrete fields, so the chart relies on the operator to provide an encoded URL. If your secret source only has discrete fields, compose database_url at sync time via ESO’s target.template.data directive — see eks.mdx for the canonical recipe.

S3 (object storage)

Bucket setup

  • Region: same as the compose host / EKS cluster (cross-region adds latency to every snapshot read)
  • Versioning: enabled (recommended; protects against accidental deletes)
  • Encryption: SSE-S3 or SSE-KMS
  • Public access: blocked at the account level
  • Lifecycle: optional — Nebula doesn’t expire its own objects, but you can set a policy on the incomplete-multipart-uploads prefix to clean up failed ingests after 7 days

IAM policy

The principal accessing S3 (instance profile / ECS task role / EKS IRSA role / IAM user) needs:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::<your-bucket>",
        "arn:aws:s3:::<your-bucket>/*"
      ]
    }
  ]
}
If you’re using SSE-KMS, also grant kms:Encrypt, kms:Decrypt, kms:GenerateDataKey on the KMS key ARN.

Compose: wire up via .env.enterprise

Append to env/.env.enterprise:
NEBULA_USE_EXTERNAL_S3=1

# Graph-engine (Rust) S3 config:
NEBULA_S3_BUCKET=<your-bucket>
NEBULA_S3_ENDPOINT_URL=
NEBULA_S3_REGION=us-east-1

# Python service S3 config (must reference the same bucket):
S3_BUCKET_NAME=<your-bucket>
S3_GRAPH_STORAGE_BUCKET=<your-bucket>
S3_ENDPOINT_URL=

# Empty AWS_*/S3_* credentials → SDK falls through to instance/task role:
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
The = with no value (e.g. NEBULA_S3_ENDPOINT_URL=) is intentional — it tells the AWS SDK to resolve the regional endpoint instead of falling back to the in-stack MinIO URL, and tells boto3 to use the default credential chain (instance profile, ECS task role) instead of static keys. bootstrap.sh auto-detects NEBULA_USE_EXTERNAL_S3=1 and skips the in-stack minio + minio-init containers.

EKS: wire up via Helm values

The example values file at helm/examples/eks/values.yaml has this pre-wired:
objectStorage:
  endpoint: ""              # empty → AWS SDK regional default
  bucket: <your-bucket>
  region: us-east-1
  forcePathStyle: false     # MUST be false for real AWS S3
  credentialsSecret: ""     # empty → SDK uses IRSA role on the SA

serviceAccount:
  create: true
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::<account-id>:role/nebula-irsa

Sanity checks

After re-bootstrapping with managed resources, verify:
CheckCommandExpected
App reaches RDScurl -fsS http://localhost:7272/v1/healthHealth endpoint returns OK
Migrations appliedAlembic head matches bundleTables collections, memories, entities exist in the nebula DB
Postgres extensions loaded\dx vector, \dx pg_partman, \dx pg_cron in psqlall three extensions are present
S3 writes succeedIngest a small docNew objects appear under <your-bucket>/nebula-graphs/
Hatchet workflows runhttp://localhost:7274Dashboard shows enqueued tasks

Migrating an existing in-stack deploy to managed resources

If you’ve been running with in-stack Postgres + MinIO and want to move to RDS + S3 without losing data:
  1. Dump in-stack Postgresdocker exec -t <postgres-container> pg_dumpall -U postgres > nebula-dump.sql
  2. Restore into RDSpsql -h <rds-endpoint> -U postgres < nebula-dump.sql
  3. Copy MinIO contents to S3aws s3 sync s3://nebula-files/ s3://<your-bucket>/ --source-region us-east-1 --region us-east-1 (with appropriate MinIO/S3 credentials)
  4. Update .env.enterprise with the managed-resource overrides above
  5. Restart: ./enterprise/bootstrap.sh — the script will pick up the overrides and skip the in-stack services
Test on a staging deployment before doing this in production; the cutover involves a brief downtime window between the in-stack stop and the managed-resource start.