环境搭建与启动:让地图上的城市"通电"
昨天画了地图,今天让它跑起来。我们盯住三个文件——api/app.py(进程入口)、api/app_factory.py(应用工厂)、api/dify_app.py(Flask 子类)——外加 docker/ 目录,弄清三件事:①进程从哪一行开始跑;②Flask app 是怎么被"工厂"造出来、再装上 20 多个扩展的;③同一份代码怎么分身成 api / worker / beat 三种角色。看懂这条启动链,后面任何"这功能在哪初始化"的疑问都能自己回答。
MODE 决定今天上哪个班。痛点:这么多文件,程序到底从哪一行开始跑
api/ 下面有 app.py、app_factory.py、dify_app.py、celery_entrypoint.py……名字都很像"入口"。到底哪个是真正被执行的第一行?为什么要分这么多文件?如果搞不清启动链,你连"在哪打断点"都不知道,更别说改代码验证了。app.py(决定造哪种 app)→ app_factory.create_app()(造裸 app)→ initialize_extensions()(按顺序装扩展)→ 由 docker/entrypoint.sh 根据 MODE 决定这个进程当 api 还是 worker。今天就顺着这条链一步步看真源码。app.py 是"决定今天开哪种店",工厂函数是"把桌椅厨具摆好",扩展加载是"通水通电通网",最后 MODE 决定"这个人今天是服务员还是厨师"。缺任何一步,店都开不了。app.py:真正被执行的第一行
进程入口是 api/app.py。它在模块顶层(不是在函数里)就决定了造哪种 app(api/app.py:36):
# api/app.py:36
if is_db_command(): # ① 如果是在跑 `flask db ...` 迁移命令
from app_factory import create_migrations_app
app = create_migrations_app() # 只装 db/migrate/commands 三个扩展的精简 app
socketio_app = app
flask_app = app
else:
from app_factory import create_app # ② 正常情况:造完整 app
socketio_app, flask_app = create_app()
app = flask_app
celery = cast("Celery", app.extensions["celery"])
is_db_command()判断你是不是在执行数据库迁移命令(api/app.py:18 检查 sys.argv 是否是 flask db ...)。迁移只需要数据库相关扩展,不用把整套东西都装上,省时省事。create_migrations_app()分支①:精简 app,只装 ext_database / ext_migrate / ext_commands(L04 会对比)。create_app()分支②:造完整 app,返回一个二元组 (socketio_app, flask_app)——因为 Dify 用了 WebSocket(socket.io),所以真正对外服务的是包了一层 socketio 的 app。celery = app.extensions["celery"]从装好的扩展里把 celery 实例取出来挂到模块变量,方便 celery -A app.celery ... 这种命令找到它。那"真正 serve"发生在哪?在 if __name__ == "__main__" 里(api/app.py:56),用 gevent 的 WSGI 服务器起在 0.0.0.0:5001:
# api/app.py:56
if __name__ == "__main__":
from gevent import pywsgi
from geventwebsocket.handler import WebSocketHandler
log_startup_banner(HOST, PORT) # HOST=0.0.0.0 PORT=5001
server = pywsgi.WSGIServer((HOST, PORT), socketio_app, handler_class=WebSocketHandler)
server.serve_forever()
create_app() 写在模块顶层(一被 import 就执行),而起服务器写在 __main__ 里(只有 python -m app 直接运行才执行)。这是为了让 gunicorn/celery 只 import 拿到 app 对象,自己决定怎么 serve,而本地开发用 python -m app 时才用内置 gevent 服务器。同一个文件,两种用法。DifyApp 到底是什么?就是 Flask 的一个子类(api/dify_app.py:11),额外声明了一个 login_manager 属性,方便类型检查。它本质就是一个 Flask app,别被名字唬住。应用工厂:create_app 怎么把 app 造出来
核心在 api/app_factory.py。先看"造裸 app"(api/app_factory.py:49):
# api/app_factory.py:49
def create_flask_app_with_configs() -> DifyApp:
"""create a raw flask app with configs loaded from .env file"""
dify_app = DifyApp(__name__)
dify_app.config.from_mapping(dify_config.model_dump()) # ① 把配置灌进 app.config
dify_app.config["RESTX_INCLUDE_ALL_MODELS"] = True
@dify_app.before_request
def before_request():
init_request_context() # ② 每个请求前:初始化日志上下文
RecyclableContextVar.increment_thread_recycles()
... # 企业版 license 校验
return dify_app
再看"装配"(api/app_factory.py:127)——这就是工厂主函数:
# api/app_factory.py:127
def create_app() -> tuple[socketio.WSGIApp, DifyApp]:
start_time = time.perf_counter()
app = create_flask_app_with_configs() # ① 先造裸 app
initialize_extensions(app) # ② 沿流水线装上所有扩展
sio.app = app
socketio_app = socketio.WSGIApp(sio, app) # ③ 再包一层 socket.io(支持 WebSocket)
...
return socketio_app, app # ④ 返回二元组,正好对上 app.py:52 的解包
dify_config.model_dump()dify_config 是从 .env 读出来的配置对象(Pydantic)。model_dump() 把它转成字典灌进 app.config——所以你在代码里能到处 dify_config.XXX 拿配置。@before_request注册"每个请求进来前先跑一遍"的钩子:初始化日志上下文、(企业版)校验 license。这也解释了为什么每个请求都能带上 trace 信息。initialize_extensions(app)★工厂的核心动作:把 db、redis、celery、login 等 20 多个扩展依次 init_app(app)。L04 细看顺序。socketio.WSGIApp(sio, app)在 Flask app 外面再套一层 socket.io,得到最终对外服务的 socketio_app——这就是为什么 app.py 拿到的是 (socketio_app, flask_app) 两个。app = Flask(__name__),全项目 import 这一个全局 app。问题是:测试时你想要不同配置的 app(比如内存数据库)、迁移时想要精简 app——全局单例做不到。工厂模式把"造 app"变成一个可调用的函数,你想造几个、造什么配置的都行(看 create_migrations_app 就是另一种组装)。代价是多一层函数、import 时机要小心;收益是可测试、可多环境、可按需精简。这是 Flask 官方推荐的大型项目结构。扩展加载:20 多个 ext_ 按顺序上流水线
initialize_extensions 里有一个顺序列表(api/app_factory.py:178),扩展是按这个顺序一个个装的:
# api/app_factory.py:178
extensions = [
ext_timezone, ext_logging, ext_warnings, ext_import_modules, ext_orjson,
ext_forward_refs, ext_compress, ext_code_based_extension,
ext_database, # ← 数据库(很多后续扩展依赖它,所以排在前面)
ext_app_metrics, ext_migrate, ext_redis, ext_storage, ext_set_secretkey,
ext_logstore, # 注释明说:storage 之后、celery 之前
ext_celery, # ← Celery(异步任务)
ext_login, ext_mail, ext_hosting_provider, ext_sentry, ext_proxy_fix,
ext_blueprints, # ← 注册路由蓝图(controllers 在这里挂上)
ext_commands, ext_fastopenapi, ext_otel, ext_enterprise_telemetry,
ext_request_logging, ext_session_factory, ext_oauth_bearer,
]
for ext in extensions: # ← 逐个装
is_enabled = ext.is_enabled() if hasattr(ext, "is_enabled") else True
if not is_enabled:
continue # 没开启的跳过
ext.init_app(app) # ★把扩展挂到 app 上
顺序有讲究★不是随便排的:ext_database 排在很前,因为 redis、celery、login 等都依赖数据库;ext_logstore 的注释直接写明"在 storage 之后、celery 之前"。装配顺序 = 依赖顺序。ext_blueprints这一步才把 controllers/ 里的路由真正注册进 app。所以你的接口能被访问,全靠这个扩展——它排在数据库/redis 之后,保证路由处理时依赖都就绪。ext.is_enabled()每个扩展可以声明"我这次要不要装"(比如 sentry 没配就不装)。没开的直接 continue 跳过——这让同一份代码能适应"最小部署"到"全家桶"各种环境。ext.init_app(app)统一约定:每个 ext_* 模块都提供一个 init_app(app),把自己(db 连接/redis 客户端/celery 实例…)挂到传入的 app 上。这就是 Flask 扩展的标准姿势。对比"精简 app"(迁移用,api/app_factory.py:224),你会更懂"按需组装"的意义:
# api/app_factory.py:224
def create_migrations_app() -> DifyApp:
app = create_flask_app_with_configs()
from extensions import ext_commands, ext_database, ext_migrate
ext_database.init_app(app) # 只装这三个
ext_migrate.init_app(app)
ext_commands.init_app(app)
return app
docker:同一份代码分身成 api / worker / beat
部署编排在 docker/(docker-compose.yaml、docker-compose.middleware.yaml 等)。而"这个容器当什么角色",由 api/docker/entrypoint.sh 根据环境变量 MODE 决定:
# api/docker/entrypoint.sh
if [[ "${MIGRATION_ENABLED}" == "true" ]]; then # :11 先跑迁移(可选)
flask upgrade-db
fi
if [[ "${MODE}" == "worker" ]]; then # :21 角色①:Celery 后厨
exec celery -A celery_entrypoint.celery worker -P ${WORKER_POOL} ... # :66
elif [[ "${MODE}" == "beat" ]]; then # :71 角色②:定时闹钟
exec celery -A app.celery beat --loglevel ... # :72
elif [[ "${MODE}" == "job" ]]; then # :74 角色③:一次性命令
flask "$@"
else # :120 角色④:默认=api 前台
if [[ "${DEBUG}" == "true" ]]; then
exec python -m app # :124 开发:内置 gevent 服务器
else
exec gunicorn ... --bind "0.0.0.0:5001" \ # :126 生产:gunicorn
--worker-class geventwebsocket...GeventWebSocketWorker
fi
fi
MODE=worker启动 Celery worker(api/docker/entrypoint.sh:66),专门消费队列里的异步任务:文档索引、发邮件、删应用等。用 gevent 池并发。MODE=beat启动 Celery beat(api/docker/entrypoint.sh:72),像闹钟一样按计划把定时任务丢进队列,再由 worker 执行。MODE=job一次性运行某个 flask 命令后退出(api/docker/entrypoint.sh:74),比如 K8s 里 create-tenant 建租户。默认(api)不是上面任何一种就当 api 服务(api/docker/entrypoint.sh:120):DEBUG 时 python -m app(回到 L02 的 __main__!),生产用 gunicorn 起 5001 端口。api 容器(MODE 默认,跑 gunicorn:5001)、worker 容器(MODE=worker,跑 celery)、web 容器(Next.js:3000)、加上中间件 db(postgres) / redis / 向量库 等。同一个 dify-api 镜像,靠 MODE 环境变量分身成 api 和 worker 两个容器——这就是"换工牌上不同班"。MODE=worker 的进程执行,不在 api 进程里。api 好好的不代表 worker 好好的。记住这条分工,排查快一半。跑起来 + 今日小结
👶 小白:我本地调试,到底该 python -m app 还是 gunicorn?在哪打断点?
👨🏫 老师:本地调试用 python -m app(对应 DEBUG=true 那条分支),它走 app.py:56 的 __main__,单进程好断点。gunicorn 是生产用的多进程,不好调。断点首选打在 controllers/ 的路由函数和 core/ 的业务函数上;想看启动流程就打在 app_factory.create_app。异步任务(索引等)要调,得单独起一个 MODE=worker 的 celery 进程并在 tasks/ 里打断点——它不在 api 进程里。
🧠 今天你应该能回答
- 进程真正的入口文件是哪个?(
api/app.py) create_app()为什么返回两个值?(socketio_app+flask_app,因为套了 socket.io)- 为什么用"应用工厂"而不是全局
app = Flask()?(可测试、可多环境、可按需精简,如迁移专用 app) - 扩展加载列表的顺序有意义吗?(有,是依赖顺序:db 先于 redis/celery/login)
- 路由是在哪一步挂上 app 的?(
ext_blueprints) - 同一份代码怎么分身成 api / worker / beat?(
entrypoint.sh看MODE环境变量) - 上传文档后卡在"索引中",先查什么?(worker 容器,因为索引是 Celery 异步任务)
✋ 10 分钟动手
cd /Users/bitmart/work/codes/github/AI_WORK/dify
# 1. 顺着启动链读三个文件
sed -n '32,62p' api/app.py # 入口:造哪种 app + 起服务
sed -n '127,139p' api/app_factory.py # 工厂主函数 create_app
sed -n '178,222p' api/app_factory.py # 扩展加载顺序 + 循环装配
# 2. 看角色分发
sed -n '11,74p' api/docker/entrypoint.sh # MODE=worker/beat/job/默认 四条分支
# 3. 数一数装了多少扩展
grep -c "ext_" api/app_factory.py
core/app/apps/,看 Dify 的五种应用类型——chat / agent-chat / completion / workflow / advanced-chat——各自的目录结构,以及它们共享的 AppMode 枚举和 Generator/Runner 骨架。