Dify二次开发实战:修改源码构建自定义镜像全流程
修改DIFY源代码构建新镜像的方法如果你看到这篇文章大概率和我一样Dify的官方功能用着用着就不够用了。要么是内置的系统提示词不满足业务场景要么是想改掉工作流编排里某个节点的默认行为要么是想接入自己团队内部的鉴权逻辑。这时候大家第一反应都是直接改源码。但真动起手来会发现Dify是个前后端分离、带独立插件守护进程和代码沙箱的复杂项目直接改完代码重启容器改动根本不生效——因为容器里跑的是官方构建好的镜像压根没有你的代码。我最早也在这个问题上卡了两三天网上能搜到的教程多半只讲docker build一下就行但对为什么要这样构建、构建完了怎么替换、改了前端为什么还要动nginx这类细节基本一笔带过。这篇文章把我自己从改代码到成功构建自定义镜像的完整过程写出来包括踩过的坑和排查思路给同样在二开Dify的团队一个可复用的参考。先说清楚这篇文章适合谁已经完成Dify本地部署不管是用docker compose还是裸机部署想修改前端界面、后端API逻辑或工作流行为并且希望把改动固化到镜像里方便分发到测试或生产环境的人。如果你纯粹只是想配个模型供应商、搭个知识库那不需要改源码官方文档就够用了。1. 为什么不能改完容器里的代码再commit很多人第一次尝试改Dify源码走的都是这条捷径进容器、改文件、docker commit、保存为新镜像。说实话这招在小项目里偶尔能糊弄过去但用在Dify身上有四个很现实的问题。首先是可写层的临时性。容器运行时的所有修改都写在容器的可写层而Dify的api容器里有大量运行时生成的缓存文件比如__pycache__、临时上传文件、.env配置的运行时副本。如果你直接commit这些垃圾文件会一并被打进镜像镜像体积膨胀不说还容易把本地环境的敏感配置一并带进生产这是很危险的事。其次是代码分散的问题。Dify不止一个代码仓库模块api目录是Python后端Flask Celeryweb目录是Next.js前端docker目录里是编排和各个服务的Dockerfile另外plugin_daemon、sandbox也都在不同目录。你在容器里改的是某一个容器内的文件但其他关联模块并不会跟着更新commit出来的镜像往往是半个新的。第三是升级问题。Dify官方版本迭代很快社区和官方几乎每周都有bug修复。如果你长期基于commit镜像维护下次官方发新版本时你根本没法优雅地做diff合并只能重新手工改一遍代码二开成本会累积得非常高。最后是不可变基础设施的原则问题。镜像本身应该是构建产物而不是手改出来的艺术品。只有把源码改动固化到Dockerfile里才能保证任何人、任何时候构建出来的镜像行为一致这才是团队协作里能复现的东西。所以在正式动手之前我强烈建议你先调整一下思路Dify的二次开发不应该在容器里改而是把整个项目源码仓库拉下来在宿主机上改然后通过构建脚本生成自己的镜像。2. 动手前的源码目录与官方构建机制拆解在改任何代码之前先把Dify的源码仓库结构搞清楚。我以目前较新的release版本为例你用git clone拉下来的目录大致是这样的dify/ ├── api/ # Python后端服务Flask ├── web/ # Next.js前端 ├── docker/ # Docker编排及镜像构建相关 │ ├── docker-compose.yaml │ ├── api/ │ │ └── Dockerfile │ ├── web/ │ │ └── Dockerfile或docker-web.Dockerfile │ ├── nginx/ │ │ └── conf.d/default.conf │ └── ... ├── plugin_daemon/ # 插件守护进程 ├── sandbox/ # 代码沙箱服务 └── ...你真正需要操心的构建工作主要集中在两块api镜像和web镜像。plugin_daemon和sandbox一般不需要频繁改动除非你要深度定制插件机制或代码解释器。官方在docker目录下提供了一个构建脚本核心逻辑是分阶段构建前端Next.js项目先执行npm run build把静态产物输出到.next目录然后复制到nginx镜像中由nginx托管后端则是把api目录拷贝进Python镜像安装依赖后启动gunicorn。你需要理解的关键点在于Dify的web镜像实际上是一个nginx 静态文件的复合镜像前端代码构建完就落入nginx的/usr/share/nginx/html目录而后端api镜像才是真正运行Python进程的容器。这个机制解释了为什么很多人改前端代码只重建api容器不生效——因为前端根本不在api里跑你改的是Next.js代码必须重新构建web镜像或者至少把新的静态产物挂载进nginx容器。另外还要注意Dify的docker-compose.yaml里各个服务用的是image: langgenius/dify-api:xxx、langgenius/dify-web:xxx这类公共镜像名。这意味着就算你本地把镜像构建出来了如果不修改compose文件里的image标签重启服务时仍会去拉公共仓库镜像你的改动还是会被覆盖掉。这一步是很多人忽略的细节我自己就在这上面栽过跟头。3. 后端API源码修改与自定义镜像构建全流程3.1 准备克隆仓库并锁定版本我建议不要直接clone最新的main分支而是先在你当前部署的版本上打tag。比如你线上跑的是1.0.0或者某个release版本就用git checkout切到对应分支。这样做的原因是main分支往往是开发态依赖和配置都可能是新的直接拿去构建很可能出现环境兼容问题而切到和线上一致的版本构建出来的镜像至少能保证和你当前的部署是一套代码基线排查问题时会省很多力气。git clone https://github.com/langgenius/dify.git cd dify git tag -l | sort -V | tail -20 git checkout 1.0.0 # 替换为你实际的版本号3.2 修改源码从哪下手后端代码改起来最常见的几个位置api/core/model_runtime/想改模型供应商接入逻辑、自定义模型参数校验、增减模型能力都在这里。api/core/tools/内置工具或自定义工具的实现。api/core/workflow/工作流节点执行逻辑比如你想给某个节点增加新的分支条件或改变上下文传递方式。api/services/业务服务层像是会话处理、知识库检索逻辑。拿我自己的例子来说当时业务需要让知识库检索时能带上用户自定义的metadata过滤条件原始代码只支持按数据集id过滤。我改的地方就在api/core/rag/datasource/下的检索逻辑给检索参数结构里增加了一个metadata_filter字段并在向量数据库查询语句中拼接了过滤条件。改代码的时候有个建议尽量在源码里补注释标明改动点和原因不要直接改得面目全非。因为后续你还需要把官方新版本的变化merge进来改动点越清晰merge时越不容易冲突。3.3 构建后端镜像Dify官方其实提供了构建脚本在docker目录下有个build_api_image.sh或者类似命名的脚本。但我不太建议直接依赖这个脚本因为你可能改了依赖文件比如新增了Python包它不一定覆盖到。我更推荐直接手动构建逻辑更透明。假设你改了api/requirements.txt新增了一个库构建命令如下cd dify/api docker build -f ../docker/api/Dockerfile -t yourregistry/dify-api:custom-1.0.0 .如果顺利镜像会构建成功。但如果你改动了Python依赖这里有个常见坑Dockerfile里的pip install会有缓存层你改了requirements.txt但构建时用了缓存新依赖根本没装进去。保险的做法是构建时加--no-cache参数docker build --no-cache -f ../docker/api/Dockerfile -t yourregistry/dify-api:custom-1.0.0 .这里顺便把Dockerfile怎么写的也讲一下方便你自己加步骤。Dify后端的Dockerfile核心内容大致是FROM python:3.10-slim WORKDIR /app/api COPY . /app/api RUN pip install --no-cache-dir -r requirements.txt ENV FLASK_APPapp.py EXPOSE 5001 CMD [gunicorn, -c, gunicorn.conf.py, app:app]如果你想把自己的配置文件或者额外的启动脚本打进去可以增加这样几行COPY custom_config /app/custom_config ENV CUSTOM_CONFIG_PATH/app/custom_config总而言之Dockerfile的改动原则是保持官方基础结构不变在你需要的环节穿插自定义步骤这样维护成本最低。3.4 构建完成后如何替换容器镜像镜像构建好之后真正要动的是docker/docker-compose.yaml。打开文件找到api服务api: image: langgenius/dify-api:1.0.0 # 其他配置...把image改成你刚才构建出来的镜像名api: image: yourregistry/dify-api:custom-1.0.0然后执行docker compose down docker compose up -d api这里有一个需要注意的顺序问题如果你只是docker compose up -d它不会自动拉你本地新构建的镜像除非本地的image tag和compose里写的一致且本地已有该镜像。所以正确的做法是先改compose文件再down再up确保容器是用新镜像创建的。验证是否生效可以进容器看一下docker exec -it dify-api-1 bash cat /app/api/core/rag/datasource/xxx.py | grep metadata_filter exit能看到你的改动就说明镜像里的代码确实是新的。4. 前端代码的修改与镜像构建别再傻傻只重建api了前端往往是大家最困惑的地方。Dify的前端是Next.js项目构建流程比后端更重还牵涉nginx。我自己最早改前端的工作流节点图标和文案时以为和改后端一样build up就完事了结果死活不生效一度怀疑是不是前端代码不在这个仓库里。后来才搞明白整个链路。4.1 前端构建的完整链路Dify前端的Dockerfile长这样核心逻辑FROM node:18-alpine AS builder WORKDIR /app/web COPY . /app/web RUN npm install --registryhttps://registry.npmmirror.com RUN npm run build FROM nginx:stable-alpine COPY --frombuilder /app/web/.next/static /app/.next/static COPY --frombuilder /app/web/.next/server /app/.next/server COPY --frombuilder /app/web/public /app/public COPY nginx.conf /etc/nginx/conf.d/default.conf也就是说前端源码会被编译成静态资源然后塞进nginx镜像。nginx容器启动后直接托管这些静态文件然后把/console/api、/api这些路径反向代理到后端的api容器。所以你改了前端代码必须重新构建web镜像只重建api容器当然不会生效。构建命令cd dify/web docker build -f ../docker/web/Dockerfile -t yourregistry/dify-web:custom-1.0.0 .前端构建比较吃内存如果你在构建过程中遇到JavaScript heap out of memory的错误多半是Node进程内存不够可以在构建时加环境变量docker build --build-arg NODE_OPTIONS--max-old-space-size4096 -f ../docker/web/Dockerfile -t yourregistry/dify-web:custom-1.0.0 .4.2 改docker-compose中的web服务同理把docker-compose.yaml里的web服务image改成你自己的web: image: yourregistry/dify-web:custom-1.0.0然后重启docker compose up -d web前端改动生效后最好强制刷新浏览器CtrlShiftR因为nginx对静态资源做了缓存有时候你以为改动没生效其实只是浏览器缓存的问题。4.3 如果只改了一处文案有没有轻量方案如果你的改动只是改几个中文字符串或样式每次重新构建一整个Next.js项目确实很亏一次构建可能要好几分钟。临时调试阶段可以这样绕过直接把修改后的文件挂载到容器里覆盖原文件。比如你在宿主机改了web/app/components/xxx.tsx你想先看效果可以进web容器找到编译后的静态资源手工替换。但这不是长久之计容器一重启改动就没了。我个人的习惯是先挂载源码改到满意再走完整构建流程固化到镜像里。调试效率和最终可交付性两头都占。5. 改plugin_daemon或sandbox的特殊注意事项如果你的改动涉及插件守护进程或代码沙箱这两个服务在docker-compose中的角色比较特殊。尤其sandbox它的构建需要Rust工具链和api、web完全不是一个套路。我自己没怎么深入改过sandbox但和几个做Dify深度二开的朋友交流过普遍反馈是能不动尽量别动能通过api层绕过去的逻辑就尽量绕。如果你确实需要自定义插件守护进程要注意plugin_daemon的启动方式是通过sandbox和plugin_daemon两个容器配合的插件市场拉取插件的逻辑在这个服务里。改动它通常意味着你需要同时改plugin_daemon和它对应的docker-compose配置比如新增环境变量、挂载新的目录。这些配置项在compose里都是显式的改起来并不难难在调试——插件守护进程的日志比较杂乱排查起来很费劲。6. 踩坑实录从改完无效到构建成功的完整排查链路这一节讲几个我实际遇到过、且网上资料极少的问题。按排查顺序写出来希望你遇到时可以少走弯路。6.1 改了代码构建镜像后容器还是老样子这是最经典的问题。我当时改了api的检索逻辑构建完新镜像也改了compose里的image标签然后docker compose up -d重启进容器一查代码居然还是旧的。排查过程如下先检查镜像本身docker run --rm yourregistry/dify-api:custom-1.0.0 cat /app/api/core/rag/datasource/xxx.py发现镜像里的代码确实是新的。那问题就出在容器没有用新镜像。于是我看了一下容器的启动时间docker ps --format table {{.Names}}\t{{.Image}}\t{{.Status}}发现容器显示的是旧镜像的ID说明docker compose up -d根本没用新镜像重建容器。后来才意识到Dify的docker-compose文件里给容器做了container_name固定并且容器已经存在时compose默认不会用新镜像重建必须显式docker compose up -d --force-recreate api。这是compose的默认行为和Dify本身无关但很容易绊倒人。之后我的标准操作就是docker compose up -d --force-recreate api web6.2 前端构建成功但页面白屏有一次我改了前端某个页面组件构建镜像成功起容器后打开页面直接白屏。控制台报的全是ChunkLoadError典型的是webpack或Next.js的运行时加载chunk失败。排查链路是这样的先看nginx静态文件是否更新——进web容器看/app/.next/static/chunks目录下文件的生成时间发现是新的。再看nginx配置发现它会缓存*.js文件缓存的key是文件名哈希。而Next.js构建时会生成新的文件名按理说不会命中缓存。那问题出在哪后来发现是docker compose up -d web的时候web容器虽然在跑但nginx配置里有一段对/_next/static/的proxy_cache缓存的存储位置是/var/cache/nginx。旧镜像容器和新镜像容器虽然名字一样但容器重建后nginx缓存是空的理论上会回源拿新文件。可问题是我本机浏览器保留了旧的Service WorkerNext.js的PWA缓存把旧的JS文件缓存住了。清掉Service Worker再强刷就好了。这个问题的教训是前端改动后出现诡异白屏优先排除浏览器缓存和Service Worker再去查镜像内容。6.3 构建api镜像时pip install超时或失败如果你的网络环境访问官方PyPI源不稳定pip install阶段很容易超时。我用的方案是改Dockerfile里的pip源为国内镜像RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple \ pip install --no-cache-dir -r requirements.txt这样构建速度会快很多也不容易中断。6.4 后端的改动不生效但你确认代码在容器里有时候镜像里的代码确实是新的但运行时的行为还是老的。这时候要怀疑两个东西一是.pyc缓存。Dify后端启动时用gunicornPython会把import过的模块编译成.pyc文件放在__pycache__里。如果你的容器是从旧镜像升级来的但挂载了持久化volume覆盖了/app/api目录那新旧代码就可能混在一起。我建议在Dockerfile里加一行构建时清理缓存RUN find /app/api -type d -name __pycache__ -exec rm -rf {} || true二是环境变量。Dify很多行为是环境变量控制的比如DEBUG模式、LOG_LEVEL、BILLING_ENDPOINT等。如果你的代码改动需要配合某个环境变量才能激活比如自定义了特性开关一定要在compose文件里显式加上。6.5 构建依赖顺序导致的改了没生效我自己在构建顺序上吃过大亏先改了后端的api代码构建了新镜像但docker-compose里的api容器一直起不来。查日志发现是启动时连数据库失败然后我以为是数据库的问题排查半天最后发现原因很简单——我改了api代码里的配置项名称但.env文件里还是旧的变量名导致应用启动时找不到配置直接崩溃。所以每次改代码尤其是涉及配置项、环境变量时一定要全局搜一下这个变量在compose文件和.env里的引用别只盯着代码本身。7. 进阶把自定义镜像固化到团队发布流程如果你只是个人实验构建完镜像手动改compose就行了。但如果你是团队协作我还是建议把流程规范化。我们团队现在的做法是在Dify源码仓库的基础上维护一个二开分支所有改动先提交到分支然后由一个构建脚本统一产出镜像。脚本核心逻辑大概是这样#!/bin/bash VERSION$1 REGISTRYyourregistry.com/dify # 构建api镜像 docker build --no-cache \ -f docker/api/Dockerfile \ -t $REGISTRY/dify-api:$VERSION \ ./api # 构建web镜像 docker build --no-cache \ -f docker/web/Dockerfile \ -t $REGISTRY/dify-web:$VERSION \ ./web # 推送镜像 docker push $REGISTRY/dify-api:$VERSION docker push $REGISTRY/dify-web:$VERSION镜像推到私有仓库后生产环境的docker-compose里的image标签就指向私有仓库的地址发布时只需docker compose pull docker compose up -d。这样每次发版都有迹可循出问题可以快速回滚到上一个镜像标签。这里补充一句镜像仓库建议用私有化的不管是Harbor还是阿里云ACR都行尽量不要把二次开发的镜像推到公共仓库毕竟里面可能带了内部业务逻辑和后端配置信息。8. 构建之外的扩展思路Dify二次开发还能做什么最后聊聊构建镜像之外的事。你费这么大劲把构建链路跑通肯定不是为了改个logo或文案对吧基于Dify做二次开发比较常见的方向有这么几类给准备入手的你一些参考一是接入内部模型网关。很多公司内部有统一的LLM网关Dify默认的模型供应商里没有。你可以在api/core/model_runtime下照着现有供应商的结构加一个自定义provider让它调用你公司内部的HTTP接口。这个改动涉及模型校验、参数映射、鉴权逻辑典型的需要走完整构建流程。二是改造知识库检索策略。Dify默认的检索策略是向量相似度加Rerank但有些场景下你需要把关键词匹配、规则过滤或业务权限过滤混进去。这些逻辑都集中在api/core/rag相关目录里改完直接关系到检索质量属于后端核心改动。三是自定义工作流节点。Dify的工作流编辑器支持通过插件系统添加自定义节点但如果你需要的节点不是简单的工具调用而是要和内部系统深度交互那直接在源码里增加一个节点类型更稳妥。这需要你同时对web前端节点面板展示和api后端节点执行逻辑做改动两个镜像都要重建正好用得上前面说的全流程。四是UI深度定制。比如把Dify的控制台嵌入到你自己的管理后台里或者修改登录页、隐藏部分菜单。前端改完web镜像就能搞定。五是嵌入企业级能力比如对接SSO、统一权限管理、审计日志。这类改动一般横跨前后端而且大概率需要引入新的Python依赖或npm包这正是构建自定义镜像才能解决的场景。从我个人的实践经验看Dify的二次开发价值很大但前提是你得把构建、发布这条链路理顺。否则每次改动都靠进容器手改来维持短期看着快长期一定会被不断上升的维护成本拖垮。希望这篇基于我踩坑经历写出来的构建指南能让你在二开Dify的路上少走几步弯路。