Install with Helm
Install the Dibbla platform onto an existing Kubernetes cluster.
The Helm chart at oci://ghcr.io/dibbla-agents/charts/dibbla installs the full platform — six platform services (API Gateway, Auth Service, Deploy API, Workflow Server, Go Toolserver, Feedback Service) plus PostgreSQL, Redis, MinIO, Loki + Grafana Alloy for log shipping, and (by default) BuildKit and a container registry. This page assumes you already have a cluster; if not, start with K3s or MicroK8s.
The Self-hosting guide generates a values.yaml tailored to
your cluster type, auth mode, data plane, and TLS setup. This page is the
manual reference.
This page’s values.yaml assumes you’re running the bundled Auth Service in your cluster. If you’d rather point at Dibbla’s hosted auth (no OAuth setup, no in-cluster Postgres for auth), read Configure authentication first — it shows the values overrides and skips most of the secrets you’d otherwise need to gather below.
Prerequisites
- A Kubernetes cluster meeting the overview prerequisites.
helm3.8+ andkubectlconfigured for the target cluster.- A wildcard DNS record
*.<your-base-domain>pointing at your ingress IP. - cert-manager installed with a
ClusterIssuer(recommended for automatic TLS). - These secrets ready:
- GHCR PAT with
read:packagesto pull platform images. - OAuth client — Google client ID + secret, and/or Microsoft Entra
clientId/clientSecret/tenantId. At least one provider is required. - JWT secret and signing key — generate each with
openssl rand -hex 32. - Workflow root token + OAuth encryption key — two more 32-byte hex keys for
workflow-server. - Anthropic and/or OpenAI API keys — required by
go-toolserverfor the workflow tools. - PostgreSQL passwords —
dibblauser +postgressuperuser. Generate any way you like. - MinIO root password — required by the bundled object storage; generate any way you like. Deploy API holds this credential to provision per-app buckets (
dibbla storage); apps themselves only ever receive keys scoped to their own bucket.
- GHCR PAT with
Minimum values.yaml
global:
baseDomain: example.com
imagePullSecret:
create: true
registry: ghcr.io
username: <your-github-username>
token: <your-ghcr-pat>
postgresql:
enabled: true
username: dibbla
password: <your-postgres-password>
superuserPassword: <your-postgres-superuser-password>
redis:
enabled: true
minio:
enabled: true
rootUser: minio
rootPassword: <your-minio-root-password>
loki:
enabled: true
alloy:
enabled: true
auth-service:
google:
clientId: <your-google-client-id>
clientSecret: <your-google-client-secret>
# microsoft: # uncomment to also enable Microsoft Entra
# clientId: <your-ms-client-id>
# clientSecret: <your-ms-client-secret>
# tenantId: <your-ms-tenant-id>
jwt:
secret: <your-jwt-secret>
signingKey: <your-signing-key>
redirectBaseUrl: https://app.example.com
allowedRedirectBaseUrls: https://app.example.com
api-gateway:
ingress:
enabled: true
type: standard
className: nginx
hosts:
- host: app.example.com
- host: api.example.com
tls:
- secretName: dibbla-tls
hosts:
- app.example.com
- api.example.com
deploy-api:
ingressClass: nginx
# workflow-server reuses auth-service's OAuth + JWT credentials so it can
# validate the same tokens. Keep these in sync with auth-service above.
workflow-server:
rootToken: <your-workflow-root-token>
oauthEncryptionKey: <your-oauth-encryption-key>
googleClientId: <your-google-client-id>
googleClientSecret: <your-google-client-secret>
jwtSecret: <your-jwt-secret>
redirectBaseUrl: https://app.example.com
go-toolserver:
anthropicApiKey: <your-anthropic-key>
openaiApiKey: <your-openai-key>
The app.example.com host serves the Auth UI (sign-in) and Deploy UI (dashboard). The api.example.com host serves the API. Adjust to your conventions.
The example above is the bundled-everything path — Postgres, Redis, MinIO (object storage: workflow artifacts, and the buckets dibbla storage provisions for your apps), Loki (log storage), and Alloy (log shipping) all run in-cluster. To bring your own managed Postgres / Redis / S3 / Loki, see External infrastructure below.
Install
helm install dibbla oci://ghcr.io/dibbla-agents/charts/dibbla \
--namespace dibbla --create-namespace \
-f values.yaml
After a minute or so:
kubectl -n dibbla get pods
You should see all platform services Running:
dibbla-api-gateway-… 1/1 Running
dibbla-auth-service-… 1/1 Running
dibbla-deploy-api-… 1/1 Running
dibbla-workflow-server-… 1/1 Running
dibbla-go-toolserver-… 1/1 Running
dibbla-feedback-service-… 1/1 Running
dibbla-postgresql-0 1/1 Running
dibbla-redis-master-0 1/1 Running
dibbla-minio-… 1/1 Running
dibbla-loki-0 1/1 Running
dibbla-alloy-… 1/1 Running # one per node (DaemonSet)
dibbla-buildkit-… 1/1 Running # if enabled
dibbla-container-registry-… 1/1 Running # if enabled
Continue to Configure authentication to create the first admin user.
Routing strategy
deploy-api.routingStrategy controls how the Deploy API wires DNS for newly deployed apps. Four valid values:
| Value | Behavior | Required values |
|---|---|---|
wildcard (default) | No DNS automation — the API Gateway routes by Host header. You manage your own wildcard DNS pointing at the ingress. | none |
k8s-ingress | Creates an Ingress per app and a Cloudflare CNAME pointing at the ingress entrypoint. | cloudflare.apiToken, cloudflare.zoneId, ingressHostTarget |
cloudflare-tunnel | Adds a tunnel route per app on the existing Cloudflare Tunnel. No Kubernetes Ingress per app. | full cloudflare.* block |
cloudflare-tunnel-ingress | Tunnel + Ingress hybrid (what Dibbla’s hosted platform uses). | cloudflare.apiToken, cloudflare.zoneId, cloudflare.tunnelId |
For most self-hosted installs the default wildcard is the right choice — point a wildcard A record at your ingress IP and you’re done. The Cloudflare modes are for installs that want per-app DNS records or run behind a Cloudflare Tunnel.
TLS
Add a cert-manager ClusterIssuer (Let’s Encrypt HTTP-01 or DNS-01) and annotate the ingress through the chart values. The example above expects a dibbla-tls secret to exist; cert-manager can produce it automatically when annotated correctly.
A minimal HTTP-01 issuer:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-account-key
solvers:
- http01:
ingress:
class: nginx
Then add to your api-gateway.ingress block:
api-gateway:
ingress:
annotations:
cert-manager.io/cluster-issuer: letsencrypt
External infrastructure
Disable the bundled subcharts and point at your managed instances:
global:
postgresql: { enabled: false }
redis: { enabled: false }
minio: { enabled: false }
loki: { enabled: false }
alloy: { enabled: true } # still needed to ship logs to externalLoki
externalPostgresql:
host: <your-postgres-host>
port: 5432
username: dibbla
password: <your-postgres-password>
sslmode: require
externalRedis:
host: <your-redis-host>
port: 6379
db: 0
externalMinio:
host: <your-s3-host>
port: 443
# Prefer a key scoped to the buckets Dibbla uses over a store-wide one.
accessKey: <your-s3-access-key>
secretKey: <your-s3-secret-key>
useSsl: "true"
bucket: workflows
externalLoki:
pushEndpoint: <your-loki-push-endpoint> # e.g. https://logs.example.com/loki/api/v1/push
queryEndpoint: <your-loki-query-endpoint> # used by deploy-api LOKI_URL
postgresql: { enabled: false }
redis: { enabled: false }
minio: { enabled: false }
loki: { enabled: false }
PostgreSQL must be reachable from the cluster, must support the dibbla user, and the chart will create databases (auth_db, agent_db, workflows, feedback_db, ai_gateway_db, notify_db) on first start. MinIO can be any S3-compatible object store (AWS S3, GCS in S3-compatibility mode, etc.). External Loki can be Grafana Cloud or self-hosted.
The chart ships single-pod Postgres (pgvector/pgvector:pg17 — Postgres 17
with pgvector) and Redis (redis:7-alpine) subcharts. Fine for evaluation and
small production. For HA, backups, or point-in-time recovery, disable the
bundled subchart and use external managed Postgres (CloudNativePG, RDS, etc.)
via global.externalPostgresql.*.
Platform connector (MCP over OAuth)
The platform connector lets an AI agent — Claude, an MCP-capable IDE, dibbla mcp platform — talk to your install through an OAuth-protected MCP endpoint at https://mcp.<base-domain>/platform. Two services take part: the Auth Service becomes an OAuth 2.1 authorization server on its own hostname, auth.<base-domain> (RFC 8414 discovery lives at the issuer’s root, which api.<base-domain>/auth/… cannot provide), and the MCP server serves the toolset plus its RFC 9728 resource metadata on mcp.<base-domain>.
It is off by default. With the switch off, every /oauth/*, /.well-known/* and /platform path answers 404 on both services, and nothing else in the install changes.
Prerequisites
- DNS:
mcp.<base-domain>andauth.<base-domain>must resolve to your ingress controller. The wildcard record from the overview already does. - TLS: the certificate must cover both names — a
*.<base-domain>wildcard cert does. Both hosts arehttpsonly; the services refuse anhttpissuer or resource. - Routing: both hosts go straight from the ingress controller to the service, never through the API Gateway. The chart renders those routes for you (below) and makes sure they win over the
*.<base-domain>catch-all that serves your deployed apps.
Values
global:
baseDomain: example.com
# Shared defaults for the two connector hosts. The api-gateway keeps its own
# api-gateway.ingress block — set both.
ingress:
enabled: true
type: standard # "traefik" on k3s (renders IngressRoutes with a priority above the apps wildcard)
className: nginx
tls:
- secretName: dibbla-tls # must cover mcp.example.com and auth.example.com
platformConnector:
enabled: true
actorAssertionSecret: <openssl rand -hex 32> # shared by both services; NOT the JWT secret
auth-service:
jwt:
secret: <your-jwt-secret>
mcp-server:
platform:
jwtSecret: <your-jwt-secret> # the same value as auth-service.jwt.secret
That is the whole configuration. The issuer (https://auth.example.com) and the resource (https://mcp.example.com/platform) are derived from global.baseDomain and match the routes the chart renders; you only set global.platformConnector.issuer / .resource if the hosts differ, and then the chart refuses a resource its route does not serve.
helm upgrade fails before touching the cluster if the two secrets are
missing or equal, if the JWT secret differs between the services, if the
services would disagree on issuer or resource, or if /platform is on while
the authorization server is off. A connector that boots half-configured would
answer with the wrong identity; the chart treats that as an error.
What you get, and where to turn it:
| Value | Default | |
|---|---|---|
Route mcp.<base-domain> → MCP server | mcp-server.ingress.* (inherits global.ingress) | on when global.ingress.enabled |
Route auth.<base-domain> → Auth Service API | auth-service.ingress.* (inherits global.ingress) | on when global.ingress.enabled and the connector is on |
| Per-IP rate limit at the edge | mcp-server.ingress.rateLimit, auth-service.ingress.rateLimit | on — a Traefik Middleware, or nginx limit-rps annotations |
Writes through /platform | mcp-server.writes.enabled | off — the toolset is read-only until you turn this on |
| NetworkPolicy peer for the ingress controller | auth-service.networkPolicy.additionalIngressAllowLabels | app.kubernetes.io/name: traefik; list ingress-nginx on nginx |
Verify
curl -fsS https://auth.example.com/.well-known/oauth-authorization-server | jq .issuer
# "https://auth.example.com"
curl -fsS https://mcp.example.com/.well-known/oauth-protected-resource/platform | jq '.resource, .authorization_servers'
# "https://mcp.example.com/platform" ["https://auth.example.com"]
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://mcp.example.com/platform
# 401 — mounted and challenged (404 means the toolset is off)
The issuer must equal the URL you fetched it from and the resource must equal https://mcp.<base-domain>/platform, character for character: clients compare them exactly.
Per-server MCP addresses
With the connector on, one more switch gives every tool server an MCP address
of its own, https://mcp.<base-domain>/platform/servers/<name>, whose tools
are that server’s functions. It is what lets a developer
publish an app’s tool server as an MCP.
global:
platformConnector:
enabled: true
servers:
enabled: true # default false
It is off by default. Off, every /platform/servers/… path answers 404
and the chart renders exactly what it rendered before the value existed. On:
- the MCP server serves the addresses, and the Auth Service accepts each one
as an OAuth resource of its own — a sign-in there yields a token for that
one server, never for
/platform; - a deploy with an
mcp:line answers “published” rather than a notice that the addresses are switched off; - the console shows each tool server’s address.
/platform itself does not change: its tools, scopes, resource metadata and
consent page are the same with the switch on. The addresses need the
connector — the chart refuses to render servers.enabled: true while
global.platformConnector.enabled is false — and the workflow engine, which
the chart always wires.
Set the global value. mcp-server.platform.servers.enabled exists as a
per-service override and is null by default, meaning “follow the global
value”; setting it there turns the addresses on in the MCP server alone, and
the console and the deploy answer would then disagree with it.
Turning the switch off again makes the addresses 404 without removing what apps have published: the publications are kept and serve again when it is turned back on.
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://mcp.example.com/platform/servers/any-name
# 401 — mounted and challenged (404 means the addresses are off)
Turn it off
Set global.platformConnector.enabled: false and upgrade — every connector path is 404 again and the auth.<base-domain> route is removed; mcp.<base-domain> stays for the other toolsets. helm rollback to the previous revision does the same, routes included. Registering OAuth clients, rotating secrets and reading the connector’s logs are covered in the chart’s RUNBOOK.md § Platform connector; the agent-side setup is in the connector guide.
New chart defaults are not picked up by helm upgrade --reuse-values, so an
upgrade to chart ≥ 0.27.0 changes nothing by itself: the connector stays off
and no route renders. Pass the values above explicitly (or drop
--reuse-values). The sub-chart switches mcp-server.platform.enabled,
auth-service.platformOAuth.enabled and the two ingress.enabled keys are
null by default, meaning “follow the global switch” — once you --set one
of them it stops following until you set it back to null.
Optional components
BuildKit and the in-cluster container registry are enabled by default and can be disabled if you’d rather use external infrastructure:
global:
buildkit:
enabled: false
containerRegistry:
enabled: false
externalBuildkit:
addr: tcp://<your-buildkit-host>:1234
externalRegistry:
url: <your-registry-host>
The bundled container registry exposes itself on nodePort: 32000 by default; switch it to ClusterIP plus its own ingress if NodePort isn’t an option.
Notifications
The notification service is always installed. Since the platform’s producers
were wired up (P-0025 Part E), global.notifyService.ingestToken is the one
value that turns event flow on: feedback-service and deploy-api carry the
same token and relay ticket and deploy-failure events through it. Leave it
empty and producers emit nothing (their outboxes stay quiet and tickets and
deploys work unaffected); set it to any long random string and events flow.
The chart’s default NetworkPolicy already admits both producers.
Delivery still needs a channel. A fresh install sends nothing: no channel
is configured by default, and every would-be notification is recorded as
dropped in the delivery ledger instead of being sent. To turn on email
delivery, configure SMTP:
global:
notifyService:
smtp:
host: <your-smtp-relay> # e.g. smtp-relay.brevo.com — empty keeps email off
username: <smtp-login>
password: <smtp-key>
# from defaults to no-reply@<baseDomain>
# Outbound cap per UTC day; over-budget mail is deferred to the next
# window, never dropped. Default 300.
sendBudgetPerDay: 300
Every notification email carries a single-use unsubscribe link (and RFC 8058
one-click headers). The redemption endpoint (/api/notify/unsubscribe) and the
portal confirmation page (app.<baseDomain>/unsubscribe) are wired by default —
the gateway routes the endpoint publicly, and the URLs derive from
global.baseDomain.
The sending domain (the domain of the from address) must carry a correct SPF
include and DKIM record for your relay before you set smtp.host. Without
them your notifications land in spam while the delivery ledger reports sent
— a silent failure. Verify a real received message’s headers show SPF and DKIM
pass; a delivery acknowledgement from the relay proves nothing about
placement.