RDS Postgres
Instance setup
- Engine: PostgreSQL 16 (Aurora PostgreSQL 16 also works)
- Databases: create the two logical databases (
nebulaandhatchet) before application bootstrap withnebula-enterprise postgres provision, unless your platform team already manages database/user creation through its own workflow. The application roles do not needCREATEDB. - Parameter group:
vector,pg_partman, andpg_cronmust be available to the master user;shared_preload_librariesmust includepg_cron;cron.database_namemust 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
:5432from 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 atinfra/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-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:
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: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:
--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-install.env and run the normal install:
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:
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 youryour-values.yaml (copied from helm/examples/eks/values.yaml):
postgres.credentialsSecret(Nebula application DB): Kubernetes Secret withusernameandpasswordkeys (those exact lowercase key names — the chart reads them viasecretKeyRef.key: username/.key: password).hatchetPostgres.credentialsSecret(Hatchet orchestration DB): Kubernetes Secret with a singledatabase_urlkey (override the key name viahatchetPostgres.databaseUrlKey) holding the full pre-encoded DSN, including?sslmode=...and&sslrootcert=/etc/ssl/hatchet-postgres-ca.crtfor TLS-strict Postgres. Hatchet v0.79 readsDATABASE_URLdirectly 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, composedatabase_urlat sync time via ESO’starget.template.datadirective — 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-uploadsprefix 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:kms:Encrypt, kms:Decrypt, kms:GenerateDataKey on the KMS key ARN.
Compose: wire up via .env.enterprise
Append to env/.env.enterprise:
= 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 athelm/examples/eks/values.yaml has this pre-wired:
Sanity checks
After re-bootstrapping with managed resources, verify:| Check | Command | Expected |
|---|---|---|
| App reaches RDS | curl -fsS http://localhost:7272/v1/health | Health endpoint returns OK |
| Migrations applied | Alembic head matches bundle | Tables collections, memories, entities exist in the nebula DB |
| Postgres extensions loaded | \dx vector, \dx pg_partman, \dx pg_cron in psql | all three extensions are present |
| S3 writes succeed | Ingest a small doc | New objects appear under <your-bucket>/nebula-graphs/ |
| Hatchet workflows run | http://localhost:7274 | Dashboard 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:- Dump in-stack Postgres —
docker exec -t <postgres-container> pg_dumpall -U postgres > nebula-dump.sql - Restore into RDS —
psql -h <rds-endpoint> -U postgres < nebula-dump.sql - Copy MinIO contents to S3 —
aws s3 sync s3://nebula-files/ s3://<your-bucket>/ --source-region us-east-1 --region us-east-1(with appropriate MinIO/S3 credentials) - Update
.env.enterprisewith the managed-resource overrides above - Restart:
./enterprise/bootstrap.sh— the script will pick up the overrides and skip the in-stack services