3. 为 Python Flask 应用配置 Gunicorn 生产级镜像
为什么 Flask 开发服务器不能直接上生产
Flask 自带的 app.run() 是单进程、单线程的开发服务器,它没有进程管理、没有优雅重启、没有并发连接池,官方文档明确说明它不适合生产环境。生产级部署需要 WSGI 服务器来接管 HTTP 请求,Gunicorn(Green Unicorn)是 Python 生态中最常用的选择之一:它用预派生(pre-fork)模型管理多个 worker 进程,支持超时控制、优雅重启和并发处理。
本例的目标是:把一个 Flask 应用打包成镜像,在容器内用 Gunicorn 以生产模式启动,并通过 HTTP 请求验证服务确实由 Gunicorn 提供。
项目结构与依赖文件
先准备一个最小但完整的 Flask 项目。目录结构如下:
flask-gunicorn-demo/
├── app.py
├── requirements.txt
└── Dockerfile
app.py 提供一个健康检查接口和一个返回运行信息的接口,便于验证请求确实经过了 Gunicorn:
# app.py
import os
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/health")
def health():
return jsonify(status="ok")
@app.route("/info")
def info():
return jsonify(
app="flask-gunicorn-demo",
pid=os.getpid(),
workers=os.environ.get("WEB_CONCURRENCY", "1"),
)
if __name__ == "__main__":
# 仅用于本地开发调试,生产环境由 Gunicorn 启动
app.run(host="0.0.0.0", port=5000)
requirements.txt 固定版本,避免构建时依赖漂移:
Flask==3.0.3
gunicorn==22.0.0
编写生产级 Dockerfile
关键点有四个:使用精简基础镜像、先装依赖再拷代码以利用层缓存、用非 root 用户运行、用 exec 形式的 CMD 让 Gunicorn 成为 PID 1 直接接收信号。
# Dockerfile
FROM python:3.12-slim
# 1. 环境变量:不写 .pyc、日志不缓冲、pip 不缓存
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /app
# 2. 先复制依赖清单并安装,代码变更不会导致重装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 3. 再复制应用代码
COPY app.py .
# 4. 创建非 root 用户并切换
RUN useradd --create-home --shell /bin/bash appuser
USER appuser
EXPOSE 8000
# 5. Gunicorn 生产启动命令
CMD ["gunicorn", \
"--bind", "0.0.0.0:8000", \
"--workers", "2", \
"--threads", "2", \
"--timeout", "30", \
"--access-logfile", "-", \
"--error-logfile", "-", \
"app:app"]
CMD 使用 JSON 数组(exec 形式),Gunicorn 直接成为容器 PID 1,能正确接收
docker stop 发出的 SIGTERM 并优雅退出;若写成 shell 形式,信号会被 shell 拦截,导致停止时等待超时。Gunicorn 启动参数的含义
| 参数 | 取值 | 作用 |
|---|---|---|
--bind | 0.0.0.0:8000 | 监听所有网卡,容器外才能访问 |
--workers | 2 | 预派生 2 个 worker 进程处理请求 |
--threads | 2 | 每个 worker 内 2 个线程,适合 I/O 等待型接口 |
--timeout | 30 | worker 超过 30 秒无响应则被重启 |
--access-logfile | - | 访问日志输出到标准输出,可被 docker logs 采集 |
--error-logfile | - | 错误日志输出到标准错误 |
worker 数量不是越多越好。经验公式是 (2 × CPU 核数) + 1,但容器通常有 CPU 限制,本例用 2 个 worker 便于观察多进程行为。生产环境可通过环境变量 WEB_CONCURRENCY 覆盖,Gunicorn 会自动读取该变量。
构建镜像并验证生产模式
构建镜像:
docker build -t flask-gunicorn-demo:1.0 .
启动容器,把容器 8000 端口映射到宿主机 8000:
docker run -d --name flask-prod -p 8000:8000 flask-gunicorn-demo:1.0
验证服务由 Gunicorn 提供,而不是 Flask 开发服务器:
# 1. 健康检查
curl -s http://localhost:8000/health
# 预期输出:{"status":"ok"}
# 2. 查看进程,应看到 gunicorn 主进程和 worker 进程
docker exec flask-prod ps -ef
# 预期可见:gunicorn: master [app:app] 以及 gunicorn: worker [app:app]
# 3. 多次请求 /info,观察 pid 变化,证明多 worker 在轮询处理
for i in 1 2 3 4; do curl -s http://localhost:8000/info; echo; done
# 预期输出中 pid 会在两个不同值之间交替
# 4. 查看访问日志,确认请求被 Gunicorn 记录
docker logs flask-prod
如果
curl 返回连接被拒绝,先检查容器是否在运行(docker ps),再检查端口映射是否正确。如果 docker logs 里出现 ModuleNotFoundError,说明 app.py 没有复制进镜像或工作目录不对。验证结果判读
/health返回{"status":"ok"}:应用已正常加载,Gunicorn 成功导入app:app。ps -ef中出现gunicorn: master和至少一个gunicorn: worker:确认是 Gunicorn 而非flask run在提供服务。- 连续请求
/info时pid交替变化:证明多个 worker 进程都在接收请求,负载被分散。 docker logs中有访问日志行:说明--access-logfile -生效,日志已接入容器标准输出。
常见问题与排查
- 容器启动后立即退出:用
docker logs flask-prod查看 Gunicorn 报错。最常见的是app:app模块路径写错,或app.py未复制进镜像。 - 宿主机访问不到:确认
--bind 0.0.0.0:8000而不是127.0.0.1:8000,后者只监听容器内部回环地址。 - worker 频繁重启:查看日志中是否有
WORKER TIMEOUT,说明接口处理超过 30 秒,需要优化代码或调大--timeout。 - 停止容器很慢:检查 CMD 是否为 exec 形式。shell 形式下 SIGTERM 发给 shell 而非 Gunicorn,容器会等到超时被 SIGKILL。