Dify部署全指南:Docker、Kubernetes与源码部署实战
1. 为什么Dify部署值得花一整天认真对待——不是装上就行而是要稳、要快、要可维护Dify 是我过去两年在客户现场落地最多的大模型应用平台之一。它不像单纯跑个 LLM 推理服务那样“启动即用”而是一个典型的生产级 AI 应用中台前端有 React 管理界面后端有 FastAPI 提供 API数据库要存知识库切片、会话历史、工作流定义向量库要支撑 RAG 检索还要对接外部 LLMOpenAI / Ollama / DeepSeek / Qwen 等甚至可能集成企业微信、飞书、钉钉等通知通道。这意味着——部署不是“复制粘贴 docker run 命令”而是一次小型基础设施架构决策。我见过太多团队踩的坑有人用 Docker Compose 在一台 8G 内存的笔记本上硬跑结果知识库上传卡死、工作流执行超时有人照着官网 Kubernetes 文档直接 apply却忘了配置 PersistentVolume重启后所有知识库全丢还有人 clone 源码后 pip install -e .发现本地 Python 环境里 PyTorch 和 Transformers 版本冲突pip 重装十几次仍报错 ModuleNotFoundError: No module named torch._C。这些都不是“不会”而是对 Dify 的分层依赖结构缺乏系统性认知。Dify 的核心价值在于“低代码构建 AI 应用”但它的部署恰恰是高决策密度环节你选 Docker就默认接受单机轻量、快速验证但要自己扛日志轮转、健康检查、配置热更新你选 Kubernetes就获得弹性伸缩、滚动升级、多租户隔离能力但必须理解 StatefulSet 与 Headless Service 的配合逻辑、Ingress 路由规则如何匹配 /api/v1/ 和 /chat/ 前缀你选源码部署则完全掌控每个模块的启动顺序、环境变量注入方式、甚至能 patch 官方未合并的 PR代价是每次升级都要手动 merge conflict。所以这篇指南不叫“Dify 安装教程”而叫“Dify 部署全指南”——它覆盖的不是“能不能跑起来”而是“跑得稳不稳、扩得快不快、修得快不快、升得顺不顺”。我会用真实客户环境中的配置片段、kubectl describe pod 输出日志、docker logs -f 的实时反馈截图文字还原、以及 git diff 的关键行告诉你每一步背后的真实意图。如果你正准备在测试环境搭一个 demo 给老板看或要在生产环境承载 50 员工的智能客服知识库又或者想基于 Dify 改造成内部 AI 工具平台——这篇文章里的每一个参数、每一处注释、每一次“我试过不行”的记录都是从 17 个不同行业客户的部署现场抠出来的。关键词不是堆砌而是锚点Dify是你要交付的平台实体Docker是最小可行单元Kubernetes是规模化底座源码是终极控制权。三者不是并列选项而是同一问题在不同成熟度阶段的解法光谱。接下来我们按这个光谱展开——从最轻量的 Docker 开始到最复杂的源码定制全程不跳步、不省略、不假设你知道 kubectl get nodes 的输出含义。2. Docker 部署单机验证的黄金标准但 90% 的失败源于环境预检缺失2.1 为什么 Docker 是首选起点——它把“环境一致性”从玄学变成可验证事实Docker 的本质不是容器技术而是环境契约封装。Dify 官方镜像difyai/dify:latest已将 Python 3.11、PostgreSQL 15、Redis 7、Elasticsearch 8、Weaviate 1.24 等全部依赖打包进一个 OCI 镜像。这意味着你在 Ubuntu 22.04、CentOS 7、macOS Sonoma 上运行 docker run得到的是完全一致的二进制行为——这解决了传统“在我机器上能跑”的协作噩梦。但前提是你的 Docker 环境本身是干净的。我统计过近三个月客户部署失败案例63% 的问题出在 Docker 引擎预检环节而非 Dify 配置本身。比如docker info显示Storage Driver: overlay2是必须的但某些旧版 CentOS 7 默认用 devicemapper会导致 PostgreSQL 数据卷写入失败现象是容器反复 restartdocker logs dify-db报错FATAL: could not write lock file postmaster.pid: No space left on device实际磁盘有 20G 剩余docker version中 Client 和 Server 版本差超过 2 个大版本如 Client 24.0Server 20.10会导致 docker-compose.yml v3.8 语法解析异常报错version is unsupported更隐蔽的是sysctl -w vm.max_map_count262144未执行导致 Elasticsearch 启动失败日志里只有一行max virtual memory areas vm.max_map_count [65536] is too low, increase to at least [262144]新手往往忽略这条警告以为是端口冲突。所以我的 Docker 部署流程强制插入三道预检关卡内核与存储驱动校验# 必须返回 overlay2 docker info | grep Storage Driver # 必须返回 1表示 cgroup v2 已启用Kubernetes 兼容前提 stat -fc %T /sys/fs/cgroup # 检查内核版本Ubuntu 22.04 要求 5.15否则 overlay2 性能劣化 uname -r资源阈值硬性检查# 内存Dify 最小推荐 8G但实测 6G 可跑基础功能无知识库 free -h | awk /^Mem:/ {print $2} # 磁盘/var/lib/docker 至少预留 20GPostgreSQL WAL 日志 Weaviate 向量索引增长 df -h /var/lib/docker | awk NR2 {print $4} # 文件句柄Dify 后端并发连接数默认 1000需 ulimit -n 65536 ulimit -nDocker Desktop 特殊处理仅 macOS/Windows提示Docker Desktop 默认分配 2GB 内存这连 PostgreSQL 都起不来。必须进入 Settings → Resources → Memory调至6GB 起步。且关闭 “Use the WSL 2 based engine”Windows或 “Use Rosetta for x86/amd64 emulation”M1/M2 Mac——Dify 镜像为 amd64 构建强行模拟性能损失 40% 以上。2.2 docker-compose.yml 的 7 处魔鬼细节——官方模板没说但线上必填Dify 官网提供的 docker-compose.yml 是极简版适合演示但离生产可用差 7 个关键补丁。我以客户真实环境Ubuntu 22.04 Docker 24.0.7的最终版为例逐行拆解version: 3.8 services: # --- 1. PostgreSQL必须显式指定 initdb 参数否则中文全文检索失效 --- db: image: postgres:15-alpine restart: always environment: POSTGRES_DB: dify POSTGRES_USER: dify POSTGRES_PASSWORD: dify # 关键启用中文分词扩展 POSTGRES_INITDB_ARGS: --auth-hostmd5 --auth-localtrust --lc-collateC.UTF-8 --lc-ctypeC.UTF-8 volumes: - ./volumes/db:/var/lib/postgresql/data # 关键禁用 fsync 强制刷盘开发/测试环境可接受提升 3x 写入速度 command: postgres -c fsyncoff -c synchronous_commitoff -c full_page_writesoff healthcheck: test: [CMD-SHELL, pg_isready -U dify -d dify] interval: 30s timeout: 10s retries: 5 # --- 2. Redis必须设置 maxmemory-policy否则内存溢出崩溃 --- redis: image: redis:7-alpine restart: always command: redis-server --save 60 1 --maxmemory 512mb --maxmemory-policy allkeys-lru volumes: - ./volumes/redis:/data healthcheck: test: [CMD, redis-cli, ping] interval: 30s timeout: 5s retries: 5 # --- 3. Web 服务环境变量不是可选而是强制覆盖项 --- web: image: difyai/dify:latest restart: always ports: - 3000:3000 # 前端 - 5001:5001 # API官方文档写 5000但 v1.0 已改为 5001 environment: # 关键必须显式声明 SECRET_KEY否则每次重启 session 失效 SECRET_KEY: your-32-byte-secret-key-here # 生成命令openssl rand -base64 32 # 关键DATABASE_URL 必须带 ?sslmodedisable否则 PostgreSQL 连接拒绝 DATABASE_URL: postgresql://dify:difydb:5432/dify?sslmodedisable # 关键REDIS_URL 必须带 db0否则 Celery 任务队列无法识别 REDIS_URL: redis://redis:6379/0 # 关键LLM_PROVIDER 必须设为 openai 或 ollama空值导致启动报错 LLM_PROVIDER: ollama # 关键OLLAMA_BASE_URL 必须指向宿主机 IP非 localhost OLLAMA_BASE_URL: http://host.docker.internal:11434 # macOS/Windows # OLLAMA_BASE_URL: http://172.17.0.1:11434 # Linuxdocker0 网桥 IP # 关键WEAVIATE_ENDPOINT 必须用 service 名不能写 localhost WEAVIATE_ENDPOINT: http://weaviate:8080 # 关键CELERY_BROKER_URL 必须与 REDIS_URL 一致否则异步任务不执行 CELERY_BROKER_URL: redis://redis:6379/0 # 关键ENABLE_WEB_SOCKET 必须设为 true否则聊天界面无实时响应 ENABLE_WEB_SOCKET: true depends_on: db: condition: service_healthy redis: condition: service_healthy weaviate: condition: service_healthy volumes: - ./volumes/web:/app/storage # 知识库文件、日志、临时上传都存在这里 # 关键添加资源限制防止单个容器吃光内存 deploy: resources: limits: memory: 3G cpus: 1.5 # --- 4. Weaviate必须禁用监控端口否则与 Dify 冲突 --- weaviate: image: semitechnologies/weaviate:1.24.4 restart: always environment: QUERY_DEFAULTS_LIMIT: 25 AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: true PERSISTENCE_DATA_PATH: /var/lib/weaviate DEFAULT_VECTORIZER_MODULE: none CLUSTER_HOSTNAME: node1 # 关键关闭 Prometheus 监控端口Dify 自带监控双开导致端口占用 DISABLE_TELEMETRY: true # 关键必须显式设置 CORS否则前端跨域请求失败 AUTHENTICATION_APIKEY_ALLOWED_KEYS: weaviate-api-key AUTHENTICATION_APIKEY_ADMIN_KEY: weaviate-admin-key volumes: - ./volumes/weaviate:/var/lib/weaviate ports: - 8080:8080 healthcheck: test: [CMD, curl, -f, http://localhost:8080/v1/.well-known/ready] interval: 30s timeout: 10s retries: 5注意host.docker.internal是 Docker Desktop 的特殊 DNSLinux 下不存在。必须用ip route | awk /default/ {print $3}获取宿主机在 docker0 网桥上的 IP通常是 172.17.0.1。这是 Linux 用户 Docker 部署失败的头号原因——他们把 macOS 配置原样复制结果 Ollama 服务根本连不上。2.3 启动后的三分钟黄金检测清单——比看日志更快定位问题docker-compose up -d后不要急着打开浏览器。执行以下 5 条命令3 分钟内确认核心链路是否打通检查容器状态是否全绿docker-compose ps | awk $4 !~ /Up/ {print $1, $4} # 任何非 Up 状态都标红如果看到web Exit 1立刻docker-compose logs web | tail -2090% 是环境变量拼写错误如DATA_BASE_URL少了个 S。验证 PostgreSQL 连通性绕过 Difydocker exec -it dify_db psql -U dify -d dify -c SELECT version();成功返回 PostgreSQL 版本即证明数据库层 OK。若报错psql: error: connection to server at localhost failed说明db服务名在web容器内解析失败——检查depends_on是否漏写或network_mode: bridge是否误加。测试 Redis 键值存取验证缓存层docker exec -it dify_redis redis-cli SET test_key hello \ docker exec -it dify_redis redis-cli GET test_key返回hello即通过。若超时检查redis容器的command是否遗漏--save参数持久化开关关闭会导致容器退出。直连 Weaviate API跳过 Dify 中间层curl -X GET http://localhost:8080/v1/meta应返回 JSON 包含version字段。若返回Connection refused检查weaviate容器的ports是否映射正确或healthcheck是否失败常见于磁盘空间不足。触发一次 Dify 内部健康检查curl -X GET http://localhost:5001/health正常返回{status:ok,timestamp:...}。若返回 503说明web容器虽运行但未能连接 DB/Redis/Weaviate 中任一服务——此时docker-compose logs web的第一行错误就是根因。这五步做完Dify 基础服务链路就算通关。接下来才是真正的业务验证上传 PDF 测试知识库切片、创建 LLM 模型配置、运行一个简单工作流。但请记住——Docker 部署的成败80% 在启动前的预检和配置20% 在启动后的验证。我见过太多人花 2 小时调web容器的环境变量却忽略db容器的POSTGRES_INITDB_ARGS导致中文搜索永远返回空。3. Kubernetes 部署不是“把 docker-compose 翻译成 YAML”而是重构运维范式3.1 Kubernetes 部署的本质矛盾Dify 的“有状态”与 K8s 的“无状态”哲学冲突Kubernetes 的设计哲学是“一切皆可驱逐、一切皆可重建”。Pod 挂了删掉Controller 会新建一个。节点宕机Pod 自动漂移到其他节点。但 Dify 的核心组件偏偏是强状态的PostgreSQL 的 WAL 日志、Weaviate 的向量索引文件、Redis 的内存数据、Web 服务的/app/storage目录存用户上传的 PDF/PPT——这些都不能随 Pod 重建而丢失。所以 Kubernetes 部署 Dify 的第一课不是写 Deployment而是重新定义“状态”的归属数据库状态必须绑定 PersistentVolumeClaimPVC且 PVC 的volumeMode: FilesystemaccessModes: [ReadWriteOnce]是底线。我曾见某团队用ReadWriteMany挂载 NFS结果 PostgreSQL 启动时报错could not open file global/pg_filenode.map: Permission denied——因为 NFS 的 POSIX 权限模型与 PostgreSQL 的文件锁机制不兼容。向量库状态Weaviate 官方明确要求使用hostPath或local类型 PV非网络存储否则向量索引写入性能下降 10 倍。这意味着你必须在每个计划运行 Weaviate 的节点上预先创建/var/lib/weaviate目录并chown 1001:1001Weaviate 容器 UID。应用状态/app/storage目录必须挂载独立 PVC且该 PVC 的storageClassName必须支持Retain回收策略而非默认的Delete。否则kubectl delete -f dify.yaml后所有知识库文件永久消失。因此Kubernetes 部署 Dify 不是“翻译”而是状态分层治理把 PostgreSQL、Weaviate、Redis 的状态分别托管给不同类型的存储后端再用 StatefulSet 确保它们有稳定的网络标识如dify-db-0.dify-db-headless最后用 Deployment 管理无状态的 Web 层。3.2 生产级 K8s 清单的 5 个不可妥协字段——比 Helm Chart 更可靠Dify 官方 Helm Chartchart.dify.ai虽方便但隐藏了太多魔改空间。我为客户定制的生产环境清单全部手写 YAML核心在于 5 个字段的精确控制3.2.1 PostgreSQL StatefulSetinitContainer 是救命稻草apiVersion: apps/v1 kind: StatefulSet metadata: name: dify-db spec: serviceName: dify-db-headless replicas: 1 template: spec: initContainers: - name: init-chmod image: busybox:1.35 command: [sh, -c] args: - | echo Fixing permissions for PostgreSQL data dir... chown -R 70:70 /var/lib/postgresql/data chmod -R 0700 /var/lib/postgresql/data volumeMounts: - name: pgdata mountPath: /var/lib/postgresql/data containers: - name: postgres image: postgres:15-alpine env: - name: POSTGRES_DB value: dify - name: POSTGRES_USER value: dify - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: dify-secrets key: db-password # 关键必须显式设置 securityContext否则 OpenShift 等加固环境启动失败 securityContext: runAsUser: 70 runAsGroup: 70 fsGroup: 70 volumeMounts: - name: pgdata mountPath: /var/lib/postgresql/data # 关键livenessProbe 必须用 pg_isready不能用 tcpSocket端口通不代表 DB 可用 livenessProbe: exec: command: [pg_isready, -U, dify, -d, dify] initialDelaySeconds: 60 periodSeconds: 30 volumeClaimTemplates: - metadata: name: pgdata spec: accessModes: [ReadWriteOnce] resources: requests: storage: 20Gi # 关键storageClassName 必须指定否则用 default class可能不满足性能要求 storageClassName: ssd-sc # 需提前创建 StorageClass实操心得initContainer的chown是 PostgreSQL 在 K8s 中存活的关键。Alpine 镜像的默认 UID 70 对应postgres用户但 PVC 挂载后目录属主常为 root导致 PostgreSQL 进程无权写入。这个 3 行脚本救活了我 8 个集群。3.2.2 Weaviate StatefulSet容忍度与亲和性必须双管齐下apiVersion: apps/v1 kind: StatefulSet metadata: name: dify-weaviate spec: serviceName: dify-weaviate-headless replicas: 1 template: spec: # 关键tolerations 让 Weaviate 只调度到 SSD 节点 tolerations: - key: disktype operator: Equal value: ssd effect: NoSchedule # 关键nodeAffinity 确保与 PostgreSQL 同节点降低向量检索延迟 affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - node-01 # 与 dify-db 同节点 containers: - name: weaviate image: semitechnologies/weaviate:1.24.4 env: - name: QUERY_DEFAULTS_LIMIT value: 25 - name: AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED value: true - name: PERSISTENCE_DATA_PATH value: /var/lib/weaviate - name: DEFAULT_VECTORIZER_MODULE value: none - name: CLUSTER_HOSTNAME value: node1 # 关键resources limits 必须设否则 OOMKilled 频发Weaviate 内存泄漏已知问题 resources: limits: memory: 4Gi cpu: 2 requests: memory: 2Gi cpu: 1 volumeMounts: - name: weaviate-data mountPath: /var/lib/weaviate # 关键livenessProbe 用 curl 检查 /v1/.well-known/ready不能用 /v1/meta后者不校验存储 livenessProbe: httpGet: path: /v1/.well-known/ready port: 8080 initialDelaySeconds: 120 periodSeconds: 60 volumeClaimTemplates: - metadata: name: weaviate-data spec: accessModes: [ReadWriteOnce] resources: requests: storage: 50Gi # 关键Weaviate 必须用 local PV不能用 network storage storageClassName: local-ssd注意local-ssdStorageClass 需提前在每个目标节点创建# 在 node-01 上执行 mkdir -p /mnt/local-ssd # 格式化为 ext4 并挂载 mkfs.ext4 /dev/nvme0n1 mount /dev/nvme0n1 /mnt/local-ssd # 创建 PV kubectl apply -f - EOF apiVersion: v1 kind: PersistentVolume metadata: name: weaviate-pv-node01 spec: capacity: storage: 50Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: local-ssd local: path: /mnt/local-ssd nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - node-01 EOF3.2.3 Web DeploymentConfigMap Secret 的分离艺术Dify 的环境变量多达 30 个全塞进 Deployment 的env字段既难维护又易泄露。我的方案是敏感信息进 Secret配置参数进 ConfigMap启动脚本做胶水。# dify-config.yaml apiVersion: v1 kind: ConfigMap metadata: name: dify-config data: # 非敏感配置放这里 APP_ENV: production LOG_LEVEL: INFO # 数据库连接字符串不含密码——注意DATABASE_URL 里密码用占位符 DATABASE_URL: postgresql://dify:dify-db:5432/dify?sslmodedisable # Redis 连接不含密码 REDIS_URL: redis://:dify-redis:6379/0 # Weaviate 地址 WEAVIATE_ENDPOINT: http://dify-weaviate:8080 # LLM 配置 LLM_PROVIDER: ollama OLLAMA_BASE_URL: http://dify-ollama:11434 --- # dify-secrets.yaml apiVersion: v1 kind: Secret metadata: name: dify-secrets type: Opaque data: # base64 编码的敏感值 db-password: ZGlm (base64 of dify) redis-password: # Redis 无密码留空 secret-key: eW91ci0zMi1ieXRlLXNlY3JldC1rZXktaGVyZQo (base64 of 32-byte key) --- # web-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: dify-web spec: replicas: 2 template: spec: containers: - name: web image: difyai/dify:latest # 关键envFrom 同时加载 ConfigMap 和 Secret自动替换占位符 envFrom: - configMapRef: name: dify-config - secretRef: name: dify-secrets # 关键启动前执行脚本拼接完整 DATABASE_URL command: [/bin/sh, -c] args: - | # 用 Secret 中的密码替换 DATABASE_URL 占位符 export DATABASE_URL\${DATABASE_URL/dify:/dify:\${db-password}} export REDIS_URL\${REDIS_URL/:/\${redis-password}} # 启动 Dify exec gunicorn --bind 0.0.0.0:5001 --workers 4 --worker-class sync --timeout 120 app:app volumeMounts: - name: storage mountPath: /app/storage volumes: - name: storage persistentVolumeClaim: claimName: dify-storage-pvc实操心得envFromcommand脚本的组合让敏感信息零明文出现在 YAML 中且避免了 Helm 的tpl函数复杂度。exec gunicorn ...中的exec是关键——它让容器 PID 1 是 gunicorn 进程而非 shell确保kubectl exec和信号传递正常。3.2.4 Ingress路径重写与 WebSocket 的生死线Dify 前端React和后端FastAPI共用一个域名但路由前缀不同/走前端/api/v1/和/chat/走后端。Kubernetes Ingress 必须精准重写否则出现“白屏”或“连接已关闭”。apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: dify-ingress annotations: # 关键nginx.ingress.kubernetes.io/rewrite-target 必须用 v1 注解 nginx.ingress.kubernetes.io/rewrite-target: /$2 # 关键WebSocket 必须开启否则聊天界面断连 nginx.ingress.kubernetes.io/upstream-hash-by: $connection_upgrade nginx.ingress.kubernetes.io/enable-cors: true nginx.ingress.kubernetes.io/cors-allow-origin: * spec: ingressClassName: nginx rules: - host: dify.example.com http: paths: - path: /api/v1/(.*) pathType: Prefix backend: service: name: dify-web port: number: 5001 - path: /chat/(.*) pathType: Prefix backend: service: name: dify-web port: number: 5001 - path: /(.*) pathType: Prefix backend: service: name: dify-web port: number: 3000注意path: /api/v1/(.*)中的(.*)是正则捕获组$2表示第二个捕获组即(.*)匹配的内容。这样/api/v1/chat-messages会被重写为/chat-messages发送给dify-web:5001。如果写成$1则重写为/api/v1/chat-messages后端路由不匹配。3.2.5 监控与日志用 Prometheus Operator 替代 Grafana CloudDify 自带/metrics端点但默认只暴露基础指标。生产环境必须注入自定义 exporter# prometheus-dify-exporter.yaml apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: dify-metrics spec: selector: matchLabels: app: dify-web endpoints: - port: http-metrics interval: 30s path: /metrics # 关键添加 bearer token 认证防未授权访问 scheme: https tlsConfig: insecureSkipVerify: true --- # dify-web-service.yaml 中添加 metrics 端口 ports: - name: http-metrics port: 9000 targetPort: 9000然后在 Dify Web 容器中启动prometheus-flask-exporter# Dockerfile.patch FROM difyai/dify:latest RUN pip install prometheus-flask-exporter COPY entrypoint.sh /entrypoint.sh ENTRYPOINT [/entrypoint.sh]# entrypoint.sh #!/bin/sh # 启动 Prometheus exporter export PROMETHEUS_MULTIPROC_DIR/tmp/prometheus_metrics gunicorn --bind 0.0.0.0:9000 --workers 1 --daemon prometheus_flask_exporter:app # 启动 Dify exec gunicorn --bind 0.0.0.0:5001 --workers 4 app:app这样Prometheus 就能采集到dify_api_request_duration_seconds_bucket等业务指标而不仅是process_cpu_seconds_total这类通用指标。4. 源码部署当标准化方案无法满足业务深度定制时的终极武器4.1 源码部署不是“高级玩家炫技”而是解决三个不可绕过的问题我坚持源码部署 Dify 的客户几乎都卡在同一个十字路口问题一需要修改知识库切片逻辑。某金融客户要求 PDF 表格识别必须保留原始行列结构而 Dify 默认的unstructured切片器会把表格打散成纯文本。这必须修改core/rag/extractor/pdf.py中的extract_pdf函数注入pdfplumber的表格提取逻辑。问题二需要对接私有认证体系。某政务云客户要求 Dify 登录必须走统一身份认证UAA而非内置的邮箱密码。这需要重写api/controllers/console/auth/login.py中的login方法并新增 UAA OAuth2 Provider。问题三需要定制工作流节点。某电商客户希望工作流中增加“调用内部 ERP 接口”节点而 Dify 官方只提供 HTTP、SQL、LLM 三类节点。这必须在core/workflow/nodes下新增erp_node.py并注册到NODES列表。这些问题用 Docker/K8s 部署无法解决——镜像里代码是冻结的。源码部署的价值不在于“能改”而在于“改完还能一键升级”。4.2 源码部署的黄金三角Python 环境、依赖管理、Git 分支策略4.2.1 Python 环境为什么必须用 pyenv virtualenv而非系统 PythonDify 依赖PyTorch 2.1、transformers 4.35、langchain 0.1.10这些包对 Python 版本极其敏感。Ubuntu 22.04 自带 Python 3.10但 Dify v1.0 要