Dibbla Docs Get started Guides Workflows Changelog
Open Console
Self-host

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.

Prefer the guided walkthrough?

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.

Choose your auth path first

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.
  • helm 3.8+ and kubectl configured 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:packages to 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-toolserver for the workflow tools.
    • PostgreSQL passwords — dibbla user + postgres superuser. 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.

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:

ValueBehaviorRequired 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-ingressCreates an Ingress per app and a Cloudflare CNAME pointing at the ingress entrypoint.cloudflare.apiToken, cloudflare.zoneId, ingressHostTarget
cloudflare-tunnelAdds a tunnel route per app on the existing Cloudflare Tunnel. No Kubernetes Ingress per app.full cloudflare.* block
cloudflare-tunnel-ingressTunnel + 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.

Bundled Postgres + Redis are single-pod

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> and auth.<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 are https only; the services refuse an http issuer 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.

Half-configured does not render

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:

ValueDefault
Route mcp.<base-domain> → MCP servermcp-server.ingress.* (inherits global.ingress)on when global.ingress.enabled
Route auth.<base-domain> → Auth Service APIauth-service.ingress.* (inherits global.ingress)on when global.ingress.enabled and the connector is on
Per-IP rate limit at the edgemcp-server.ingress.rateLimit, auth-service.ingress.rateLimiton — a Traefik Middleware, or nginx limit-rps annotations
Writes through /platformmcp-server.writes.enabledoff — the toolset is read-only until you turn this on
NetworkPolicy peer for the ingress controllerauth-service.networkPolicy.additionalIngressAllowLabelsapp.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.

Upgrading with --reuse-values

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.

Set up SPF and DKIM before enabling SMTP

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.