> ## Documentation Index
> Fetch the complete documentation index at: https://docs.controlplane.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Dify

> Dify open-source builder for LLM apps, agents, RAG knowledge bases and MCP servers, with bundled PostgreSQL (pgvector) and Redis. Login always required. Single by default, optional HA PostgreSQL and up to 3 replicas. Deploys into an existing GVC; four secrets must exist first.

## Overview

Dify is an open-source builder for LLM applications: chatbots, agents and visual workflows with RAG knowledge bases. You build an app in the browser, then publish it as a web page, call it through the REST service API or expose it as an MCP server. This template runs Dify on a bundled PostgreSQL with pgvector and a bundled Redis. A login is always required.

> [!NOTE]
>
> This template does not create a GVC. It deploys into an existing GVC that you already have, and that GVC should have exactly one location, because the database, Redis, both volumes and the plugin daemon run in one location.

### What Gets Created

| Resource | Name | Purpose | Optional |
| - | - | - | - |
| Workload | `RELEASE_NAME-dify` | nginx gateway on port `8080`, the only public workload (standard, 1 replica by default, up to 3) | — |
| Workload | `RELEASE_NAME-dify-web` | Console and published web apps on port `3000` | — |
| Workload | `RELEASE_NAME-dify-api` | Console API, service API and MCP server on port `5001`; applies database migrations at start | — |
| Workload | `RELEASE_NAME-dify-worker` | Celery worker for document indexing and asynchronous tasks (1 replica by default, up to 3) | — |
| Workload | `RELEASE_NAME-dify-beat` | Scheduler for periodic tasks (always 1 replica) | — |
| Workload | `RELEASE_NAME-dify-plugin-daemon` | Installs and runs model-provider and tool plugins (stateful, 1 replica) | — |
| Workload | `RELEASE_NAME-dify-sandbox` | Runs the Python and JavaScript of Code and Template nodes | Omitted when `codeExecution.endpoint` is set |
| Workload | `RELEASE_NAME-dify-ssrf-proxy` | squid proxy on port `3128` that keeps flows' HTTP requests off private and in-GVC addresses | `ssrfProxy.enabled: true` (default) |
| Volume set | `RELEASE_NAME-dify-storage` | Shared file storage mounted by the API and worker (uploads and the keys that decrypt saved provider credentials) | — |
| Volume set | `RELEASE_NAME-dify-plugins` | Installed plugins and their Python environments | — |
| Identity | `RELEASE_NAME-dify-identity` | Workload identity for the Dify tiers | — |
| Policy | `RELEASE_NAME-dify-policy` | Grants the identity `reveal` on exactly the secrets you name | — |
| Identity | `RELEASE_NAME-dify-sandbox-identity` | Workload identity for the code sandbox | — |
| Policy | `RELEASE_NAME-dify-sandbox-policy` | Grants the sandbox `reveal` on its own key only | — |
| Workload | `RELEASE_NAME-postgres` | PostgreSQL with pgvector (stateful, 1 replica) | `postgres.enabled: true` (default) |
| Volume set | `RELEASE_NAME-pg-vs` | PostgreSQL data volume | `postgres.enabled: true` (default) |
| Identity | `RELEASE_NAME-pg-identity` | Workload identity for PostgreSQL | `postgres.enabled: true` (default) |
| Policy | `RELEASE_NAME-pg-policy` | Grants the PostgreSQL identity `reveal` on the database credentials secret | `postgres.enabled: true` (default) |
| Workload | `RELEASE_NAME-postgres-backup` | Scheduled backups of PostgreSQL to object storage | `postgres.backup.enabled: true` |
| Workload | `RELEASE_NAME-postgres-ha` | Patroni PostgreSQL (stateful, 3 replicas by default) | `postgresHA.enabled: true` |
| Workload | `RELEASE_NAME-etcd` | etcd cluster for Patroni | `postgresHA.enabled: true` |
| Workload | `RELEASE_NAME-postgres-ha-proxy` | HAProxy endpoint that always points at the PostgreSQL leader | `postgresHA.enabled: true` |
| Volume set | `RELEASE_NAME-postgres-ha-vs` | PostgreSQL data volume, one per replica | `postgresHA.enabled: true` |
| Volume set | `RELEASE_NAME-etcd-vs` | etcd data volume | `postgresHA.enabled: true` |
| Identity | `RELEASE_NAME-postgres-ha-identity` | Workload identity for Patroni PostgreSQL | `postgresHA.enabled: true` |
| Policy | `RELEASE_NAME-postgres-ha-policy` | Grants the Patroni identity `reveal` on its startup secrets and the database credentials secret | `postgresHA.enabled: true` |
| Identity | `RELEASE_NAME-etcd-identity` | Workload identity for etcd | `postgresHA.enabled: true` |
| Policy | `RELEASE_NAME-etcd-policy` | Grants the etcd identity `reveal` on its startup secret | `postgresHA.enabled: true` |
| Secret | `RELEASE_NAME-patroni-startup` | Patroni startup script | `postgresHA.enabled: true` |
| Secret | `RELEASE_NAME-etcd-startup` | etcd startup script | `postgresHA.enabled: true` |
| Secret | `RELEASE_NAME-postgres-proxy-startup` | HAProxy startup script | `postgresHA.enabled: true` |
| Secret | `RELEASE_NAME-wal-g-backup-script` | Backup script used when backups are enabled | `postgresHA.enabled: true` |
| Workload | `RELEASE_NAME-postgres-ha-backup` | Scheduled backups of the Patroni PostgreSQL | `postgresHA.backup.enabled: true` |
| Secret | `RELEASE_NAME-postgres-config` | Backup configuration | `postgresHA.backup.enabled: true` |
| Workload | `RELEASE_NAME-redis` | Cache, task queue and plugin state (single node, password required) | `redis.enabled: true` (default) |
| Identity | `RELEASE_NAME-redis-identity` | Workload identity for Redis | `redis.enabled: true` (default) |
| Policy | `RELEASE_NAME-redis-policy` | Grants the Redis identity access to its configuration secret | `redis.enabled: true` (default) |
| Secret | `RELEASE_NAME-redis-config` | Redis configuration | `redis.enabled: true` (default) |

The template creates no credentials. The public `*.cpln.app` URL is on by default and every page needs a Dify login. Each Dify tier accepts connections only from the tiers that call it.

## Prerequisites

**1. Create the Dify keys secret**

A **dictionary** secret holding Dify's own keys. Use only letters and digits in every value, and keep `initPassword` to at most 30 characters:

```bash theme={null}
cpln secret create-dictionary --name DIFY_SECRET_NAME \
  --entry secretKey="$(openssl rand -hex 32)" \
  --entry initPassword="$(openssl rand -hex 12)" \
  --entry pluginDaemonKey="$(openssl rand -hex 32)" \
  --entry pluginInnerApiKey="$(openssl rand -hex 32)"
```

Set `dify.secretName` to this name.

**2. Create the code sandbox key secret**

An **opaque** secret holding the key that Dify and the code sandbox share:

```bash theme={null}
printf '%s' "$(openssl rand -hex 32)" | \
  cpln secret create-opaque --name SANDBOX_KEY_SECRET_NAME --encoding plain -f -
```

Set `codeExecution.apiKeySecretName` to this name.

**3. Create the database credentials secret**

A **dictionary** secret with exactly `username`, `password` and `database`, letters and digits only:

```bash theme={null}
cpln secret create-dictionary --name DB_CREDENTIALS_SECRET_NAME \
  --entry username=dify \
  --entry password="$(openssl rand -hex 32)" \
  --entry database=dify
```

Set `postgres.config.credentialsSecretName` to this name (or `postgresHA.config.credentialsSecretName` with the HA database).

**4. Create the Redis password secret**

A **dictionary** secret with a `password` key, letters and digits only. A password is required because user code runs inside this GVC:

```bash theme={null}
cpln secret create-dictionary --name REDIS_SECRET_NAME \
  --entry password="$(openssl rand -hex 32)"
```

Set `redis.redis.auth.fromSecret.name` to this name.

Secret names are org-wide, so give each release its own secrets. A value with other characters stops the API, worker and scheduler at start with a log line naming the key. Backups and a custom domain are optional, see [Backing Up](#backing-up) and [Connecting](#connecting).

> [!WARNING]
>
> Every secret your release names must exist **before** you install. If one does not, the deployment waits silently: `cpln logs` returns nothing, and the only place the missing secret is named is `status.versions[].message` in `cpln workload get-deployments RELEASE_NAME-dify-api --gvc GVC_NAME -o yaml`. Create the secret and the deployment recovers on its own after several minutes, or run `cpln workload force-redeployment WORKLOAD_NAME --gvc GVC_NAME` on each Dify workload to skip the wait.

## Installation

The required values are the names of the four secrets from [Prerequisites](#prerequisites):

```bash theme={null}
cpln helm install RELEASE_NAME oci://ghcr.io/controlplane-com/templates/dify \
  --version 1.0.0 --gvc GVC_NAME \
  --set dify.secretName=DIFY_SECRET_NAME \
  --set codeExecution.apiKeySecretName=SANDBOX_KEY_SECRET_NAME \
  --set postgres.config.credentialsSecretName=DB_CREDENTIALS_SECRET_NAME \
  --set redis.redis.auth.fromSecret.name=REDIS_SECRET_NAME
```

To install with the highly available database, add `--set postgres.enabled=false --set postgresHA.enabled=true --set postgresHA.config.credentialsSecretName=DB_CREDENTIALS_SECRET_NAME`.

To install with the CLI, UI, Terraform or Pulumi, follow the instructions for your preferred method:

> [card] [UI](https://docs.controlplane.com/template-catalog/install-manage/ui.md)
>
> Browse, install, and manage templates visually

> [card] [CLI](https://docs.controlplane.com/template-catalog/install-manage/cli.md)
>
> Manage templates from your terminal

> [card] [Terraform](https://docs.controlplane.com/template-catalog/install-manage/terraform.md)
>
> Declare templates in your Terraform configurations

> [card] [Pulumi](https://docs.controlplane.com/template-catalog/install-manage/pulumi.md)
>
> Declare templates in your Pulumi programs

## Configuration

### Dify

```yaml theme={null}
dify:
  # Official images; api, worker and beat share apiImage. Keep api and web on the same tag.
  apiImage: langgenius/dify-api:1.19.0
  webImage: langgenius/dify-web:1.19.0
  gatewayImage: nginx:1.30.6-alpine
  # Replicas of the gateway, web, API, code sandbox and SSRF proxy (1-3).
  # set dify.replicas: 2, worker.replicas: 2 and postgresHA.enabled: true for near-zero-downtime upgrades and failover
  replicas: 1
  secretName: my-dify-secrets
  publicUrl: ""                 # "" = the gateway's *.cpln.app endpoint; or https://dify.example.com (no trailing /) for a custom domain
  requestTimeoutSeconds: 600    # longest request or stream on the public URL before it is cut (1-3600)
  volumeset:
    capacity: 10                # GiB (minimum 10): uploads and the keys that decrypt stored provider credentials; never delete it
  # Plain (non-secret) Dify settings for api, worker and beat, e.g. UPLOAD_FILE_SIZE_LIMIT, LOG_LEVEL.
  extraEnv: []
  # extraEnv:
  #   - name: UPLOAD_FILE_SIZE_LIMIT
  #     value: "50"
  resources:
    api:
      minCpu: 250m
      maxCpu: 1000m
      minMemory: 512Mi
      maxMemory: 1536Mi
```

The gateway, web and beat tiers take the same `resources` shape under `dify.resources`.

```yaml theme={null}
worker:
  replicas: 1                   # 1-3
  resources:
    minCpu: 250m
    maxCpu: 1000m
    minMemory: 512Mi
    maxMemory: 1536Mi
pluginDaemon:
  image: langgenius/dify-plugin-daemon:0.6.14-local
  resources:
    minCpu: 250m
    maxCpu: 1000m
    minMemory: 512Mi
    maxMemory: 2Gi
  volumeset:
    capacity: 20                # GiB (minimum 10): installed plugins and their Python environments
```

Each API and worker process holds up to 8 database connections (`SQLALCHEMY_POOL_SIZE` 5 plus `SQLALCHEMY_MAX_OVERFLOW` 3), which allows about 8 concurrent runs per API replica. To allow more, set both in `dify.extraEnv`, staying within PostgreSQL's 100 connections:

```yaml theme={null}
dify:
  extraEnv:
    - name: SQLALCHEMY_POOL_SIZE
      value: "20"
    - name: SQLALCHEMY_MAX_OVERFLOW
      value: "10"
```

### Code Execution and Network

```yaml theme={null}
codeExecution:
  endpoint: ""                  # "" = the bundled dify-sandbox workload; or the URL of your own sandbox-compatible executor
  apiKeySecretName: my-dify-sandbox-key
  sandbox:
    image: langgenius/dify-sandbox:0.2.15
    resources:
      minCpu: 100m
      maxCpu: 500m
      minMemory: 128Mi
      maxMemory: 512Mi
ssrfProxy:
  enabled: true                 # false lets HTTP Request nodes, tools and code call any service in this GVC
  image: ubuntu/squid:6.6-24.04_edge
publicAccess:
  enabled: true                 # public *.cpln.app URL; /install is guarded by initPassword, every page needs a login
internalAccess:                 # who in Control Plane may reach the gateway (the other tiers admit only each other)
  type: same-gvc                # none | same-gvc | same-org | workload-list
  workloads: []                 # used only with workload-list
  # workloads:
  #   - //gvc/GVC_NAME/workload/WORKLOAD_NAME
```

### Database

```yaml theme={null}
postgres:                       # every knob of the postgres template
  enabled: true
  image: pgvector/pgvector:0.8.7-pg18
  config:
    credentialsSecretName: my-dify-db-credentials
  resources:
    minCpu: 250m
    maxCpu: 1000m
    minMemory: 512Mi
    maxMemory: 1Gi
  volumeset:
    capacity: 10                # GiB (minimum 10)
  backup:
    enabled: false              # scheduled backups to object storage; provider aws, gcp or minio (postgres template)
postgresHA:                     # every knob of the postgres-highly-available template
  enabled: false                # enable exactly one of postgres / postgresHA
  image: controlplanecorporation/patroni-postgres:0.8   # includes pgvector 0.8.7
  config:
    credentialsSecretName: my-dify-db-credentials
  replicas: 3
  resources:                    # per Patroni replica
    minCpu: 250m
    maxCpu: 1000m
    minMemory: 512Mi
    maxMemory: 2Gi
  volumeset:
    capacity: 10                # GiB per replica (minimum 10)
  backup:
    enabled: false              # logical or wal-g backups (postgres-highly-available template)
```

### Redis

```yaml theme={null}
redis:
  enabled: true                 # required
  mode: dev                     # required: one node; the chart refuses production (Sentinel) mode
  redis:
    image: redis:8
    resources:
      maxCpu: 200m
      maxMemory: 256Mi
      minCpu: 80m
      minMemory: 128Mi
    auth:
      fromSecret:
        enabled: true
        name: my-dify-redis
        passwordKey: password
```

## Connecting

| What | Address | Notes |
| - | - | - |
| Console (public) | `status.canonicalEndpoint` of `cpln workload get RELEASE_NAME-dify --gvc GVC_NAME -o yaml` | See First Run |
| From your machine (private install) | `cpln port-forward RELEASE_NAME-dify 8080:8080 --gvc GVC_NAME`, then `http://localhost:8080` | Requires `dify.publicUrl: http://localhost:8080` |
| Service API | `POST PUBLIC_URL/v1/chat-messages` or `/v1/workflows/run` | Header `Authorization: Bearer APP_API_KEY`; create the key on the app's API Access page |
| Internal service API | `http://RELEASE_NAME-dify.GVC_NAME.cpln.local:8080/v1` | Reachable per `internalAccess` |
| App as MCP server | `PUBLIC_URL/mcp/server/.../mcp` | Turn on MCP Server for an app; the app shows the full URL |
| Webhook trigger | `PUBLIC_URL/triggers/webhook/...` | A published workflow with a webhook trigger node shows its URL |
| Control Plane MCP server | Tools → MCP → add `https://mcp.cpln.io/mcp?toolsets=readonly` | Sign in with your Control Plane account through OAuth; the read-only toolset has no create, update or delete tools |
| Models | Settings → Model Provider | Install a model-provider plugin, then enter its API key there |

Dify builds every link, cookie and webhook from the public URL, and the login works only on that exact address. With `dify.publicUrl` empty it is the gateway's `*.cpln.app` endpoint. Behind a custom domain, route the domain to the `RELEASE_NAME-dify` workload and set `dify.publicUrl` to it. With `publicAccess.enabled: false`, set `dify.publicUrl` to the address users open, such as `http://localhost:8080` for port-forward.

To check the gateway, forward the port and read its health:

```bash theme={null}
cpln port-forward RELEASE_NAME-dify 8080:8080 --gvc GVC_NAME
curl http://localhost:8080/gateway-health
```

Streaming responses and long runs pass through the gateway unbuffered. The public endpoint cuts a request after `dify.requestTimeoutSeconds`; calls from inside the GVC over `cpln.local` are not subject to it.

### First Run

Open the public URL. Dify asks for the `initPassword` from the Dify keys secret, then lets you create the admin account on the `/install` page. Read the password with `cpln secret reveal DIFY_SECRET_NAME -o yaml`. Self sign-up and creating more workspaces are turned off; invite members from Settings → Members, which shows an invite link to copy because the template sends no email.

## Operations

### Backing Up

All state lives in PostgreSQL and the file storage volume. Enable `postgres.backup.enabled` (or `postgresHA.backup.enabled` with the HA database) and fill in the provider block; the keys and defaults are in the chart's `values.yaml`. The bucket, cloud account and IAM policy are set up as described in the [postgres](https://docs.controlplane.com/template-catalog/templates/postgres.md) or [postgres-highly-available](https://docs.controlplane.com/template-catalog/templates/postgres-highly-available.md) template, and a MinIO provider needs a dictionary secret with the keys `accessKey` and `secretKey`. Restore steps are in those templates' pages. The backup covers the database only; uploaded files and the keys that decrypt saved provider credentials on the file storage volume are not included.

### Scaling and Availability

Set `dify.replicas` from 1 to 3 for the gateway, web, API, code sandbox and SSRF proxy, and `worker.replicas` from 1 to 3 for the worker. The scheduler and the plugin daemon always run one replica. When several API replicas start together, one applies the database migrations and the others wait for it.

For near-zero-downtime upgrades and failover, set `dify.replicas: 2`, `worker.replicas: 2` and `postgresHA.enabled: true`. Redis stays a single node.

After changing a secret a workload reads, redeploy the workloads that use it:

```bash theme={null}
cpln workload force-redeployment WORKLOAD_NAME --gvc GVC_NAME
```

### Clearing a Login Lockout

Dify locks an email address out for 24 hours after 6 failed logins, from any address. To clear it, delete the lock from Redis:

```bash theme={null}
cpln workload exec RELEASE_NAME-redis --gvc GVC_NAME --container redis -- sh -c 'REDISCLI_AUTH="$CUSTOM_REDIS_PASSWORD" redis-cli DEL login_error_rate_limit:USER_EMAIL'
```

### Troubleshooting

> [accordion] The deployment is stuck and cpln logs returns nothing
>
> A secret named in the values does not exist. Read `status.versions[].message` with `cpln workload get-deployments RELEASE_NAME-dify-api --gvc GVC_NAME -o yaml`, create the secret, then run `cpln workload force-redeployment WORKLOAD_NAME --gvc GVC_NAME` on each Dify workload.

> [accordion] The API, worker or scheduler stops at start with a log line naming a key
>
> A database or Redis value contains characters other than letters and digits, `secretKey` is empty, or `initPassword` is longer than 30 characters. Fix the secret and redeploy the Dify workloads.

> [accordion] Login fails or the console loops back to the sign-in page
>
> The login works only on the exact address in `dify.publicUrl`. For a private install or a custom domain, set `dify.publicUrl` to the address users open, without a trailing slash.

> [accordion] An HTTP Request node cannot reach a service in my GVC
>
> The SSRF proxy refuses private, link-local and in-GVC addresses (`*.cpln.local`) for HTTP Request nodes, tools and Python HTTP calls. Set `ssrfProxy.enabled: false` to lift it, knowing that flows can then call any service in the GVC. Model-provider plugins run in the plugin daemon, not through the proxy, so Ollama, vLLM and LiteLLM providers can use in-GVC URLs.

> [accordion] Runs fail with QueuePool limit of size 5 overflow 3 reached
>
> Concurrent runs exhausted the database connection pool. Raise `SQLALCHEMY_POOL_SIZE` and `SQLALCHEMY_MAX_OVERFLOW` in `dify.extraEnv`, as shown in Configuration.

> [accordion] Long runs are cut off on the public URL
>
> The public endpoint cuts requests after `dify.requestTimeoutSeconds` (default 600, maximum 3600). Raise it for longer calls.

> [accordion] An email is locked out after failed logins
>
> Clear the lock as shown in Clearing a Login Lockout.

> [accordion] The chart refuses to render
>
> The error names the offending value. The template rejects replica counts outside 1-3, enabling both or neither database, `redis.mode` other than `dev`, and `extraEnv` names that the chart sets itself.

## Important Notes

* The Dify licence allows one workspace per install, not a multi-tenant service, and forbids removing or changing the Dify logo and copyright in the console.
* Never rotate `secretKey` (it signs every login and shared link) and never delete the file storage volume (it holds the keys that decrypt the model-provider credentials saved in Dify).
* Anyone who can edit an app can run Python and JavaScript in the code sandbox, and that code can open direct connections to the internet. The Dify API, web and plugin daemon refuse the sandbox, but the other services in this GVC, including the bundled PostgreSQL and Redis, are guarded only by their passwords. Give accounts only to people you trust.
* With the SSRF proxy on, HTTP Request nodes, tools and Python `requests` calls in Code nodes go through it. JavaScript code gets no proxy.
* Plugins install from the Dify marketplace or an uploaded package, and only signed plugins are accepted.
* Use a single-location GVC: the database, Redis, both volumes and the plugin daemon run in one location.
* The first `helm upgrade` after an install can restart the bundled database, which briefly interrupts Dify.
* Uninstalling deletes the database and both volumes, and with them every app, user, knowledge base and installed plugin.

## External References

> [card] [Dify documentation](https://docs.dify.ai)
>
> Concepts, apps and deployment

> [card] [Environment variables](https://docs.dify.ai/en/self-host/deploy/configuration/environments)
>
> Dify settings

> [card] [Dify service API](https://docs.dify.ai/en/guides/application-publishing/developing-with-apis)
>
> Calling published apps over REST

> [card] [Dify as an MCP server](https://docs.dify.ai/en/cloud/use-dify/publish/publish-mcp)
>
> Exposing apps as MCP tools

> [card] [Dify on GitHub](https://github.com/langgenius/dify)
>
> Source, licence and issues
