Installation
graal installs on your side, through a Helm chart, into two namespaces of a Kubernetes cluster you administer. This page states what you need before you start, what the platform expects from your platform team, and which objects the installation creates — or does not create — in your cluster.
What you need
Section titled “What you need”| Item | Details |
|---|---|
| Kubernetes | A distribution that provides an ingress controller, certificate management, secret synchronisation and a PostgreSQL operator (see below). Two namespaces, provided by your platform team. Pods follow the PSA restricted profile |
| PostgreSQL | A dedicated application database. The reference deployment uses an operator that generates the role passwords itself |
| Keycloak 26 | One realm per tenant, plus an administration realm. Federating your own directory is possible |
| S3 object storage | MinIO, Ceph RGW or equivalent: data buckets and Apache Iceberg tables |
| An SMTP relay | Invitations, notifications, password resets. Without it those features stay silent — the platform still starts |
| An ingress controller and a certificate | A single hostname is enough: the console and the API share it |
| An image registry | One you control: platform images and runtime images (Python, Spark, Jupyter, VS Code) |
The SQL engine (Trino) and the experiment-tracking facade (compatible with the MLflow client, of which it implements a subset) are deployed by the platform; you do not need to provide them.
One constraint worth knowing about the registry: the runtime images repository must be pullable
anonymously, because the execution namespace holds no Secret — therefore no pull secret. The
platform images themselves may come from a private repository. Each runtime image is a chart value:
changing registry means changing those values, without touching any code.
The two namespaces
Section titled “The two namespaces”The installation occupies two namespaces, and the split is structural:
| Namespace | What lives there | Why |
|---|---|---|
| application namespace | console, API, low-code engine, platform services | this is the platform’s trust zone |
| execution namespace | users’ jobs and workspaces | a workspace runs user code: nothing confidential is mounted there, and no secret is materialised in it |
In the reference deployment they are called graal and graal-run. This separation is what lets us
say that a user running arbitrary code cannot read the platform’s credentials: they are not in their
namespace.
Runtime images are pulled anonymously from a public repository of your registry, with no pull secret, for the same reason. Corollary: nothing confidential, and never a customer’s image, in that repository.
What the installation does not create
Section titled “What the installation does not create”This is deliberate, and it is checked when the chart is rendered: the installation neither creates nor requests the right to create:
- no namespace — a namespace is a network boundary and a resource ceiling: it does not declare itself;
- no
Secret; - no cluster-scoped object: no
ClusterRole, noClusterRoleBinding, noStorageClass, noClusterIssuer; - no CRD installed by graal — the reference deployment does, however, consume the custom resources of four families provided by your platform: ingress, certificates, secrets and database;
- no
Role,RoleBinding,NetworkPolicy,ResourceQuotaorLimitRange: your guard rails stay yours, and a tenant that writes its own RBAC has no visible ceiling.
So what your platform team provides once, before the installation: the two namespaces with their
quota and network policy, an execution ServiceAccount in the execution namespace, and a
namespaced Role allowing the scheduler to create pods there. The scheduler no longer creates a
ServiceAccount, a RoleBinding or a pull secret per job.
Practical consequence: graal cannot be installed with a cluster-admin account by accident, and an audit of your permissions does not have to trust our documentation — the rendered chart can be read.
The variables that matter
Section titled “The variables that matter”The chart exposes many; six families decide whether the installation works.
GRAAL_SECRETS_SALT_KEY — mandatory, generated once
Section titled “GRAAL_SECRETS_SALT_KEY — mandatory, generated once”This is the root key of the tenant vault: a project’s secrets (your credentials to your own systems) are encrypted at rest in the database, with AES-256-GCM, using a key derived from this salt through HKDF-SHA256 — a distinct key per tenant, so that a value copied from one tenant to another does not open.
- Mandatory: missing, too short or empty, the API refuses to start, naming the variable. It is the only setting whose absence stops start-up, and that is deliberate — silently falling back to a default key would be worse than refusing.
- Permanent: changing it makes everything already sealed unreadable. The value set at the first installation is the one for the whole life of the installation.
- Generate it once, before the first installation, e.g.
openssl rand -hex 16(at least 32 characters), then back it up the way you back up a database encryption key. Losing it is losing the projects’ secrets.
Object storage
Section titled “Object storage”| Variable | Role |
|---|---|
GRAAL_S3_ENDPOINT_INTERNAL |
Required to create a tenant: the address through which the API and the pods reach the storage. Without it, creating a tenant fails with TENANT_CONFIGURATION_ERROR |
GRAAL_S3_ENDPOINT |
The storage’s public URL, the one handed to users. Falls back to the internal address |
GRAAL_S3_TYPE, GRAAL_S3_REGION, GRAAL_S3_PATH_STYLE_ACCESS, GRAAL_S3_SSL |
The defaults applied to every tenant |
The two addresses are separate because in practice they almost always differ: a pod reaches the storage through an internal service name, a user through a public one.
GRAAL_API_INTERNAL_URL
Section titled “GRAAL_API_INTERNAL_URL”The origin through which a pod in the execution namespace reaches the API back. It has no useful default in a real cluster, and its absence is invisible: runs fail with no explicit message in the interface. Set it explicitly, and verify it with a first real run rather than by reading health probes.
Keycloak
Section titled “Keycloak”| Variable | Role |
|---|---|
GRAAL_KEYCLOAK_URL |
Keycloak’s public origin. The expected token issuer is <origin>/realms/<realm> |
GRAAL_KEYCLOAK_AUTH_ENDPOINT_INTERNAL |
The same, as seen from a pod |
GRAAL_KEYCLOAK_JWKS_ENDPOINT |
Where to download the public keys. Does not change which issuer is accepted |
GRAAL_KEYCLOAK_ISSUERS |
The list of origins accepted as issuer |
GRAAL_KEYCLOAK_ADMIN_REALM, GRAAL_KEYCLOAK_ADMIN_CLIENT_ID, GRAAL_KEYCLOAK_ADMIN_USERNAME, GRAAL_KEYCLOAK_ADMIN_PASSWORD |
The account that provisions tenant realms |
GRAAL_KEYCLOAK_UI_CLIENT_ID, GRAAL_KEYCLOAK_UI_REDIRECT_URIS |
The console’s public client and its allowed redirect URIs |
GRAAL_KEYCLOAK_AGENT_CLIENT_ID |
A tenant’s “graal Application”: the identity of MCP agents |
The part that costs the most time: the public origin freezes the tokens’ issuer (iss). A token
obtained from outside is rejected inside — and the other way round — if the two views do not point at
the same hostname. That is why the public origin and the internal origin are two separate settings,
and why only the first decides what is accepted.
Database
Section titled “Database”The host, the port and the database are not three independent settings but a single URL. The password, in the reference deployment, comes from the PostgreSQL operator and never from a vault: two sources of truth for the same secret, one of them silently ignored, is a rotation that breaks the application with nothing tying it to its cause.
Mind the TLS mode: some managed PostgreSQL distributions refuse any cleartext connection. Where the database is managed, require TLS explicitly.
GRAAL_SMTP_HOST, GRAAL_SMTP_PORT, GRAAL_SMTP_USERNAME, GRAAL_SMTP_PASSWORD,
GRAAL_SMTP_AUTH, GRAAL_SMTP_STARTTLS.
Mail is optional: until it is configured the platform works and the mail health indicator stays off rather than declaring the platform unhealthy. An unconfigured feature must not turn the whole thing red.
Verifying the installation holds
Section titled “Verifying the installation holds”In this order, because each step assumes the previous one:
- The API starts. If it refuses, it names the missing variable — read the pod log before looking anywhere else.
- A tenant can be created. This is what exercises the object storage and Keycloak provisioning.
- A user signs in to the console through Keycloak. This is what exercises the public origin and the redirect URIs.
- A job really runs, and its log comes back. This is the only check that exercises the execution
namespace, the provided
ServiceAccount, image pulling andGRAAL_API_INTERNAL_URL. A platform with green probes may well never execute a single run: do not stop before this step.
Further reading
Section titled “Further reading”- Sovereignty and architecture — what stays with you, and why
- Security and governance
- Architecture — components and network
- AI agents (MCP)