diff --git a/blueprints/dify/docker-compose.yml b/blueprints/dify/docker-compose.yml new file mode 100644 index 00000000..6c89415b --- /dev/null +++ b/blueprints/dify/docker-compose.yml @@ -0,0 +1,256 @@ +# Dify - open-source LLM app development platform +# Adapted from the official docker/docker-compose.yaml (tag 1.15.0). +# Minimal functional stack: api + worker + worker_beat + web + postgres +# (pgvector, reused as the vector store) + redis + sandbox + ssrf_proxy +# + plugin_daemon, fronted by a lightweight internal nginx gateway that +# mirrors the upstream path routing (TLS terminates at Traefik). + +x-shared-api-env: &shared-api-env + LOG_LEVEL: INFO + DEPLOY_ENV: PRODUCTION + SECRET_KEY: ${SECRET_KEY} + MIGRATION_ENABLED: "true" + # Public URLs - a single domain, path-routed like the upstream nginx + CONSOLE_API_URL: https://${DIFY_DOMAIN} + CONSOLE_WEB_URL: https://${DIFY_DOMAIN} + SERVICE_API_URL: https://${DIFY_DOMAIN} + APP_API_URL: https://${DIFY_DOMAIN} + APP_WEB_URL: https://${DIFY_DOMAIN} + FILES_URL: https://${DIFY_DOMAIN} + INTERNAL_FILES_URL: http://api:5001 + TRIGGER_URL: https://${DIFY_DOMAIN} + ENDPOINT_URL_TEMPLATE: https://${DIFY_DOMAIN}/e/{hook_id} + # The dedicated api_websocket collaboration service is not deployed + ENABLE_COLLABORATION_MODE: "false" + WEB_API_CORS_ALLOW_ORIGINS: "*" + CONSOLE_CORS_ALLOW_ORIGINS: "*" + # PostgreSQL + DB_USERNAME: postgres + DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_HOST: db + DB_PORT: "5432" + DB_DATABASE: dify + # Redis / Celery + REDIS_HOST: redis + REDIS_PORT: "6379" + REDIS_PASSWORD: ${REDIS_PASSWORD} + REDIS_DB: "0" + CELERY_BROKER_URL: redis://:${REDIS_PASSWORD}@redis:6379/1 + # File storage on a local volume + STORAGE_TYPE: opendal + OPENDAL_SCHEME: fs + OPENDAL_FS_ROOT: storage + # Vector store: pgvector, reusing the same PostgreSQL instance + VECTOR_STORE: pgvector + PGVECTOR_HOST: db + PGVECTOR_PORT: "5432" + PGVECTOR_USER: postgres + PGVECTOR_PASSWORD: ${POSTGRES_PASSWORD} + PGVECTOR_DATABASE: dify + PGVECTOR_MIN_CONNECTION: "1" + PGVECTOR_MAX_CONNECTION: "5" + # Code execution sandbox + CODE_EXECUTION_ENDPOINT: http://sandbox:8194 + CODE_EXECUTION_API_KEY: ${SANDBOX_API_KEY} + # SSRF proxy (squid) for user-triggered outbound requests + SSRF_PROXY_HTTP_URL: http://ssrf_proxy:3128 + SSRF_PROXY_HTTPS_URL: http://ssrf_proxy:3128 + # Plugin daemon (model providers / tools are plugins since Dify 1.0) + PLUGIN_DAEMON_URL: http://plugin_daemon:5002 + PLUGIN_DAEMON_KEY: ${PLUGIN_DAEMON_KEY} + INNER_API_KEY_FOR_PLUGIN: ${PLUGIN_INNER_API_KEY} + PLUGIN_MAX_PACKAGE_SIZE: "52428800" + PLUGIN_REMOTE_INSTALL_HOST: localhost + PLUGIN_REMOTE_INSTALL_PORT: "5003" + MARKETPLACE_ENABLED: "true" + MARKETPLACE_API_URL: https://marketplace.dify.ai + +services: + nginx: + image: nginx:1.27-alpine + restart: always + volumes: + - ../files/dify-nginx.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + api: + condition: service_healthy + web: + condition: service_started + plugin_daemon: + condition: service_started + + # The dify-api image runs as the non-root user 1001, so the shared + # storage volume has to be handed over to it once (same trick as the + # upstream init_permissions container). + init_permissions: + image: busybox:1.36 + command: + - sh + - -c + - | + FLAG_FILE="/app/api/storage/.init_permissions" + if [ -f "$$FLAG_FILE" ]; then + echo "Permissions already initialized." + exit 0 + fi + echo "Initializing permissions for /app/api/storage" + chown -R 1001:1001 /app/api/storage && touch "$$FLAG_FILE" + volumes: + - dify-app-storage:/app/api/storage + restart: "no" + + api: + image: langgenius/dify-api:1.15.0 + restart: always + environment: + <<: *shared-api-env + MODE: api + depends_on: + init_permissions: + condition: service_completed_successfully + db: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - dify-app-storage:/app/api/storage + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5001/health"] + interval: 15s + timeout: 5s + retries: 20 + start_period: 120s + + worker: + image: langgenius/dify-api:1.15.0 + restart: always + environment: + <<: *shared-api-env + MODE: worker + depends_on: + init_permissions: + condition: service_completed_successfully + db: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - dify-app-storage:/app/api/storage + + worker_beat: + image: langgenius/dify-api:1.15.0 + restart: always + environment: + <<: *shared-api-env + MODE: beat + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + + web: + image: langgenius/dify-web:1.15.0 + restart: always + environment: + CONSOLE_API_URL: https://${DIFY_DOMAIN} + APP_API_URL: https://${DIFY_DOMAIN} + SERVER_CONSOLE_API_URL: http://api:5001 + ENABLE_COLLABORATION_MODE: "false" + MARKETPLACE_API_URL: https://marketplace.dify.ai + MARKETPLACE_URL: https://marketplace.dify.ai + NEXT_TELEMETRY_DISABLED: "1" + + db: + image: pgvector/pgvector:pg16 + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: dify + PGDATA: /var/lib/postgresql/data/pgdata + volumes: + - dify-db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d dify"] + interval: 5s + timeout: 3s + retries: 30 + + redis: + image: redis:6-alpine + restart: always + environment: + REDISCLI_AUTH: ${REDIS_PASSWORD} + command: redis-server --requirepass ${REDIS_PASSWORD} + volumes: + - dify-redis-data:/data + healthcheck: + test: ["CMD-SHELL", "redis-cli ping | grep -q PONG"] + interval: 5s + timeout: 3s + retries: 30 + + sandbox: + image: langgenius/dify-sandbox:0.2.15 + restart: always + environment: + API_KEY: ${SANDBOX_API_KEY} + GIN_MODE: release + WORKER_TIMEOUT: "15" + ENABLE_NETWORK: "true" + HTTP_PROXY: http://ssrf_proxy:3128 + HTTPS_PROXY: http://ssrf_proxy:3128 + SANDBOX_PORT: "8194" + volumes: + - dify-sandbox-deps:/dependencies + + ssrf_proxy: + image: ubuntu/squid:latest + restart: always + volumes: + - ../files/dify-squid.conf:/etc/squid/squid.conf:ro + + plugin_daemon: + image: langgenius/dify-plugin-daemon:0.6.3-local + restart: always + environment: + DB_HOST: db + DB_PORT: "5432" + DB_USERNAME: postgres + DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_DATABASE: dify_plugin + DB_SSL_MODE: disable + REDIS_HOST: redis + REDIS_PORT: "6379" + REDIS_PASSWORD: ${REDIS_PASSWORD} + SERVER_PORT: "5002" + SERVER_KEY: ${PLUGIN_DAEMON_KEY} + MAX_PLUGIN_PACKAGE_SIZE: "52428800" + DIFY_INNER_API_URL: http://api:5001 + DIFY_INNER_API_KEY: ${PLUGIN_INNER_API_KEY} + PLUGIN_REMOTE_INSTALLING_HOST: 0.0.0.0 + PLUGIN_REMOTE_INSTALLING_PORT: "5003" + PLUGIN_WORKING_PATH: /app/storage/cwd + FORCE_VERIFYING_SIGNATURE: "true" + PYTHON_ENV_INIT_TIMEOUT: "120" + PLUGIN_MAX_EXECUTION_TIMEOUT: "600" + PLUGIN_STORAGE_TYPE: local + PLUGIN_STORAGE_LOCAL_ROOT: /app/storage + PLUGIN_INSTALLED_PATH: plugin + PLUGIN_PACKAGE_CACHE_PATH: plugin_packages + PLUGIN_MEDIA_CACHE_PATH: assets + volumes: + - dify-plugin-storage:/app/storage + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + +volumes: + dify-app-storage: + dify-db-data: + dify-redis-data: + dify-sandbox-deps: + dify-plugin-storage: diff --git a/blueprints/dify/instructions.md b/blueprints/dify/instructions.md new file mode 100644 index 00000000..f554f671 --- /dev/null +++ b/blueprints/dify/instructions.md @@ -0,0 +1,15 @@ +## Instructions + +### First setup + +- The first boot runs the database migrations, so it can take **2-4 minutes** before the application responds. +- Open your domain: you will be redirected to `/install`, where you create the admin (workspace owner) account. +- After signing in, go to **Settings -> Model Provider** and install a model provider plugin (OpenAI, Anthropic, Ollama, etc.) from the marketplace, then configure your API keys. + +### Notes + +- **HTTPS is required for the console login**: Dify issues `__Host-`/`Secure` session cookies, so make sure the domain has a certificate (e.g. Let's Encrypt in the domain settings). Over plain HTTP the browser will drop the session cookies and login will not persist. + +- The vector store is **pgvector**, running inside the bundled PostgreSQL instance (no separate Weaviate/Qdrant container needed). +- All routing (`/console/api`, `/api`, `/v1`, `/files`, `/e/`, ...) is handled by the bundled internal nginx gateway, mirroring the upstream deployment, so the Dify service API is available at `https:///v1`. +- Code execution runs in the isolated `sandbox` service, and user-triggered outbound HTTP requests go through the `ssrf_proxy` (squid) service, like in the official deployment. diff --git a/blueprints/dify/logo.svg b/blueprints/dify/logo.svg new file mode 100644 index 00000000..52ed710f --- /dev/null +++ b/blueprints/dify/logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/blueprints/dify/meta.json b/blueprints/dify/meta.json new file mode 100644 index 00000000..170013dd --- /dev/null +++ b/blueprints/dify/meta.json @@ -0,0 +1,17 @@ +{ + "id": "dify", + "name": "Dify", + "version": "1.15.0", + "description": "Dify is an open-source LLM app development platform. Build AI workflows, RAG pipelines, agents and chatbots with a visual interface and publish them as APIs or web apps.", + "logo": "logo.svg", + "links": { + "github": "https://github.com/langgenius/dify", + "website": "https://dify.ai/", + "docs": "https://docs.dify.ai/" + }, + "tags": [ + "ai", + "llm", + "rag" + ] +} diff --git a/blueprints/dify/template.toml b/blueprints/dify/template.toml new file mode 100644 index 00000000..ded696fb --- /dev/null +++ b/blueprints/dify/template.toml @@ -0,0 +1,168 @@ +[variables] +main_domain = "${domain}" +secret_key = "${base64:42}" +postgres_password = "${password:32}" +redis_password = "${password:32}" +sandbox_api_key = "${password:32}" +plugin_daemon_key = "${base64:42}" +plugin_inner_api_key = "${base64:42}" + +[config] +env = [ + "DIFY_DOMAIN=${main_domain}", + "SECRET_KEY=${secret_key}", + "POSTGRES_PASSWORD=${postgres_password}", + "REDIS_PASSWORD=${redis_password}", + "SANDBOX_API_KEY=${sandbox_api_key}", + "PLUGIN_DAEMON_KEY=${plugin_daemon_key}", + "PLUGIN_INNER_API_KEY=${plugin_inner_api_key}", +] + +[[config.domains]] +serviceName = "nginx" +port = 80 +host = "${main_domain}" + +# Internal nginx gateway replicating the upstream Dify nginx routing +# (TLS terminates at Traefik, which forwards everything to this nginx). +[[config.mounts]] +filePath = "dify-nginx.conf" +content = ''' +map $http_x_forwarded_proto $dify_forwarded_proto { + default $http_x_forwarded_proto; + "" $scheme; +} + +server { + listen 80; + server_name _; + + client_max_body_size 100M; + + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $dify_forwarded_proto; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_buffering off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + + location /console/api { proxy_pass http://api:5001; } + location /api { proxy_pass http://api:5001; } + location /v1 { proxy_pass http://api:5001; } + location /openapi { proxy_pass http://api:5001; } + location /files { proxy_pass http://api:5001; } + location /mcp { proxy_pass http://api:5001; } + location /triggers { proxy_pass http://api:5001; } + location /explore { proxy_pass http://web:3000; } + + location /e/ { + proxy_pass http://plugin_daemon:5002; + # nginx drops inherited proxy_set_header directives as soon as a + # location defines its own, so the shared ones are repeated here. + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $dify_forwarded_proto; + proxy_set_header Connection ""; + proxy_set_header Dify-Hook-Url $dify_forwarded_proto://$host$request_uri; + } + + location / { proxy_pass http://web:3000; } +} +''' + +# Squid SSRF proxy configuration, rendered from the upstream +# docker/ssrf_proxy/squid.conf.template with its default values. +[[config.mounts]] +filePath = "dify-squid.conf" +content = ''' +acl client_localnet src 0.0.0.1-0.255.255.255 +acl client_localnet src 10.0.0.0/8 +acl client_localnet src 100.64.0.0/10 +acl client_localnet src 169.254.0.0/16 +acl client_localnet src 172.16.0.0/12 +acl client_localnet src 192.168.0.0/16 +acl client_localnet src fc00::/7 +acl client_localnet src fe80::/10 +acl to_private_networks dst 0.0.0.0/8 +acl to_private_networks dst 10.0.0.0/8 +acl to_private_networks dst 100.64.0.0/10 +acl to_private_networks dst 127.0.0.0/8 +acl to_private_networks dst 169.254.0.0/16 +acl to_private_networks dst 172.16.0.0/12 +acl to_private_networks dst 192.168.0.0/16 +acl to_private_networks dst 224.0.0.0/4 +acl to_private_networks dst 240.0.0.0/4 +acl to_private_networks dst ::/128 +acl to_private_networks dst ::1/128 +acl to_private_networks dst ::ffff:0:0/96 +acl to_private_networks dst ::/96 +acl to_private_networks dst fc00::/7 +acl to_private_networks dst fe80::/10 +acl SSL_ports port 443 +acl Safe_ports port 80 +acl Safe_ports port 21 +acl Safe_ports port 443 +acl Safe_ports port 70 +acl Safe_ports port 210 +acl Safe_ports port 1025-65535 +acl Safe_ports port 280 +acl Safe_ports port 488 +acl Safe_ports port 591 +acl Safe_ports port 777 +acl CONNECT method CONNECT +acl allowed_domains dstdomain .marketplace.dify.ai + +http_port 3128 + +http_access deny !Safe_ports +http_access deny CONNECT !SSL_ports +http_access allow localhost manager +http_access deny manager +http_access deny to_private_networks +http_access allow allowed_domains +http_access allow client_localnet +http_access allow localhost +http_access deny all +tcp_outgoing_address 0.0.0.0 + +coredump_dir /var/spool/squid +refresh_pattern ^ftp: 1440 20% 10080 +refresh_pattern ^gopher: 1440 0% 1440 +refresh_pattern -i (/cgi-bin/|\?) 0 0% 0 +refresh_pattern . 0 20% 4320 + +client_request_buffer_max_size 100 MB +max_filedescriptors 65536 + +connect_timeout 30 seconds +request_timeout 2 minutes +read_timeout 2 minutes +client_lifetime 5 minutes +shutdown_lifetime 30 seconds + +server_persistent_connections on +client_persistent_connections on +persistent_request_timeout 30 seconds +pconn_timeout 1 minute + +client_db on +server_idle_pconn_timeout 2 minutes +client_idle_pconn_timeout 2 minutes + +quick_abort_min 16 KB +quick_abort_max 16 MB +quick_abort_pct 95 + +memory_cache_mode disk +cache_mem 256 MB +maximum_object_size_in_memory 512 KB + +dns_timeout 30 seconds +dns_retransmit_interval 5 seconds + +logformat dify_log %ts.%03tu %6tr %>a %Ss/%03>Hs %