flask | uwsgi生产化部署flask服务

0x00 WSGI简介

WSGI是 Web Server Gateway Interface 的缩写。它是 Python应用程序(application)或框架(如 Django)和 Web服务器之间的一种接口,已经被广泛接受。

它是一种协议,一种规范,其是在 PEP 333提出的,并在 PEP 3333 进行补充(主要是为了支持 Python3.x)。这个协议旨在解决众多 web 框架和web server软件的兼容问题。有了WSGI,你不用再因为你使用的web 框架而去选择特定的 web server软件

常见的web应用框架有:Django,Flask等

常用的web服务器软件有:uWSGI,Gunicorn等

WSGI 接口有服务端和应用端两部分,服务端也可以叫网关端,应用端也叫框架端。服务端调用一个由应用端提供的可调用对象。如何提供这个对象,由服务端决定。例如某些服务器或者网关需要应用的部署者写一段脚本,以创建服务器或者网关的实例,并且为这个实例提供一个应用实例。另一些服务器或者网关则可能使用配置文件或其他方法以指定应用实例应该从哪里导入或获取。

0x01 WSGI服务结构

WSGI 对于 application 对象有如下三点要求

  • 必须是一个可调用的对象
  • 接收两个必选参数environ、start_response。
  • 返回值必须是可迭代对象,用来表示http body。

我根据其架构组成的不同将这个过程的实现分为两种:

1、两级结构

在这种结构里,uWSGI作为服务器,它用到了HTTP协议以及wsgi协议,flask应用作为application,实现了wsgi协议。当有客户端发来请求,uWSGI接受请求,调用flask app得到相应,之后相应给客户端。 这里说一点,通常来说,Flask等web框架会自己附带一个wsgi服务器(这就是flask应用可以直接启动的原因),但是这只是在开发阶段用到的,在生产环境是不够用的,所以用到了uwsgi这个性能高的wsgi服务器。

2、三级结构

这种结构里,uWSGI作为中间件,它用到了uwsgi协议(与nginx通信),wsgi协议(调用Flask app)。当有客户端发来请求,nginx先做处理(静态资源是nginx的强项),无法处理的请求(uWSGI),最后的相应也是nginx回复给客户端的。 多了一层反向代理有什么好处?

提高web server性能(uWSGI处理静态资源不如nginx;nginx会在收到一个完整的http请求后再转发给wWSGI)

nginx可以做负载均衡(前提是有多个服务器),保护了实际的web服务器(客户端是和nginx交互而不是uWSGI)

何时需要nginx?

  1. 需要返回静态资源时,nginx处理静态资源更高效

  2. 当有多个服务器时,nginx可以做负载均衡

  3. 需要 TLS/HTTPS(证书集中在前端,后端用纯 HTTP)

  4. 需要缓冲慢客户端(Nginx 先接收完整请求体,再迅速把数据交给 uWSGI,释放后端 worker)

  5. 需要统一的限流、连接数控制、WAF、安全过滤、IP 黑白名单

  6. 需要把不同路径路由到不同后端(/api -> Flask;/static -> 本地目录;/svc2 -> 另一服务)

何时不需要nginx?

  1. 只有一个服务器
  2. 不需要返回静态资源,静态资源托管在对象存储 (S3/OSS/CDN),只暴露简单 JSON API
  3. 不需要 TLS/HTTPS(证书集中在前端,后端用纯 HTTP)

绝大多数对外生产场景,把 TLS 终止(HTTPS)放在 Nginx(或等价前端)是“职责分离 + 性能 + 安全 + 运维”最佳实践;uWSGI/Flask 保持明文本地通信即可。

所以,生成环境下部署 Flask 应用的推荐架构是:Nginx + uWSGI + Flask

0x02 uWSGI简介

uWSGI(官方写法为"uWSGI")是一个Web服务器和应用服务器,用于将Web应用程序和Web服务器之间进行通信。它充当Web服务器和Python Web应用程序之间的桥梁,实现了WSGI协议(Web Server Gateway Interface)的处理。

Nginx本身就是Web服务器,我们为什么还需要uWSGI这个Web服务器呢? Django不是自带runserver服务器?Flask不是自带Werkzeug吗? 答案是Nginx处理静态文件非常优秀,却不能直接与我们的Python Web应用程序进行交互。Django和Flask本身是Web框架,并不是Web服务器,它们自带的runserver和Werkzeug也仅仅用于开发测试环境,生产环境中处理并发的能力太弱。

为了解决Web 服务器与应用程序之间的交互问题,就出现了Web 服务器与应用程序之间交互的规范。最早出现的是CGI,后来又出现了改进 CGI 性能的FasgCGI,Java 专用的 Servlet 规范。在Python领域,最知名的就是WSGI规范了。

0x03 uWSGI常用命令

一个简易的wsgi app的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
def application(environ, start_response):
status = '200 OK'
headers = [('Content-Type','text/html')]
start_response(status,headers)
''' start_response 是 WSGI 服务器传给应用 (application) 的一个可调用对象。
你调用它来“启动响应”:确定 HTTP 状态行与响应头,
并把这些元信息交给服务器。它的签名: start_response(status,
response_headers, exc_info=None) 返回值:一个 write(body_bytes) 函数
(很少直接用;通常忽略,改用迭代器返回响应体)
'''
return [b'Hello World']

可以使用如下命令来启动uwsgi

1
2
3
4
5
6
7
8
uwsgi --http :8080 --wsgi-file myapp.py

# 启动一个简单的 Flask 应用
uwsgi --http :8080 --wsgi-file myflaskapp.py --callable app
# --callable 指定 Flask app 对象的名称,默认是 application

# 使用uwsgi命令行启动Django项目,端口8000
$ uwsgi --http :8000 --module myproject.wsgi:application

--module 期望“可导入模块路径 + 可调用对象”(默认可调用名为 application)。Django 项目自动生成 myproject/wsgi.py,里面有 application = get_wsgi_application(),正好符合默认名称,所以写 --module myproject.wsgi:application(甚至可省略 :application)。

wsgi-file 直接给文件路径,uWSGI 读取并执行该文件,再从其中拿到指定 --callable(默认仍叫 application;若你用 app 需 --callable app)。

Flask 示例里通常文件名不是一个包路径(或你懒得设 PYTHONPATH),所以示例常用 --wsgi-file app.py --callable app。

两者可互换:如果你的 Flask 项目是包结构,如 mysite/init.py 里有 app,对应也可用 --module mysite:app;反之也可以对 Django 用 --wsgi-file myproject/wsgi.py(因其中也定义了 application)。

Django 必须先加载 settings 和框架初始化逻辑(wsgi.py 已封装),所以最自然是 --module myproject.wsgi。Flask 没这个自动生成的 wsgi.py,最小示例就是一个脚本文件。

常用命令如下

1
2
3
4
5
6
7
8
9
10
11
# 启动uWSGI服务器
$ uwsgi --ini uwsgi.ini

# 重启uWSGI服务器
$ sudo service uwsgi restart

# 查看所有uWSGI进程
$ ps aux | grep uwsgi

# 停止所有uWSGI进程
$ sudo pkill -f uwsgi -9

uWSGI常用配置选项如下所示,稍加修改(项目名,项目根目录)即可部署大部分Python Web项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
[uwsgi]
uid=www-data # Ubuntu系统下默认用户名
gid=www-data # Ubuntu系统下默认用户组
project=mysite1 # 项目名
base = /home/user1 # 项目根目录

home = %(base)/Env/%(project) # 设置项目虚拟环境,Docker部署时不需要
chdir=%(base)/%(project) # 设置工作目录
module=%(project).wsgi:application # wsgi文件位置

master=True # 主进程
processes=2 # 同时进行的进程数,一般

# 选项1, 使用unix socket与nginx通信,仅限于uwsgi和nginx在同一主机上情形
# Nginx配置中uwsgi_pass应指向同一socket文件
socket=/run/uwsgi/%(project).sock

# 选项2,使用TCP socket与nginx通信
# Nginx配置中uwsgi_pass应指向uWSGI服务器IP和端口
# socket=0.0.0.0:8000 或则 socket=:8000

# 选项3,使用http协议与nginx通信
# Nginx配置中proxy_pass应指向uWSGI服务器一IP和端口
# http=0.0.0.0:8000

# socket权限设置
chown-socket=%(uid):www-data
chmod-socket=664

# 进程文件
pidfile=/tmp/%(project)-master.pid

# 以后台守护进程运行,并将log日志存于temp文件夹。
daemonize=/var/log/uwsgi/%(project).log

# 服务停止时,自动移除unix socket和pid文件
vacuum=True

# 为每个工作进程设置请求数的上限。当处理的请求总数超过这个量,进程回收重启。
max-requests=5000

# 当一个请求花费的时间超过这个时间,那么这个请求都会被丢弃。
harakiri=60

#当一个请求被harakiri杀掉会,会输出一条日志
harakiri-verbose=true

# uWsgi默认的buffersize为4096,如果请求数据超过这个量会报错。这里设置为64k
buffer-size=65536

# 如果http请求体的大小超过指定的限制,打开http body缓冲,这里为64k
post-buffering=65536

#开启内存使用情况报告
memory-report=true

#设置平滑的重启(直到处理完接收到的请求)的长等待时间(秒)
reload-mercy=10

#设置工作进程使用虚拟内存超过多少MB就回收重启
reload-on-as=1024

0x04 uWSGI和Nginx之间的通信方式

unix socket,http-socket和http。Nginx的配置必需与uwsgi配置保持一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 选项1, 使用unix socket与nginx通信
# 仅限于uwsgi和nginx在同一主机上情形
# Nginx配置中uwsgi_pass应指向同一socket文件地址
socket=/run/uwsgi/%(project).sock

# 选项2,使用TCP socket与nginx通信
# Nginx配置中uwsgi_pass应指向uWSGI服务器IP和端口
socket==0.0.0.0:8000 或则 socket=:8000

# 选项3,使用http协议与nginx通信
# Nginx配置中proxy_pass应指向uWSGI服务器IP和端口
http==0.0.0.0:8000

如果你的nginx与uwsgi在同一台服务器上,优先使用本地机器的unix socket进行通信,这样速度更快。此时nginx的配置文件如下所示:

1
2
3
4
location / {     
include /etc/nginx/uwsgi_params;
uwsgi_pass unix:/run/uwsgi/django_test1.sock;
}

如果nginx与uwsgi不在同一台服务器上,两者使用TCP socket通信,nginx可以使用如下配置:

1
2
3
4
location / {     
include /etc/nginx/uwsgi_params;
uwsgi_pass uWSGI_SERVER_IP:8000;
}

如果nginx与uwsgi不在同一台服务器上,两者使用http协议进行通信,nginx配置应修改如下:

1
2
3
4
location / {     
# 注意:proxy_pass后面http必不可少哦!
proxy_pass http://uWSGI_SERVER_IP:8000;
}

include字段在Nginx配置中的作用是包含外部文件或模块的配置内容。它允许将其他文件中的配置指令包含到当前的配置文件中,以便实现代码的模块化和重用。

具体来说,include指令用于将指定路径下的文件内容包含到当前位置。这些被包含的文件可以包含Nginx的配置指令、变量定义、映射规则、缓存规则等。

include指令可以用于以下场景:

分割配置文件:将大型的Nginx配置文件分割为多个小文件,使得配置更加清晰和易于维护。可以根据不同的功能或服务,将相关的配置指令放在不同的文件中,并使用include指令将它们包含到主配置文件中。

共享配置片段:如果有多个Nginx服务器或虚拟主机需要共享一些相同的配置片段,可以将这些公共配置片段放在一个独立的文件中,并在需要的地方使用include指令进行引用。这样可以避免重复编写相同的配置,提高代码的重用性和可维护性。

引入第三方模块的配置:某些功能可能需要额外的第三方模块或插件来实现。这些模块通常会提供自己的配置文件,我们可以使用include指令将这些配置文件包含到主配置文件中,以启用这些模块的功能。

0x05 docker容器化部署nginx + uwsgi + flask + mysql

flask/uwsgi 镜像 dockerfile.prod

flask镜像可以从alpine或者python官方镜像构建

镜像构建步骤如下所示:

  • 修改时区
  • 修改apk源
  • 安装python和pip
  • 通过pip安装依赖包
  • 创建一个非root用户避免在root下运行uwsgi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# 二开推荐阅读[如何提高项目构建效率](https://developers.weixin.qq.com/miniprogram/dev/wxcloudrun/src/scene/build/speed.html)
# 选择基础镜像。如需更换,请到[dockerhub官方仓库](https://hub.docker.com/_/python?tab=tags)自行选择后替换。
# 已知alpine镜像与pytorch有兼容性问题会导致构建失败,如需使用pytorch请务必按需更换基础镜像。
FROM alpine:3.13

# 容器默认时区为UTC,如需使用上海时间请启用以下时区设置命令
RUN apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo Asia/Shanghai > /etc/timezone

# 使用 HTTPS 协议访问容器云调用证书安装
RUN apk add ca-certificates

# 安装依赖包,如需其他依赖包,请到alpine依赖包管理(https://pkgs.alpinelinux.org/packages?name=php8*imagick*&branch=v3.13)查找。
# 选用国内镜像源以提高下载速度
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tencent.com/g' /etc/apk/repositories \
# 安装python3
&& apk add --update --no-cache python3 py3-pip \
&& rm -rf /var/cache/apk/*



# 解决sqlclient构建环境问题
# 解决mysqlclient构建环境问题(Alpine下用apk安装)增加 gcc、musl-dev、libffi-dev、python3-dev、mariadb-connector-c-dev 等依赖
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tencent.com/g' /etc/apk/repositories \
&& apk add --update --no-cache python3 py3-pip gcc musl-dev libffi-dev python3-dev mariadb-connector-c-dev \
&& rm -rf /var/cache/apk/*

# 添加appuser用户组避免在root下运行
RUN addgroup -g 1000 web && adduser -D -u 1000 -G web -h /app appuser \
&& mkdir -p /var/run/uwsgi \
&& mkdir -p /app/logs \
&& chown -R appuser:web /app /var/run/uwsgi


# 先复制依赖文件,安装依赖
COPY requirements.txt /app/requirements.txt
WORKDIR /app

# 安装依赖到指定的/install文件夹
# 选用国内镜像源以提高下载速度
RUN pip config set global.index-url http://mirrors.cloud.tencent.com/pypi/simple \
&& pip config set global.trusted-host mirrors.cloud.tencent.com \
&& pip install --upgrade pip \
# pip install scipy 等数学包失败,可使用 apk add py3-scipy 进行, 参考安装 https://pkgs.alpinelinux.org/packages?name=py3-scipy&branch=v3.13
&& pip install -r requirements.txt

# 拷贝当前项目到/app目录下(.dockerignore中文件除外)
COPY . /app

ENV PYTHONPATH="/app"

USER appuser
# 执行启动命令
# 写多行独立的CMD命令是错误写法!只有最后一行CMD命令会被执行,之前的都会被忽略,导致业务报错。
# 请参考[Docker官方文档之CMD命令](https://docs.docker.com/engine/reference/builder/#cmd)
EXPOSE 8000
CMD ["uwsgi", "--ini", "uwsgi.ini"]

uwsgi 配置 uwsgi.ini

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[uwsgi]
chdir = /app
module = wsgi:application

master = true
processes = 4
threads = 2

socket = /var/run/uwsgi/uwsgi.sock
chmod-socket = 660
vacuum = true
stats = /var/run/uwsgi/stats.sock

pidfile = /var/run/uwsgi/uwsgi.pid
die-on-term = true
lazy-apps = true
single-interpreter = true
log-date = true
disable-logging = false

对于module方式,需要指定wsgi文件位置和application对象名称

创建socket文件夹/var/run/uwsgi,并将其权限设置为660,便于nginx访问

stats.sock用于监控uwsgi状态

nginx 镜像 dockerfile.nginx

1
2
3
4
5
6
FROM nginx:1.25-alpine
# 添加与应用一致的组 (gid 1000) 并把 nginx 用户加入
RUN addgroup -g 1000 -S web && addgroup nginx web
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD wget -q -O - http://127.0.0.1/healthz || exit 1

nginx 配置 nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
events { worker_connections 1024; }
http {
server {
listen 80;
server_name _;
location / {
uwsgi_pass unix:/var/run/uwsgi/uwsgi.sock;
include uwsgi_params;
uwsgi_read_timeout 60;
}
location /healthz {
return 200 'ok';
add_header Content-Type text/plain;
}
access_log /dev/stdout;
error_log /dev/stderr warn;
}
}

主要是对路由进行一个转发保护后端(处理慢客户端),后期可以添加tls等配置。

docker服务编排 docker-compose.yml

整个服务采用flask/uwsgi + nginx + mysql三容器编排方式部署

优点:

  • 资源隔离:每个服务运行在独立的容器中,互不影响,便于管理和扩展。
  • 便于部署:使用docker-compose可以一键启动和停止所有服务,简化了部署流程。
  • 易于扩展:可以根据需要轻松增加或减少服务实例,满足不同的负载需求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

services:
app:
profiles: ["prod"]
build:
context: .
dockerfile: Dockerfile.prod
volumes:
- uwsgi-run:/var/run/uwsgi
environment:
- FLASK_DEBUG=0
- FLASK_APP=wsgi.py
- APP_ENV=prod
- MYSQL_USERNAME=name
- MYSQL_PASSWORD=password
- MYSQL_ADDRESS=mysql:3306
- MYSQL_DATABASE=database
depends_on:
- mysql
restart: unless-stopped

# 开发环境服务(nodemon + 代码挂载) - dev profile
app_dev:
profiles: ["dev"]
build:
context: .
dockerfile: Dockerfile.dev
image: app_dev
volumes:
- .:/app # 本地代码同步到容器,便于热重载
environment:
- FLASK_DEBUG=1
- FLASK_APP=run.py
- APP_ENV=dev
- MYSQL_USERNAME=name
- MYSQL_PASSWORD=password
- MYSQL_ADDRESS=mysql:3306
- MYSQL_DATABASE=database
ports:
- "8000:8000" # run.py 默认 8000,按需改为 80
depends_on:
- mysql
restart: unless-stopped


nginx:
profiles: ["prod"]
build:
context: .
dockerfile: Dockerfile.nginx
depends_on:
- app
volumes:
- uwsgi-run:/var/run/uwsgi
ports:
- "80:80"
restart: unless-stopped

mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=root_pass
- MYSQL_DATABASE=database
- MYSQL_USER=name
- MYSQL_PASSWORD=password
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
restart: unless-stopped
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3307:3306"

volumes:
mysql_data:
uwsgi-run:

如上所示,app镜像使用dockerfile.prod文件构建,nginx镜像使用dockerfile.nginx文件构建

配置文件为了方便开发,使用了两个profile,prod和dev,dev中采用了代码挂载和nodemon热重载方式,抛弃了nginx和uwsgi,直接使用flask自带的run.py启动服务,便于开发调试。

mysql服务挂载了mysql_data数据卷以便数据持久化。

网络通信结构,Compose 会在同一自定义网络内为每个服务注册一个内部 DNS 名(服务名 / 网络别名)。flask访问mysql时,MYSQL_ADDRESS=mysql:3306。uwsgi和nginx的通信方式采用unix socket,uwsgi配置中socket=/run/uwsgi/mysite1.sock,nginx配置中uwsgi_pass unix:/run/uwsgi/mysite1.sock,并将/run/uwsgi目录挂载到数据卷uwsgi-run(对于socket访问权限,需要将nginx和uwsig加入到统同一用户组中)。

1
2
docker-compose --profile prod up -d # 生产环境 使用--build 参数强制重建镜像
docker-compose --profile dev up -d # 开发环境

0xff 参考文章


flask | uwsgi生产化部署flask服务
https://jfsas.github.io/2025/09/27/flask-uwsgi生产化部署flask服务/
作者
JFSAS
发布于
2025年9月27日
许可协议