083、Flask 生产部署:Gunicorn、Nginx 反向代理与 Docker 容器化 083、Flask 生产部署Gunicorn、Nginx 反向代理与 Docker 容器化从一次线上事故说起上周五晚上十一点我正躺在床上刷手机突然收到报警群消息生产环境 Flask 服务响应超时用户反馈页面加载需要 30 秒以上。我立刻爬起来连上服务器ps aux一看Flask 开发服务器还在跑着——没错就是那个flask run启动的单进程服务。同事图省事直接把开发环境部署到了生产结果并发一上来请求排队CPU 打满服务直接瘫痪。这个坑我踩过不止一次。Flask 自带的 Werkzeug 服务器是单进程单线程的开发调试没问题但生产环境必须用 WSGI 服务器来承载并发。今天这篇笔记我就把生产部署的完整链路讲清楚Gunicorn 做 WSGI 服务器、Nginx 做反向代理、Docker 做容器化。每一步都有血泪教训别走我的老路。Gunicorn给 Flask 装上多进程引擎先解决最核心的问题Flask 本身不能直接处理高并发。我们需要一个 WSGI 服务器来管理 worker 进程。Gunicorn 是 Python 生态里最成熟的选择配置简单性能稳定。安装与基础启动pipinstallgunicorn假设你的 Flask 应用入口文件是app.py里面有个app Flask(__name__)启动命令长这样gunicorn-w4-b0.0.0.0:8000 app:app-w 4表示启动 4 个 worker 进程-b绑定地址和端口。这里有个关键点app:app第一个app是模块名文件名去掉.py第二个app是 Flask 实例变量名。别写反了我见过有人写成app:app但文件名是main.py结果报ModuleNotFoundError。Worker 类型的选择Gunicorn 默认用同步 worker每个 worker 一次只能处理一个请求。如果你的 Flask 视图函数里有 I/O 操作比如查数据库、调外部 API同步 worker 会阻塞浪费 CPU。这时候需要换异步 worker。gunicorn-w4-kgevent-b0.0.0.0:8000 app:app-k gevent使用 gevent 协程 worker每个 worker 可以同时处理多个请求。但注意gevent 需要安装pip install gevent而且你的代码里不能有阻塞式的同步调用比如time.sleep否则协程会失效。这里踩过坑有个同事在视图里用了requests.get没加超时结果 gevent 也救不了请求全卡住。另一个选择是uvicorn如果你用 ASGI 框架比如 FastAPI但 Flask 是 WSGI所以 Gunicorn 更合适。生产配置建议别在命令行里写死参数用配置文件更靠谱。新建gunicorn.conf.py# gunicorn.conf.pyimportmultiprocessing bind0.0.0.0:8000workersmultiprocessing.cpu_count()*21# 经典公式别问为什么经验值worker_classgeventtimeout30# 超时时间单位秒别设太大否则慢请求拖死所有workerkeepalive5# 长连接保持时间accesslog/var/log/gunicorn/access.logerrorlog/var/log/gunicorn/error.logloglevelinfo启动时直接指定配置文件gunicorn-cgunicorn.conf.py app:appworkers数量不是越多越好。CPU 核心数 * 2 1 是经典公式但如果你用 gevent每个 worker 内部能处理大量并发worker 数量可以少一些比如 2-4 个。我一般 4 核机器用 4 个 gevent worker每个 worker 配 1000 个协程足够应对日常流量。Nginx 反向代理给服务加一层铠甲Gunicorn 直接暴露在公网上是不安全的。Nginx 作为反向代理可以处理静态文件、负载均衡、SSL 终止、限流等。Flask 只负责动态逻辑静态资源交给 Nginx。基础配置安装 Nginx 后在/etc/nginx/sites-available/下新建配置文件比如myflaskserver { listen 80; server_name yourdomain.com; # 静态文件直接由 Nginx 处理别让 Flask 管 location /static/ { alias /path/to/your/static/; expires 30d; # 缓存30天减少请求 } # 动态请求转发给 Gunicorn location / { proxy_pass http://127.0.0.1:8000; # Gunicorn 监听本地端口 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; } }这里有个容易忽略的点proxy_set_header Host $host必须写否则 Flask 拿不到真实的 Host 头生成 URL 时会出错。比如用户访问https://example.com但 Flask 看到的是127.0.0.1:8000生成的链接就变成内网地址了。为什么需要 Nginx有人问Gunicorn 直接监听公网端口不行吗技术上可以但有几个问题安全Gunicorn 没有内置的 DDoS 防护、限流功能Nginx 可以配置limit_req限制请求频率。静态文件Flask 处理静态文件效率低Nginx 用 sendfile 系统调用性能高一个数量级。SSLNginx 处理 HTTPS 证书更成熟Gunicorn 虽然也能配但配置复杂且性能差。负载均衡如果你有多个 Gunicorn 实例Nginx 可以轮询分发请求。踩坑记录502 Bad Gateway部署后最常见的错误是 502。原因通常是 Nginx 连不上 Gunicorn。检查几点Gunicorn 是否在运行ps aux | grep gunicorn端口是否绑定正确netstat -tlnp | grep 8000防火墙是否放行ufw status或iptables -LNginx 配置里的proxy_pass地址是否写对注意http://127.0.0.1:8000末尾不要加/否则路径会变。有一次我排查了半天发现是 Gunicorn 绑定了127.0.0.1:8000但 Nginx 配置里写成了localhost:8000而/etc/hosts里 localhost 被解析成了::1IPv6导致连接失败。改成127.0.0.1就好了。Docker 容器化环境一致性救星部署到生产环境最怕什么环境不一致。开发机上是 Python 3.10服务器是 3.8某个依赖版本不同代码就跑不起来。Docker 把应用和依赖打包成镜像保证任何地方运行结果一致。编写 Dockerfile# 使用官方 Python 镜像别用 alpine坑太多 FROM python:3.10-slim # 设置工作目录 WORKDIR /app # 先复制 requirements.txt利用 Docker 缓存层 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制应用代码 COPY . . # 暴露端口 EXPOSE 8000 # 启动命令用 Gunicorn 而不是 flask run CMD [gunicorn, -c, gunicorn.conf.py, app:app]这里有个优化点COPY requirements.txt .和RUN pip install放在COPY . .之前。因为requirements.txt不常变而代码经常改。这样每次构建时只要requirements.txt没变Docker 会复用缓存的 pip 安装层节省大量时间。使用 docker-compose 编排单容器不够我们还需要 Nginx 和数据库。用docker-compose.yml统一管理version:3.8services:flask:build:.expose:-8000# 只暴露给内部网络不映射到宿主机environment:-FLASK_ENVproduction-DATABASE_URLpostgresql://user:passdb:5432/mydbdepends_on:-dbnginx:image:nginx:alpineports:-80:80-443:443volumes:-./nginx.conf:/etc/nginx/conf.d/default.conf-./static:/staticdepends_on:-flaskdb:image:postgres:13environment:POSTGRES_USER:userPOSTGRES_PASSWORD:passPOSTGRES_DB:mydbvolumes:-pgdata:/var/lib/postgresql/datavolumes:pgdata:注意flask服务没有ports映射只有expose。这意味着 Gunicorn 的 8000 端口只在 Docker 内部网络中可访问Nginx 通过服务名flask:8000来连接。这样更安全外部无法直接访问 Gunicorn。构建与启动docker-composebuilddocker-composeup-d查看日志用docker-compose logs -f。如果某个服务启动失败可以单独重启docker-compose restart flask。踩坑记录容器内 localhost 问题在 Docker 容器里localhost指向容器自身而不是宿主机。如果你的 Flask 代码里连接数据库用了localhost:5432在容器里会连不上因为数据库在另一个容器里。解决方案使用服务名db:5432或者用环境变量配置数据库地址。另一个坑静态文件路径。Nginx 容器里挂载了./static:/static但 Flask 代码里url_for(static, filenamestyle.css)生成的路径是/static/style.cssNginx 配置里location /static/要对应到容器内的/static目录。别写错路径否则 404。性能调优与监控部署完了不代表万事大吉。生产环境需要持续监控。Gunicorn 的优雅重启更新代码后需要重启 Gunicorn 而不中断现有请求。使用HUP信号kill-HUPgunicorn_pidGunicorn 会启动新的 worker等待旧 worker 处理完当前请求后再关闭。如果直接kill -9正在处理的请求会丢失。日志管理Gunicorn 的日志默认输出到 stdout在 Docker 里会被 Docker 日志驱动捕获。建议配置accesslog和errorlog到文件然后用logrotate轮转防止日志撑爆磁盘。Nginx 的日志同样重要。我习惯在 Nginx 配置里加上access_log /var/log/nginx/access.log main;并自定义日志格式记录响应时间、上游地址等方便排查慢请求。健康检查Docker 支持健康检查在docker-compose.yml里添加flask:healthcheck:test:[CMD,curl,-f,http://localhost:8000/health]interval:30stimeout:10sretries:3Flask 里加一个/health端点返回 200 表示服务正常。这样 Docker 可以自动重启不健康的容器。个人经验总结部署这件事没有银弹。Gunicorn Nginx Docker 这套组合拳我用了三年踩过无数坑总结几条经验永远不要在生产用flask run。这不是玩笑我见过不止一个团队因为这个翻车。哪怕流量再小也要用 Gunicorn 或 uWSGI。Docker 镜像要精简。用python:3.10-slim而不是python:3.10能省几百兆。别装不必要的包比如gcc、build-essential除非你的依赖需要编译。日志是救命稻草。生产环境出问题第一件事看日志。Gunicorn 的 error log、Nginx 的 access log、Flask 的 app log三个都要配好。别等到出事了才想起来没开日志。压力测试不能省。部署前用ab或wrk压一下看看 Gunicorn 的 worker 数量是否合理Nginx 的worker_connections是否够用。我一般压到 CPU 使用率 80% 左右看响应时间是否稳定。回滚方案要准备好。Docker 的好处是镜像不可变出问题直接回滚到上一个版本。docker-compose down docker-compose up -d就能恢复。但前提是你有版本管理别用latest标签用具体的版本号比如myapp:v1.2.3。最后说一句部署不是终点是起点。上线后持续观察监控指标调整配置优化代码这才是生产环境的常态。别以为部署完就万事大吉半夜被报警吵醒的滋味一次就够了。