Flask生产部署指南:Gunicorn+Supervisor+Nginx+Celery实战 在开发环境里跑 Flask 项目python app.py一行命令世界就安静了。但等你真正把项目放到 Linux 服务器上问题会一个个冒出来窗口一关服务是不是就跑了并发一高会不会崩定时任务谁来触发进程挂了谁来拉起来我去年在云服务器上部署一个 Flask 后台项目时用的就是 Gunicorn Supervisor Nginx 这套经典组合再加一个 Celery 处理定时任务把一台配置不算好的小机器收拾得服服帖帖。这篇文章就是从零到上线的完整记录每一段配置背后我会把“为什么这么做”也讲清楚希望帮你少走几个弯路。1. 为什么是这套组合每个组件到底解决什么问题1.1 Flask 自带服务器真的只能“开发用”刚开始学 Flask 的时候大家应该都敲过flask run或者python app.py。Werkzeug 自带的开发服务器确实方便改代码还能自动重载但它有几个非常致命的问题性能不行并发高一点请求就直接排队安全性不行没有经过严格的生产环境考验最要命的是它没有进程守护能力终端窗口一关整个服务就跟着走了。所以生产环境里必须引入真正能扛压力的 WSGI 服务器。WSGI 是 Python Web 应用和服务器之间的标准接口你的 Flask 应用是一个实现了 WSGI 接口的可调用对象服务器负责接收 HTTP 请求把它转成 environ 字典调用你的应用对象再把返回值转成 HTTP 响应。Gunicorn 就是这样一个成熟稳定的 WSGI 服务器。1.2 四个组件各司其职这套组合之所以经典是因为每个组件都只干自己最擅长的事不越权不重复造轮子。我用一个粗糙的比喻来帮你建立直觉组件扮演的角色生活化类比Gunicorn运行 Flask 应用处理 WSGI 请求后厨里专门炒菜的厨师Supervisor守护 Gunicorn / Celery 进程挂了自动拉起来值班经理盯着人不许撂挑子Nginx对外部的唯一入口做反向代理、静态文件服务、负载均衡大堂迎宾所有客人先见它它再带你去后厨Celery处理定时任务和异步任务后厨里设定好时间自动运转的烤箱到点就干活Gunicorn 直接对你的 Flask 应用负责但它自己也可能崩溃所以要用 Supervisor 在系统层面监督它。Nginx 负责暴露在公网把请求转给内网的 Gunicorn同时帮你把静态文件、上传限制、HTTPS 这些事一并办了。Celery 则负责那些“不该让用户等太久”的任务比如发邮件、生成报表、定时清理临时文件。1.3 有别的选择吗对比着看就明白了这套组合不是唯一解但对你我这种普通项目来说是最省心的解。Gunicorn 和 uWSGIuWSGI 性能更强、功能更全但配置复杂度也高一个量级编译安装还容易踩 C 扩展的坑。Gunicorn 是纯 Python 实现pip 装完就能用配置逻辑直白对中小项目绝对够用。Supervisor 和 systemd现在的 Linux 发行版都自带 systemd用它也能做进程守护而且更“原生”。但 systemd 的 unit 文件语法对很多 Python 开发者来说不如 Supervisor 的 INI 配置好懂。另外 Supervisor 可以同时用一行命令方便地查看和管理多个项目进程我至今还是习惯用它。Celery 和 APScheduler如果你只是要一个“每天凌晨跑一次”的定时任务APScheduler 确实更轻。但项目一旦需要异步任务、分布式、任务重试、结果存储Celery 的生态优势就出来了。既然标题都写了 Celery我们就不回头了。2. 动手前的项目结构与环境准备2.1 一个可部署的 Flask 项目长什么样部署不只是写配置项目结构先要清爽。我自己习惯的组织方式大概是这样的以演示项目为例项目名叫flask-demo/home/deploy/flask-demo/ ├── app/ │ ├── __init__.py # 应用工厂创建 Flask app │ ├── views/ │ │ └── main.py # 业务路由 │ └── tasks/ │ └── periodic.py # Celery 定时任务 ├── config.py # 配置文件 ├── requirements.txt # 依赖清单 ├── wsgi.py # 入口from app import create_app ├── gunicorn.conf.py # Gunicorn 配置 ├── run_celery.sh # 可选封装 Celery 启动命令 └── static/ # 静态资源注意wsgi.py必须是“模块级”的Gunicorn 要能通过from wsgi import application导入应用对象。我最开始把创建 app 的代码塞在app/__init__.py里Gunicorn 参数传成app:app也行但一旦要用应用工厂就必须有一个独立的 wsgi 入口否则每次都会踩“模块无法导入”的坑。2.2 虚拟环境部署的第一道安全网千万不要把依赖装到系统 Python 里必须为项目建一个独立虚拟环境。我用的是venvcd /home/deploy/flask-demo python3 -m venv venv source venv/bin/activate pip install -r requirements.txtrequirements.txt里至少要有这些核心依赖Flask3.0.3 gunicorn22.0.0 celery5.4.0 redis5.0.4 supervisor4.2.5Supervisor 其实全局装更好因为它是系统级工具和项目虚拟环境无关。但如果你图省事也可以装在项目虚拟环境里后面启动时用绝对路径指向它就行。我一般直接pip install supervisor在系统里这样supervisord命令在任何路径下都能用。2.3 准备一个能跑的 Flask 示例下面是最小可运行版本。wsgi.py内容# wsgi.py from app import create_app application create_app() if __name__ __main__: application.run()app/__init__.py里写一个简单的应用工厂# app/__init__.py from flask import Flask def create_app(): app Flask(__name__) app.config.from_pyfile(../config.py) app.route(/) def index(): return Hello, Flask! return appconfig.py里留一个 Redis 配置的位置后面 Celery 要用# config.py import os SECRET_KEY os.environ.get(SECRET_KEY, dev-secret) CELERY_BROKER_URL os.environ.get(CELERY_BROKER_URL, redis://127.0.0.1:6379/0) CELERY_RESULT_BACKEND os.environ.get(CELERY_RESULT_BACKEND, redis://127.0.0.1:6379/1)到这里手动跑python wsgi.py能通部署的第一步就准备好了。3. GunicornWSGI 服务器的选型与配置细节3.1 为什么不用 uWSGI 或单纯靠 Nginx有人可能会问Nginx 本身能不能直接跑 Python不行Nginx 对 WSGI 协议支持有限通常只做反向代理和静态资源。真正跟你 Flask 代码打交道的必须是一个 WSGI 服务器。Gunicorn 的核心能力就是启动多个 worker 进程每个 worker 独立处理请求它们共享同一个监听端口由操作系统做负载分发。uWSGI 我在几个项目里也用过性能非常猛但它配置项多到让人头大而且经常要编译安装依赖一堆 C 语言库。对于并发量在几百到几千这个量级的 Flask 项目Gunicorn 的收益够高心智负担低很多。3.2 先跑通命令行再谈配置先手动验证 Gunicorn 能不能把项目跑起来cd /home/deploy/flask-demo source venv/bin/activate gunicorn --workers 4 --bind 127.0.0.1:8000 wsgi:application这里wsgi:application的意思就是“从 wsgi.py 模块里导入名为 application 的对象”。如果你启动时报ModuleNotFoundError十有八九是当前路径不对或虚拟环境没激活。Gunicorn 自己没有“自动定位项目目录”能力必须用cd先切到项目根目录或者用后面的 Supervisor 配置里的directory把工作目录固定住。跑通之后浏览器访问http://服务器IP:8000能看到文字说明已经成功。注意这个 bind 地址我用的是127.0.0.1不是0.0.0.0因为我的架构里 Nginx 和 Gunicorn 在同一台机器上Nginx 通过内网访问它没必要把端口直接暴露到公网。如果 Nginx 在另一台机器bind 地址就要改成内网 IP。3.3 worker 类型sync、gthread 还是 geventworker 类型是 Gunicorn 最重要的一个选择。默认是 sync——一个 worker 同时只能处理一个请求如果代码里有 IO 等待比如查数据库、调第三方 API、读 redis其他请求就得排队。这对大多数 API 服务来说并不是最优解。我一般按这个原则选项目以 IO 密集型为主比如大量查库、调接口用gevent或gthread最有优势项目是简单计算任务或者就是返回几个页面sync 也行用了 gevent 一定要确保代码库里没有会阻塞事件循环的同步库否则反而更慢。gevent需要额外安装pip install gevent然后在 Gunicorn 配置里指定worker_class gevent worker_connections 1000worker_connections表示每个 worker 能同时处理的连接数。我的经验值是 1000如果服务器内存不大调成 500 更稳妥。3.4 把 Gunicorn 配置写进文件命令行参数虽然方便但你不希望每次重启都敲一长串参数。我习惯写一个gunicorn.conf.py# gunicorn.conf.py import multiprocessing bind 127.0.0.1:8000 # 官方的通用公式CPU 核数 × 2 1 workers multiprocessing.cpu_count() * 2 1 worker_class gevent worker_connections 1000 timeout 30 graceful_timeout 30 keepalive 5 # 防止单个 worker 内存无限涨 max_requests 1000 max_requests_jitter 100 daemon False几个参数说明一下workers不是越多越好每个 worker 都是一个独立进程都会占内存。机器是 2 核的话2*215个 worker 已经很多了内存小的机器建议固定写 2 或 3。timeout如果某个接口跑太久没返回Gunicorn 会把它杀掉。注意这不适用于 gevent 的长时间挂起连接但能兜底一些死锁请求。max_requests和max_requests_jitterworker 处理到 1000 个请求左右就会自行重启有效缓解 Python 进程内存缓慢增长的问题。jitter 是随机抖动避免所有 worker 同一时刻一起重启造成服务抖动。4. Supervisor让 Gunicorn 和 Celery 变成“杀不死”的后台服务4.1 先想清楚到底让 Supervisor 管哪些进程部署系统里Supervisor 要管的进程至少有三个Gunicorn 的 worker通常 master 进程会派生多个 worker、Celery 的worker服务、Celery 的beat调度器。注意 Celery 的 worker 和 beat 是两个不同进程很多人只启动了 worker忘了 beat导致定时任务压根不触发这是最经典的失误。我再强调一次celery worker负责执行任务celery beat负责按计划发送任务指令。生产环境里两者需要同时运行。4.2 安装和基础配置先安装 Supervisor我用的是 Debian/Ubuntu 系命令sudo apt update sudo apt install -y supervisor安装完后配置文件在/etc/supervisor/其中/etc/supervisor/conf.d/是放自定义进程配置的地方。主配置文件/etc/supervisor/supervisord.conf最后一行通常会有[include] files /etc/supervisor/conf.d/*.conf没有的话请手动补上。然后在/etc/supervisor/conf.d/下新建项目配置文件我命名为flask-demo.conf[program:flask-demo] command/home/deploy/flask-demo/venv/bin/gunicorn -c /home/deploy/flask-demo/gunicorn.conf.py wsgi:application directory/home/deploy/flask-demo userdeploy autostarttrue autorestarttrue startsecs5 stderr_logfile/var/log/supervisor/flask-demo.err.log stdout_logfile/var/log/supervisor/flask-demo.out.log loglevelinfo字段含义command必填必须用绝对路径最好直接指向虚拟环境里的 gunicorn 可执行文件不要图省事只写gunicorn。directory工作目录Gunicorn 会在该目录下启动避免找不到wsgi.py。user以哪个系统用户运行。建议用一个普通账号比如deploy别用 root否则项目文件权限一旦混乱后面静态文件、日志都会出问题。autostartSupervisor 启动时就拉起这个进程。autorestart进程意外挂了自动重启。startsecs进程启动后要稳定存活多少秒才算“启动成功”否则会判定启动失败并触发重启。Celery 的配置也写在同一个文件里再添加两个 program 段[program:flask-demo-celery] command/home/deploy/flask-demo/venv/bin/celery -A app.tasks.periodic worker --loglevelinfo directory/home/deploy/flask-demo userdeploy autostarttrue autorestarttrue startsecs10 stderr_logfile/var/log/supervisor/flask-demo-celery.err.log stdout_logfile/var/log/supervisor/flask-demo-celery.out.log [program:flask-demo-beat] command/home/deploy/flask-demo/venv/bin/celery -A app.tasks.periodic beat --loglevelinfo directory/home/deploy/flask-demo userdeploy autostarttrue autorestarttrue startsecs10 stderr_logfile/var/log/supervisor/flask-demo-beat.err.log stdout_logfile/var/log/supervisor/flask-demo-beat.out.log注意-A参数指向的是承载 Celery 实例的模块我们后面会写这个模块这里先留个标记。4.3 用 supervisorctl 管理进程配置写好之后执行sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl statusreread让 Supervisord 重新扫描配置文件目录update会把新增的 program 启动起来。如果没有报错status 输出里应该能看到三行 RUNNING。日常操作命令sudo supervisorctl restart flask-demo sudo supervisorctl restart flask-demo-celery sudo supervisorctl restart flask-demo-beat sudo supervisorctl tail -f flask-demotail -f可以直接看进程日志排查问题比journalctl直观太多。这是我一直喜欢 Supervisor 的原因之一。4.4 设置日志切割防止磁盘被撑爆默认日志会一直无限写下去时间长了磁盘肯定受不了。Supervisor 主配置里有一段[supervisord] logfile/var/log/supervisor/supervisord.log logfile_maxbytes50MB logfile_backups10每个 program 的日志也建议配一下stdout_logfile_maxbytes50MB stdout_logfile_backups10 stderr_logfile_maxbytes50MB stderr_logfile_backups10加了这个之后单个日志文件超过 50MB 会自动轮转保留最近 10 份。我因为忘了配这个吃过一次亏一个 Celery worker 日志半年写了 30GB直接塞满磁盘导致服务全部停摆。5. Nginx反向代理与静态资源分流5.1 为什么前面还要再架一层 NginxGunicorn 已经能提供服务了Supervisor 已经能保活了为什么还要 Nginx因为 Gunicorn 处理 HTTP 请求没问题但让它处理静态文件、大文件上传、HTTPS 证书这些“边缘事务”效率并不高。Nginx 是 C 语言写的对静态文件、连接管理、并发连接的处理能力都远胜 Python 层。另外Nginx 作为唯一暴露在公网的入口可以把这个端口后面到底是哪台机器、哪个端口都藏起来。以后哪怕 Gunicorn 从 8000 端口换到 9000外部客户端也不需要感知。5.2 写一份能用的 server 与 location 配置在/etc/nginx/sites-available/下新建一个文件比如flask-demoserver { listen 80; server_name 你的域名或IP; # 请求体大小限制默认是 1MB上传文件需要调大 client_max_body_size 20m; location / { proxy_pass http://127.0.0.1:8000; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location /static/ { alias /home/deploy/flask-demo/static/; expires 7d; } }几个配置点要注意proxy_pass必须和 Gunicorn 的 bind 地址一致这里都是127.0.0.1:8000。proxy_set_header Host $host一定要有否则 Flask 里request.host得到的会是127.0.0.1:8000你做域名相关逻辑时会踩坑。X-Forwarded-Proto也重要如果你的 Flask 应用用request.is_secure判断是不是 HTTPS 请求缺了这个头就会永远得到 False。/static/用alias而不是root区别在于alias会把 URL 前缀/static/去掉再映射到目录root则会拼接路径。创建完软链接到sites-enabledsudo ln -s /etc/nginx/sites-available/flask-demo /etc/nginx/sites-enabled/flask-demo然后测试配置并重载sudo nginx -t sudo systemctl reload nginxnginx -t是必做的操作语法错了它会直接告诉你哪一行有问题避免了改坏配置导致整个站点挂掉的惨剧。我身边有人改完配置没做语法检查就直接 reload结果 Nginx 起不来全部站点瞬间 502这种事故一次就够记一辈子。5.3 静态文件的分流策略为什么要让 Nginx 直接服务静态文件因为 Flask 处理静态文件时每次请求都要经过 Python 层涉及路由解析、上下文创建、响应封装开销不小。Nginx 直接读文件发回去速度能快一个量级。如果项目里用到了 Flask 的蓝图和url_for(static, filename...)配置location /static/时注意静态文件实际路径别写错。可以用nginx -T查看最终生效的配置来核对。6. Celery 定时任务和 Flask 同住一个屋檐下的注意事项6.1 核心问题Celery 任务拿不到 Flask 应用上下文Celery 的任务函数通常定义在项目模块里但 Celery 进程和 Flask 应用进程是相互独立的——Celery worker 启动时不会运行你的 Flask 应用所以任务函数里直接from flask import current_app大概率得到的是RuntimeError: Working outside of application context。这是很多新手第一次部署 Celery 时必经的拦路虎。解决方案也不是很复杂就是把 Celery 实例的创建方式改造成和 Flask 应用上下文绑定。6.2 延迟初始化方案我采用一个make_celery函数来解决# app/tasks/__init__.py from celery import Celery from flask import Flask def make_celery(app: Flask) - Celery: celery Celery( app.import_name, brokerapp.config.get(CELERY_BROKER_URL, redis://127.0.0.1:6379/0), backendapp.config.get(CELERY_RESULT_BACKEND, redis://127.0.0.1:6379/1), ) celery.conf.update( timezoneAsia/Shanghai, enable_utcTrue, task_serializerjson, result_serializerjson, accept_content[json], ) class ContextTask(celery.Task): def __call__(self, *args, **kwargs): with app.app_context(): return self.run(*args, **kwargs) celery.Task ContextTask return celery关键是celery.Task ContextTask每个任务在执行时都会自动包一层app.app_context()这样任务函数内部就能使用current_app、db这些 Flask 上下文相关的对象了。在应用工厂里创建全局的celery实例# app/__init__.py from flask import Flask from app.tasks import make_celery celery None def create_app(): app Flask(__name__) app.config.from_pyfile(../config.py) global celery celery make_celery(app) return app这里还涉及一个重要问题如果整个项目有多个 Flask 进程每个进程都创建自己的 Celery 实例是没问题的因为 broker 和 backend 都在 Redis多个进程共享同一个队列互不冲突。6.3 定时任务的几个典型配置Celery 的定时任务由beat进程负责调度配置文件里设置beat_schedule。我现在经常挂在任务模块里的配置长这样# app/tasks/periodic.py from celery.schedules import crontab from app import celery celery.task def clean_expired_file(): # 实际业务逻辑清理过期临时文件 return cleaned celery.on_after_configure.connect def setup_periodic_tasks(sender, **kwargs): sender.add_periodic_task( crontab(hour2, minute0), clean_expired_file.s(), nameclean-expired-file-daily, )crontab(hour2, minute0)表示每天凌晨 2 点执行。Celery 的 crontab 支持day_of_month、month_of_year等字段基本能覆盖你见过的所有 cron 表达式。如果只是要“每 10 分钟跑一次”就写成sender.add_periodic_task( 600.0, clean_expired_file.s(), nameclean-expired-file-10min, )注意任务函数里一旦涉及 Django orm、Flask-SQLAlchemy 这类需要应用上下文的东西就必须走我们刚才的ContextTask否则会直接报错。6.4 定时任务部署后最容易犯的错忘记启动 beat只启动了celery worker定时任务永远不触发。Supervisor 的配置里必须单独管理 beat 进程。EUR 时区和本地时区搞混如果enable_utcTrue但timezone没设置定时任务会按 UTC 时间执行凌晨 2 点实际上是北京时间上午 10 点。任务里传 ORM 对象Celery 序列化的是任务参数如果你把一个 SQLAlchemy 模型实例传进去反序列化时会因为数据库 session 已经关闭而报各种诡异错误。正确做法是传对象 ID在任务内部重新查询。任务日志不落盘Celery 默认日志输出到 stdout如果 Supervisor 没重定向日志会丢失。在 command 里加--logfile/var/log/supervisor/celery.log会更稳。7. 部署完成后的验证流程与几个真实的坑7.1 上线前的九步验证法配置全部写完后别急着说大功告成。我每次部署新环境都会走一遍这个验证清单# 1. 检查 Supervisor 是否正常管理三个进程 sudo supervisorctl status # 2. 看日志有没有错误 sudo supervisorctl tail -f flask-demo sudo supervisorctl tail -f flask-demo-celery sudo supervisorctl tail -f flask-demo-beat # 3. 直接用 curl 打 Gunicorn 端口 curl -I http://127.0.0.1:8000/ # 4. 再通过 Nginx 入口打一遍 curl -I http://127.0.0.1/ # 5. 检查 Nginx 错误日志 sudo tail -f /var/log/nginx/error.log # 6. 检查 Celery 任务有没有注册成功 celery -A app.tasks.periodic inspect registered # 7. 手动触发一次任务看 worker 日志 celery -A app.tasks.periodic call app.tasks.periodic.clean_expired_file --args[] # 8. 等一会儿看 beat 日志确认定时任务真的触发了 sudo tail -f /var/log/supervisor/flask-demo-beat.out.log # 9. 用外部机器访问公网 IP确认 Nginx 入口对外开放正常第 7 步手动触发任务是我最推荐的调试手段。很多定时任务问题你以为“写对了”其实任务函数里有一行语法错误或者导入错误手动触发能第一时间暴露出来比干等 beat 到点再报错高效太多。7.2 权限问题deploy 用户和 Nginx 用户的恩怨部署完最常见的表象是页面访问 502、静态文件 403。绝大多数时候是权限问题。Gunicorn 进程是deploy用户跑的静态文件目录如果权限是root读写Nginx worker 读不了就会 403如果 Gunicorn 的工作目录里的临时文件、日志路径没有写权限进程直接起不来。我的习惯是sudo chown -R deploy:deploy /home/deploy/flask-demo sudo chmod -R 755 /home/deploy/flask-demo如果用了 Nginx 直接服务静态文件还要保证deploy用户至少对静态文件目录有读权限Nginx worker 用户也能沿路径访问到。遇到 403 用namei -om /home/deploy/flask-demo/static/xxx.png查看路径上每一级目录的权限是排查权限问题最快的命令。7.3 内存管理与 max_requests 的真实收益我的项目启动时每个 Gunicorn worker 占用内存大概 150MB5 个 worker 就是 750MB加上 Celery worker、beat、Redis、Nginx2G RAM 的小机器已经开始紧张。时间一长Python 进程内部会有碎内存堆积典型表现是内存占用只涨不降。max_requests的意义就在这里——让每个 worker 处理固定数量的请求后自行退出由 Gunicorn 重新拉起一个全新的 worker。这相当于给进程装了个自动重启机制比任何内存监控脚本都简单有效。把max_requests绑上去之后我就再没做过定时重启 Gunicorn 的操作。7.4 时区与日志两个看似不起眼却总出事的点时区问题在 Celery 定时任务里特别隐蔽。你写crontab(hour2)自以为自己每天凌晨 2 点跑结果全公司都在上午 10 点看到定时任务日志才出现。检查方法很简单在 Celery worker 日志里看任务触发时打出的时间戳以及celery.timezone的实际值。日志方面我强烈建议把 Supervisor 和 Celery 的日志级别都调到 INFO这样能保留完整的任务执行轨迹。如果你的任务执行失败日志里会有Task xuixxx raised的 stacktrace这是排查问题最关键的线索。我见过有人连 loglevel 都省了任务挂了只能一脸懵。部署这套组合我第一次走完全程用了一天半踩了差不多七八个坑。后来再部署类似的 Flask 项目半个小时就能把环境拉起。这套组合最舒服的地方在于每个部分都“信得过”Gunicorn 负责跑Supervisor 负责管Nginx 负责扛流量入口Celery 负责定时干活结构清晰也方便以后横向扩展。最后再分享一个小习惯每次改完任何配置都先在测试环境完整走一遍验证流程别直接上生产操作。服务器部署这件事稳定永远比炫技重要。