3. 为 Python Flask 应用配置 Gunicorn 生产级镜像

为 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 启动参数的含义

参数取值作用
--bind0.0.0.0:8000监听所有网卡,容器外才能访问
--workers2预派生 2 个 worker 进程处理请求
--threads2每个 worker 内 2 个线程,适合 I/O 等待型接口
--timeout30worker 超过 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 - 生效,日志已接入容器标准输出。
宿主机 curl localhost:8000 端口映射 -p 8000:8000 HTTP 请求入口 :8000 Docker 容器 flask-prod Gunicorn Master (PID 1) --bind 0.0.0.0:8000 · --timeout 30 · 接收 SIGTERM 优雅退出 pre-fork 派生 Worker 1 --threads 2 处理 /health /info 独立进程 · 独立 PID Worker 2 --threads 2 处理 /health /info 独立进程 · 独立 PID Flask 应用 app:app WSGI 可调用对象 · 由 Gunicorn 导入并调用 访问日志 / 错误日志 → stdout / stderr → docker logs 运行用户 appuser (非 root) USER appuser 镜像层顺序 1. python:3.12-slim 2. pip install 依赖 3. COPY app.py 4. CMD gunicorn

常见问题与排查

  • 容器启动后立即退出:用 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。