最近接了个人工智能平台私有化落地的需求,要求特别直接:Dify要在完全隔离的内网环境中部署,机器不允许访问公网,所有依赖都得提前准备好,一次性搬进内网。这个需求听起来不复杂,但真做起来,坑比想的要多。Dify本身依赖的组件不少,前端、后端、异步任务、数据库、向量库、沙箱执行环境,再加上插件机制和本地模型服务,每一环都要在离线状态下自洽运转。这篇文章就是把这套Dify内网离线部署的完整思路、操作步骤和踩坑经验整理出来,给同样要做私有化部署的团队一个直接能用的参考。
先说清楚这篇文章适合谁看:负责企业内网AI平台部署的工程师、想在生产环境做Dify私有化交付的团队,以及那些对数据安全要求极高、必须把模型和业务系统都放在隔离网里的项目。阅读之后你会得到一条可复现的离线部署路径,而不是零散的命令片段。
1. 为什么要在内网离线部署Dify(先把思路捋清楚)
1.1 什么样的场景需要离线部署
我这次面对的客户属于数据敏感型行业,业务流程中涉及的数据不允许出内网,服务器也不能主动访问公网。这类要求在很多行业都能见到:政务系统、金融核心业务、能源调度、医疗数据平台,甚至一些大型企业内部的自研系统,都对网络边界有严格约束。
除了“完全禁止外联”这种最严的场景,还有两类变体:一类是服务器可以上外网,但只开放特定域名白名单,比如允许访问模型API,不允许访问其他站点;另一类是服务器在DMZ区,需要从中转机单向导入数据。不同约束对应的部署策略差别很大。
真正让我觉得“必须按离线部署来设计”的,不是网络能不能通,而是交付的确定性。如果每次都靠“临时开个窗口下载依赖”,哪天资源被回收、网络策略变更,整个系统可能直接瘫掉。把Dify连同所有中间件、模型文件、插件包全部离线打包,到了内网一次装好,才是可持续的交付方式。
1.2 Dify架构里到底依赖了哪些组件
在动手之前,必须先把Dify的部署拓扑看清楚。Dify不是单进程应用,而是一组容器协作运行的平台。从docker-compose配置来看,主要包含这些角色:
- api:后端服务,负责应用编排、对话逻辑、知识库管理、鉴权等核心业务。
- worker:Celery异步任务队列消费者,处理文档解析、索引构建、批量任务等耗时操作。
- web:前端静态资源服务,也就是用户在浏览器里看到的界面。
- db:PostgreSQL,保存业务数据、用户信息、应用配置。
- redis:缓存和消息队列,api与worker之间的任务传递依赖它。
- weaviate / qdrant:向量数据库,知识库的向量检索全靠它。
- sandbox:代码执行沙箱,用于Agent工具和插件里的代码片段安全执行。
- plugin_daemon:较新版本引入的插件守护进程,负责插件的生命周期管理。
- nginx / ssrf_proxy:入口网关和SSRF防护代理。
这意味着离线部署不是“拷一个安装包”那么简单。上述每个容器的镜像、配置、数据卷结构都要一并迁移。更麻烦的是,Dify本身不包含大模型能力,你还得额外准备本地推理服务和模型权重文件,这些同样属于离线资产,少一个都跑不起来。
1.3 为什么选docker compose而不是其他方式
有人会问,都做私有化部署了,为什么不用Kubernetes或者直接裸机安装?我的选择逻辑很简单:Dify官方主推的部署形态就是docker compose,离线环境下这个形态最容易控制依赖。
Kubernetes的离线部署涉及镜像仓库、etcd、kubelet组件、容器运行时等一整套基础设施,成本远高于Dify本身。裸机安装则需要手工管理Python环境、Node环境、PostgreSQL等,升级维护很痛苦。docker compose把所有依赖收敛成“镜像列表”和“一份compose文件”,离线包体积虽然大,但结构清晰:镜像tar包加上源码目录,在内网无论用docker load还是私有镜像仓库,都能稳定复现。
这里有个核心原则:能用官方默认方式就不要自创部署方式。Dify官方一直维护docker compose的部署文件,跟着官方走,遇到问题还能对照文档和社区经验排查。离线部署本来就要多处理了一个“搬运依赖”的环节,没必要再增加部署形态的变量。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 离线部署前的准备工作(能省的坑先省掉)
2.1 中转机的角色与网络规划
离线部署的第一步,是先找一台能上公网的“中转机”。这台机器负责四件事:拉取Docker镜像、下载Dify源码包、收集插件文件、准备模型权重。中转机不需要很高配置,但磁盘一定要大,Dify全家桶镜像加起来十几个GB很常见,如果还要带本地大模型,那几十GB甚至上百GB都有可能。
规划时我在中转机上建了一个统一目录,例如/data/dify-offline,下面分了images、source、models、plugins四个子目录。后期打包、拷贝、记录都很直观。
目标机的网络规划也不能忽视。内网机器之间通常有管理网段和业务网段,需要确认目标机与模型服务器、数据库之间的端口可以互通。Dify部署完成后,浏览器需要访问web入口端口(通常是80或者你自定义的端口),API与工作流服务需要访问模型推理端口,比如Ollama的11434、vLLM的8000。这些网络打通工作要提前和网络管理员确认,等到现场再调试,纯靠防火墙策略放行会浪费大量时间。
传输方式也值得提前定下来。离线包的传递路径无非三种:移动硬盘、内网共享目录、文件服务器。文件大、文件数量多,我建议用支持断点续传的文件传输方式,或者直接把整个离线目录打包成一个大压缩包,减少散碎文件拷贝时的意外。
2.2 镜像版本与依赖清单的锁定
开发环境里大家习惯用latest标签,但离线部署绝对不能这么干。镜像一旦生成,目标机就永远只能用离线包里这个版本,latest在转运机上可能随时漂移,今天拉的和明天拉的都不是同一个东西,离线包就失去了可复现性。
正确做法是锁版本。进入Dify源码中的docker目录,打开docker-compose.yaml,把每个image:字段整理成一份清单。可以用一个简单命令提取:
bash复制grep "image:" docker-compose.yaml | awk '{print $2}' | sort -u > images.txt
拿到这份清单后,逐项核对版本号。Dify自身组件的版本要跟Dify Release版本对应,数据库、Redis、向量库这些中间件也要固化版本,比如postgres:15-alpine、redis:7-alpine,不要含糊。为什么要连中间件版本都锁住?因为Dify的数据库迁移脚本是针对特定PostgreSQL版本测试过的,换个大版本,轻则性能异常,重则迁移直接失败。
版本锁定后,建议把images.txt和完整compose文件里实际引用的image:逐个核对一遍。经常出现的情况是,从外部拷贝的镜像清单和compose文件里引用的镜像不一致,加载完才发现文里少了某个tag,只能重新往返一趟。
2.3 模型文件与本地推理服务的准备
Dify只是应用编排层,本身不带模型权重。离线内网里如果要跑对话、知识库检索,必须有一个本地推理服务,常见的有三个选择:
- Ollama:部署最简单,适合单机、小参数模型,模型管理方便。
- Xinference:支持多种模型格式和推理后端,适合做统一模型管理。
- vLLM:吞吐性能好,适合GPU资源充足的场景,提供OpenAI兼容接口。
模型权重的离线准备,是很多团队会忽略的一环。以Ollama为例,如果你在外网机器上执行过ollama pull,模型文件会存在~/.ollama/models目录下。离线部署的正确姿势是:在转运机上用同一版本Ollama把所有需要的模型拉好,然后把整个models目录打包,拷贝到目标机挂载给Ollama容器。需要注意的是,Ollama不同版本对模型存储的路径和管理方式可能有调整,转运机和目标机尽量使用相同版本的Ollama镜像,减少兼容性问题。
如果是vLLM,则需要把模型权重文件按HuggingFace目录结构复制到目标机,例如qwen、bge这类模型,一个文件夹包含配置文件、权重文件和tokenizer文件。大模型权重动辄十几GB,这个传输量要提前估算。
2.4 插件与静态资源的离线准备
Dify较新版本把很多能力拆成了插件机制,比如网页解析工具、搜索工具、模型供应商适配器,都是插件。默认情况下插件从公网插件市场下载,离线环境根本连不上。如果不处理,插件守护进程会反复尝试连接市场,页面加载都受影响。
提前解决方法是:在公网环境进入Dify插件管理界面,搜索你需要的插件,下载对应的.difypkg离线包,放到离线包目录的plugins子目录。到了内网后,在插件管理中通过“上传/导入”方式安装这些插件。
另外,前端资源本身已经打包在web镜像中,正常不需要外网。但有一个细节要注意:如果界面里配置了需要外网才能访问的图标、字体CDN、统计脚本地址,浏览器在加载页面时会发出外网请求。离线环境下这些请求会超时,表现为页面打开慢、样式错乱。排查时需要打开浏览器开发者工具看Network面板,把外部URL逐个找出来去掉。
3. 镜像搬运实操:从外网到内网的核心步骤
3.1 在中转机拉取镜像并导出
镜像清单整理好后,进入中转机的docker目录执行拉取操作:
bash复制docker compose pull
用docker compose统一拉取的好处是省事,但输出埋在一堆日志里。如果某个镜像拉取失败不容易定位。我更建议按images.txt逐行拉取,写个简单的循环:
bash复制while read -r image; do
docker pull "$image"
done < images.txt
这样每拉一个镜像都有清晰的成功或失败输出,哪个镜像卡住了一眼就能看到。拉完之后,把全部镜像导出成一个tar包:
bash复制docker save $(cat images.txt) -o dify-images.tar
如果镜像数量多、体积大,可以先看下总大小:du -sh dify-images.tar。有人会问要不要压缩,docker save生成的tar本身没有压缩,用gzip压缩能省不少体积,比如十几个GB可能压到七八GB,但压缩耗时也比较明显。如果你的存储介质是高速移动硬盘,不压缩拷贝反而更快;如果走网络传输,压缩一次更划算。我通常不做gzip压缩,直接拷贝原包,少一个解压环节就少一个出错点。
导出完成后,顺手生成一个校验文件:
bash复制md5sum dify-images.tar > dify-images.tar.md5
到了内网可以快速校验包是否损坏。
3.2 目标机导入镜像并验证
离线包拷贝到目标机后,执行导入:
bash复制docker load -i dify-images.tar
导入过程中会逐层加载镜像层信息,输出量很大,不用每个都看。导入完成后做两件事。第一,检查磁盘空间:df -h,Docker镜像解压后占用的空间会比tar包体积更大,因为tar包是压缩存储层的拼接,导入后会完全展开。第二,列出镜像核对:
bash复制docker images | grep -E "dify|postgres|redis|weaviate"
确保所有镜像的REPOSITORY和TAG都在。这里最容易出的问题不是镜像缺失,而是tag不一致。比如compose文件里写的是postgres:15-alpine,但你离线包里只有postgres:15.2-alpine,启动时Docker会尝试从公网拉取,离线环境拉不到就直接失败。所以,compose文件的镜像tag必须以离线包实际导入的内容为准,或者反过来,离线包严格按照compose文件引用tag生成,两边必须一一对应。
3.3 离线私有镜像仓库方案
如果只是单机部署,docker load就够了。但如果你有一个内网集群,或者是需要频繁交付多套环境,那最好在内网搭一个私有镜像仓库(Registry)。操作思路是:
- 在目标内网找一台机器运行
registry:2容器,这台机器作为内网镜像中心。 - 在中转机拉完镜像后,把镜像推到这个内网仓库,而不是打tar包。
- 内网各节点从私有仓库拉镜像。
不过要注意,要让中转机能推送到内网仓库,中转机必须能连到内网仓库地址,这在内网隔离场景下不一定成立。更常见的做法是先docker save成tar,带到内网后load到某一台机器上,再把这台机器变成私有仓库,其他节点从它这里拉取。
为了长期维护考虑,我建议至少准备一套私有仓库方案。离线交付不是只交付一次,后续Dify升级、模型替换、节点扩容都离不开镜像分发。每台机器都手动load十几个G的tar包,不是长久之计。
4. Dify容器化服务的离线启动与配置
4.1 准备Dify代码与配置目录
镜像只是运行时,还需要有Dify的部署配置。建议直接从官方Release下载指定版本的源码压缩包(zip或tar.gz),在转运机解压后,连同docker目录整体拷贝到目标机的/opt/dify路径下。不要只拷贝compose文件,.env.example、volumes目录结构都是部署必需的。
进入/opt/dify/docker目录,执行:
bash复制cp .env.example .env
打开.env文件修改几个关键参数。第一,SECRET_KEY必须换成随机字符串,这是Dify会话和数据加密的密钥。第二,POSTGRES_PASSWORD要设置一个强密码,注意别用会被URL特殊字符解析干扰的符号,比如@、#,因为很多连接串是拼在URL里的。第三,INIT_PASSWORD是初始化管理员账号的密码,安装完成后用这个密码登录,登录后立即改掉。第四,EXPOSE_NGINX_PORT决定对外访问端口,默认80,如果机器上已有服务占用,改成其他端口。
4.2 中间件与服务启动顺序
离线环境首次启动,千万别一个docker compose up -d全部拉起,那样所有容器同时启动,数据库还没就绪API就开始连接,很容易出现一堆报错。我的习惯是先启动基础中间件:
bash复制docker compose up -d db redis weaviate
然后观察数据库是否就绪:
bash复制docker compose ps
docker compose exec db pg_isready
pg_isready返回accepting connections表示数据库可连接。这时再启动剩余服务:
bash复制docker compose up -d
API容器在启动时通常会执行数据库迁移。看日志:
bash复制docker compose logs -f api
当日志稳定下来、不再报错,再通过浏览器访问Web界面。不同版本的Dify迁移逻辑稍有差别,但“等API日志稳定再访问”这个原则通用。刚启动就疯狂刷新页面,反而容易因为服务还没就绪造成浏览器端缓存了错误状态,后续排查更混乱。
4.3 本地模型服务接入
Dify离线环境的关键一环是模型配置。以Ollama为例,目标机上先启动Ollama容器:
bash复制docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama
如果模型文件是通过挂载目录预置的,启动后直接就能识别;如果需要在线拉取,进入容器执行:
bash复制docker exec -it ollama ollama pull qwen2.5:7b
在Dify后台进入“设置 -> 模型供应商 -> Ollama”,配置Ollama的Base URL。这里有个经典坑:把Base URL填成http://localhost:11434。在Dify容器内部,localhost指向的是容器自己,并不是宿主机。正确做法是填宿主机在目标内网中的实际IP,比如http://192.168.10.5:11434。
如果使用vLLM或Xinference这类提供OpenAI兼容接口的服务,配置方式类似:在Dify里添加“OpenAI-API-compatible”供应商,Base URL填http://服务IP:端口/v1,API Key随便填一个合规字符串即可,因为本地推理服务通常不校验Key。
知识库场景还需要配置Embedding模型,否则文档上传后无法向量化。本地可以用bge-m3这类模型,在Ollama里拉取后,同样配置到Dify的Embedding选项里。配好模型后,不要急着建应用,先在模型供应商页面点“测试”,确认连通性和模型响应正常,再继续往下走。
4.4 插件离线安装操作
Dify启动后,进入“插件管理”页面,如果插件市场访问失败,页面会出现大量加载超时。这时需要手动导入插件包。在离线包plugins目录里,把提前准备好的.difypkg文件逐个上传,平台会自动安装并启用。
如果某个插件安装后依赖了外网服务,比如在线搜索、地图查询,那在内网环境里依然不可用,这不是部署问题,而是插件功能本身的边界问题。选择插件时务必要看插件描述,确认它是本地工具型插件,而不是依赖云服务。
插件安装完成后,重启一下相关容器(通常是docker compose restart plugin_daemon api worker),让插件信息同步到业务进程。如果暂时不需要插件,也可以在系统设置中关闭自动访问插件市场,减少无谓的网络超时。
5. 常见问题排查与避坑实录
5.1 镜像导入与存储问题
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
docker load报no space left on device |
磁盘空间不足 | 清理目标机冗余镜像或扩展存储,重新导入 |
docker load报invalid tar header |
tar包损坏、传输不完整 | 用校验文件md5sum核对,重新拷贝 |
启动时提示image not found |
镜像tag与compose不一致 | 两边统一tag,离线包以compose引用为准 |
| 拉取镜像时一直转圈/超时 | 离线环境拉了未内置的镜像 | 确认compose中所有镜像都在离线包内 |
印象最深的一次,是现场发现离线包里镜像全在,但启动时Docker还在尝试连外网。追查后发现compose环境变量里有个版本号写错了,导致镜像tag被拼成了另一个不存在的名字。所以不管离线包准备得多充分,启动时多留个心眼:如果某个容器一直ImagePullBackOff,先检查引用镜像tag是否正确,绝大多数情况不是网络问题,而是名字写歪了。
5.2 服务启动与页面访问问题
Web页面打不开,第一反应不要查代码,先看端口映射和容器状态。用docker compose ps看每个容器的状态,用docker compose logs nginx看入口代理日志。最常见的场景是API容器还在做数据库迁移,此时页面能打开但接口全是502。这种情况不用慌,等一两分钟再看。
如果页面一直转圈、部分样式丢失,打开浏览器控制台看Network面板,定位那个永远pending的外部请求。离线环境下这种请求不会成功,只会拖慢页面。找到后关闭对应功能,或检查.env里是否配置了外部统计、遥测相关变量。
离线环境不要忽略健康检查和依赖顺序。很多初始化失败不是逻辑错误,而是启动顺序错了。db还没准备好,api就在连库,日志里报一堆连接拒绝。先把中间件起好、确认healthy,再起业务容器,这个顺序能避免大部分启动问题。
5.3 模型调用相关排查
对话时报model not found,多半是模型名字写得不完全一致。Ollama里拉取的模型名可能是qwen2.5:7b,Dify里配置时也要填一模一样的名字,模型名多一个冒号或少一个tag都会直接失败。
模型调用超时,先确认容器到推理服务的网络到底通不通。进入Dify容器执行:
bash复制docker compose exec api python -c "import requests; print(requests.get('http://192.168.10.5:11434').status_code)"
如果返回200,网络链路正常,问题多半在模型性能或者超时配置;如果连接被拒,那就要检查两边的防火墙、IP地址和端口映射了。
CPU环境下跑7B以上的大模型,速度会非常感人,对话可能等几分钟才有响应。不是Dify卡了,是模型推理本身太慢。这时考虑换小参数模型,或者降低并发请求数,否则用户会以为平台故障了。
5.4 升级与长期维护建议
离线环境升级Dify,本质上还是那套流程:中转机拉新版本镜像、导出、拷贝、导入,然后更新源码配置,执行docker compose up -d。但要注意,跨版本升级往往伴随数据库迁移,迁移不可逆,升级前一定备份PostgreSQL和向量数据库的数据卷。
备份卷最直接的方式:
bash复制docker run --rm -v dify_db_data:/data -v /backup:/backup alpine tar czf /backup/db_data_$(date +%F).tar.gz -C /data .
迁移失败时还能用备份卷回滚。
另外,每次交付或升级,建议把离线包目录维护成一份带版本号和日期的归档,里面至少包含:镜像tar包、源码目录、.env模板(不含敏感信息)、部署脚本、README记录关键配置。再多一句说明都不要写进代码里,全部写进README,包括端口、密码方案、模型地址。这样三个月后有人接手,或者你自己回头维护,都能快速恢复上下文。
最后的小建议
离线部署这件事,真正难的不是技术,而是把所有依赖提前想清楚。在转运机上多花半小时把镜像清单、模型文件、插件包、配置文件全部对齐,现场就能省下熬夜调试的时间。我习惯做完一个离线包后,自己在干净环境里完整走一遍,看看README能不能照着搭起来。如果自己都搭不起来,说明离线包还没做好准备。
另一个很值得做的,是留好内网私有镜像仓库的位置。第一套环境先扛过去,后续扩容或者升级时,有个内网镜像中心会舒服很多,省去每台机器搬运十几个G的tar包。离线部署的最终目标不是“能跑起来”,而是“稳定、可复现、可升级”,把交付物做成一个标准的部署包,这件事才算真正闭环。
