部署Flask + uWSGI + Nginx

  • 本站文章除注明转载外,均为本站原创或者翻译。
  • 本站文章欢迎各种形式的转载,但请18岁以上的转载者注明文章出处,尊重我的劳动,也尊重你的智商;
  • 本站部分原创和翻译文章提供markdown格式源码,欢迎使用文章源码进行转载;
  • 本博客采用 WPCMD 维护;
  • 本文标题:部署Flask + uWSGI + Nginx
  • 本文链接:http://zengrong.net/post/2568.htm

2017-01-06 更新: 增加 Flask 502 错误解决


作为一个选择综合症+洁癖患者,在部署 Flask app 的时候着实有点纠结。互联网上能找到的教程都不怎么靠谱,要么太老,要么太乱连话都说不通顺,更别提那些转了无数手的资料了。 uwsgi 的参数别名众多,各种旧版本的配置也不尽相同,让急着把自己的 Flask 站点搭建到正式服务器上的新手们无所适从。

我把自己的部署过程写出来,所有资源参考官方文档,我也尽量给出选择的原因。这教程适用于各个 linux 发行版。若有疑问,请留言。

组件选择

  • Flask 开发使用 python3.5 ;
  • python 虚拟环境,使用 python3.3 开始自带的 venv;
  • 启动器使用 supervisor v3,uwsgi 使用 v2;
  • uwsgi 和 supervisor 均使用 pip 安装,而不使用 linux 自带的版本。

因为 ubuntu 自带 uwsgi,我曾经纠结过使用系统自带版本还是使用 pip 版本。使用自带的版本的好处,就是安装后可以直接使用 /etc/init.d/uwsgi start 来启动,不需要自己写启动脚本,但我仍然选择了使用 pip 版本。下面是原因。

uwsgi 的版本在 Ubuntu 12.04 上比较老了(1.03),这让人心生不快。supervisor 的优势摆在那里,而且也是 python 写成,这激发了我的“纯正血统情怀”。还有一个优势就是 uwsgi 可以安装在 venv 之下,这样不必影响系统包的稳定性(不过我并没有用上这个优势,后面会讲到原因)。

supervisor 也有系统自带版本,版本号很老了(3.0a8-1.1)。supervisor 文档 中贴心地提供了全套 initscripts ,在各个发行版上启动都不是问题。

项目文件夹结构

待部署的项目名称为 EM ,文件夹结构如下,部署在服务器的 /srv/www/em 文件夹下:

├── README.md
├── app
│   ├── __init__.py
│   ├── main
│   ├── models.py
│   ├── static
│   └── templates
├── config.py
├── manage.py
├── migrations
├── requirements.txt
├── tests
├── venv
└── uwsgi.ini

安装 supervisor

supervisor 不支持 python3 ,这在官方文档中就已经说明:

Supervisor is known to work with Python 2.4 or later but will not work under any version of Python 3.

好在服务器上都有自带的 python2 。但我的服务器上的 python2 中并没有 pip,因此需要先安装 pip。

标准的 pip 安装方法是:

wget https://bootstrap.pypa.io/get-pip.py
python get-pip.py

安装和下载过程中的全部内容都是国外的服务器导致速度堪忧,下面提供一个简单的方法。

1. 访问 豆瓣的 pypi 源 ,在该页面中搜索 6.1.0.tar.gz ,下载搜索到的那个链接,例如我的下载链接为:

http://pypi.doubanio.com/packages/6c/84/432eb60bbcb414b9cdfcb135d5f4925e253c74e7d6916ada79990d6cc1a0/pip-6.1.0.tar.gz#md5=d0c349765bbc23743cec42b37bd8a281

更多的源请参见: 常用开源镜像站整理

2. pip 的安装依赖 setuptools ,搜索 20.1.tar.gz ,下载搜索到的那个链接。

3. 将下载到的两个压缩包解压,然后安装:

python setuptools-20.1/setup.py build
python setuptools-20.1/setup.py install
python pip-6.1.0/setup.py build
python pip-6.1.0/setup.py install

上面的版本号不一定要和我的一致,但一定要选择 tar.gz 的压缩包,这样的压缩包还是使用 egg 格式发布的,可以使用 setup.py 安装。新版本的 pip 和 setuptools 都使用 wheel 格式发布了,解压后无法使用 setup.py 安装。想了解 egg 和 wheel 的区别,可参考 Python 包管理工具解惑

安装了 pip 之后,就可以使用 pip install supervisor 了。注意,不应该将 supervisor 安装在虚拟环境中,作为一个通用组件,它应该是一个系统级别的存在。

这里还有一个小插曲。我使用 常用开源镜像站整理 中提到的方法把 pypi 的源设置成了豆瓣 HTTPS 源,但更新 pip 的时候报错:

# pip install -U pip
/usr/local/lib/python2.7/dist-packages/pip-6.1.0-py2.7.egg/pip/_vendor/requests/packages/urllib3/util/ssl_.py:79: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning.
  InsecurePlatformWarning
DEPRECATION: Failed to find 'pip' at https://pypi.doubanio.com/simple/pip/. It is suggested to upgrade your index to support normalized names as the name in /simple/{name}.
/usr/local/lib/python2.7/dist-packages/pip-6.1.0-py2.7.egg/pip/_vendor/requests/packages/urllib3/util/ssl_.py:79: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning.
  InsecurePlatformWarning
Cannot fetch index base URL https://pypi.doubanio.com/simple/
/usr/local/lib/python2.7/dist-packages/pip-6.1.0-py2.7.egg/pip/_vendor/requests/packages/urllib3/util/ssl_.py:79: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning.
  InsecurePlatformWarning
Requirement already up-to-date: pip in /usr/local/lib/python2.7/dist-packages/pip-6.1.0-py2.7.egg

看反馈的信息可能是 urllib3 自带的 ssl 加密问题。pip 升级到 8.1.2 也有相同的问题。

因此,我把 pypi 源换成了没有 https 加密的地址(见下方),安装成功。

# pip install supervisor
Collecting supervisor
  Downloading http://pypi.zenlogic.net/packages/44/80/d28047d120bfcc8158b4e41127706731ee6a3419c661e0a858fb0e7c4b2d/supervisor-3.3.0.tar.gz (416kB)
    100% 419kB 3.3MB/s 
Collecting meld3>=0.6.5 (from supervisor)
  Downloading http://pypi.zenlogic.net/packages/py2.py3/m/meld3/meld3-1.0.2-py2.py3-none-any.whl
Installing collected packages: meld3, supervisor
  Running setup.py install for supervisor ... done
Successfully installed meld3-1.0.2 supervisor-3.3.0

配置 supervisor

下面以 ubuntu 为例进行配置。

1. 在initscripts 下载 ubuntu 的启动脚本,复制到 /etc/init.d/supervisor

2. 修改启动脚本中的路径,使其与安装路径相匹配。如果使用 pip 安装,supervisord 一般位于 /usr/local/bin/ 而非默认的 /usr/bin/ ;

3. 执行 echo_supervisord_conf > /etc/supervisor/supervisord.conf 创建一个默认的配置文件;

4. 调整 /etc/supervisor/supervisord.conf 中的几个路径相关的参数,使其与 /etc/init.d/supervisor 中的路径参数完全一致:

; (the path to the socket file)
file=/var/run/supervisor.sock
; (main log file;default $CWD/supervisord.log)
logfile=/var/log/supervisord.log
; (supervisord pidfile;default supervisord.pid)
pidfile=/var/run/supervisord.pid
; use a unix:// URL for a unix socket
serverurl=unix:///var/run/supervisor.sock

上面的三个参数默认的值位于 /tmp 中,这样可能会导致执行 /etc/init.d/supervisor start 的时候由于找不到 pid 文件导致误判。

5. 调整 /etc/supervisor/supervisord.conf 中的 [includes] section 为如下所示(注意要使用绝对路径):

[include]
files = /etc/supervisor/conf.d/*.conf

这样,加入程序的时候就不必修改主配置文件,只需要在 /etc/supervisor/conf.d 文件夹下面增加一个 .conf 配置文件即可。

来看看成果吧!

# service supervisor start
Starting supervisor: supervisord.
# service supervisor status
supervisord is  not running.

安装 uwsgi

在服务器上 clone 了 flask 项目,然后安装 uwsgi ,但碰到如下错误:

# pip3 install uwsgi
......
d -ldl -lutil -lrt -lm /usr/local/lib/python3.5/config-3.5m/libpython3.5m.a -lutil -lcrypt
    /usr/local/lib/python3.5/config-3.5m/libpython3.5m.a(pytime.o): In function `pygettimeofday':
    /root/software/Python-3.5.1/Python/pytime.c:494: undefined reference to `clock_gettime'
    /usr/local/lib/python3.5/config-3.5m/libpython3.5m.a(pytime.o): In function `pymonotonic':
    /root/software/Python-3.5.1/Python/pytime.c:633: undefined reference to `clock_gettime'
    /usr/local/lib/python3.5/config-3.5m/libpython3.5m.a(pytime.o): In function `pygettimeofday':
    /root/software/Python-3.5.1/Python/pytime.c:494: undefined reference to `clock_gettime'
    /root/software/Python-3.5.1/Python/pytime.c:508: undefined reference to `clock_getres'
    /usr/local/lib/python3.5/config-3.5m/libpython3.5m.a(pytime.o): In function `pymonotonic':
    /root/software/Python-3.5.1/Python/pytime.c:633: undefined reference to `clock_gettime'
    /root/software/Python-3.5.1/Python/pytime.c:646: undefined reference to `clock_getres'
    /usr/local/lib/python3.5/config-3.5m/libpython3.5m.a(timemodule.o): In function `py_process_time':
    /root/software/Python-3.5.1/./Modules/timemodule.c:977: undefined reference to `clock_gettime'
    /root/software/Python-3.5.1/./Modules/timemodule.c:983: undefined reference to `clock_getres'
    /usr/local/lib/python3.5/config-3.5m/libpython3.5m.a(timemodule.o): In function `time_clock_getres':
    /root/software/Python-3.5.1/./Modules/timemodule.c:205: undefined reference to `clock_getres'
    /usr/local/lib/python3.5/config-3.5m/libpython3.5m.a(timemodule.o): In function `time_clock_gettime':
    /root/software/Python-3.5.1/./Modules/timemodule.c:151: undefined reference to `clock_gettime'
    /usr/local/lib/python3.5/config-3.5m/libpython3.5m.a(timemodule.o): In function `time_clock_settime':
    /root/software/Python-3.5.1/./Modules/timemodule.c:182: undefined reference to `clock_settime'
    collect2: ld returned 1 exit status
    *** error linking uWSGI ***

    ----------------------------------------
Command "/urr/local/bin/python3.5 -u -c "import setuptools, tokenize;__file__='/tmp/pip-build-vz6gni75/uwsgi/setup.py';exec(compile(getattr(tokenize, 'open', open)(__file__).read().replace('\r\n', '\n'), __file__, 'exec'))" install --record /tmp/pip-klu1fasz-record/install-record.txt --single-version-externally-managed --compile --install-headers /url/local/lib/site/python3.5/uwsgi" failed with error code 1 in /tmp/pip-build-vz6gni75/uwsgi/

我的服务器中的 python3.5 是自行编译安装的。因为服务器发行版是 Ubuntu12.04,官方源中并没有 python3.5。自行编译安装的 python 缺少了 python3.5-dev 这个包,导致 uwsgi 安装的时候无法编译通过。我碰到的问题和 这里 描述的一致。

我采用了一种比较简单的解决方案,就是删除编译安装的 python3.5 ,加入 PPA 源,再安装 PPA 源中的 python3.5 。

但是,python3.5 的源码包并没有提供 make uninstall 命令,因此删除已经编译安装的 python3.5 成了一个问题。有两个方法解决这个问题。

1. 使用 checkinstall 基于当时编译用的源码创建一个 deb 包,然后安装它,再删除它。 via

# 安装 checkinstall
sudo apt-get install checkinstall
cd /home/rzeng/Python_3.5.1
# 创建一个 deb 包。注意在这里提供一个特定的包名。例如 python351,如果使用默认的 python,则可能和发行版自带的 python2 冲突。
sudo checkinstall -D --fstrans=no make install
# 基于创建的 deb 包重新安装 python
sudo dpkg -i python_3.5.1-1.amd64.deb
# 移除刚才安装的包
sudo dpkg -r python351

2. 直接删除已经安装的文件。 via

使用 whereis 获取到编译安装的 python 的安装路径,然后逐一删除即可。

# whereis python3
python3: /usr/bin/python3.5m-config /usr/bin/python3.5m /usr/bin/python3.5-config /usr/bin/python3.5 /etc/python3.5 /usr/lib/python3 /usr/lib/python3.5 /usr/local/lib/python3.5 /usr/include/python3.5m /usr/include/python3.5

如果怕 whereis 删不干净,也可以重新编译安装一遍,记录所有的安装日志:make install &> |tee make.log ,然后在日志里分析所有文件的安装路径,再逐个删除。

下面是加入 ppa 源并安装 python3.5 就简单多了:

sudo add-apt-repository ppa:fkrull/deadsnakes
sudo apt-get update
sudo apt-get install python3.5 python3.5-dev

现在安装 uwsgi ,一次成功。注意,和 supervisor 一样,不应该将 uwsgi 安装在虚拟环境中。

# python3.5 -m pip install uwsgi
Collecting uwsgi
  Downloading http://pypi.zenlogic.net/packages/83/22/47b6ff871a5f01b9f660121cf61ba1eccbf7886b5cbe24caacccd0d00d07/uwsgi-2.0.13.1.tar.gz (784kB)
    100% 788kB 3.0MB/s 
Installing collected packages: uwsgi
  Running setup.py install for uwsgi ... done
Successfully installed uwsgi-2.0.13.1

配置 uwsgi

uwsgi 支持使用 ini/xml/json/yaml 格式作为配置文件(关于配置文件,可参考 常用配置文件格式简析 )。我建议使用 ini 格式,这样最为简单。下面是个最简单的配置文件(uwsgi.ini):

[uwsgi]
; 执行的启动文件
wsgi-file = manage.py
; 对应 manage.py 的同名全局变量,是一个 Flask app
callable = app

; 在 ubuntu 下,使用 www-data 权限来执行,其他发行版请自行修改
uid = www-data
gid = www-data

; 虚拟环境的文件夹
venv = /srv/www/em/venv

; 启动 http 监听,注意不要写成了 socket
http = 0.0.0.0:5000

在 uwsgi 的配置中,有许多配置是有多个别名的。例如设置虚拟环境的这个配置 venv,就有如下几个别名:

  • venv
  • virtualenv
  • home
  • pyhome

wsgi-file 的别名是 file

所以,在网上搜索到的教程使用各种别名的都有,让人感觉很混乱。查一下 官方文档 就很清楚了。由于 home 这个名称有欺骗性,我偏爱于使用 venv 这个名称。

接下来启动 uwsgi 进程,显示如下:

#uwsgi uwsgi.ini
[uWSGI] getting INI configuration from uwsgi.ini
*** Starting uWSGI 2.0.13.1 (64bit) on [Wed Jul 27 20:29:52 2016] ***
compiled with version: 4.6.3 on 27 July 2016 09:59:07
os: Linux-3.2.0-60-generic #91-Ubuntu SMP Wed Feb 19 03:54:44 UTC 2014
nodename: aliyun
machine: x86_64
clock source: unix
pcre jit disabled
detected number of CPU cores: 1
current working directory: /srv/www/em
detected binary path: /usr/local/bin/uwsgi
setgid() to 33
setuid() to 33
*** WARNING: you are running uWSGI without its master process manager ***
your processes number limit is 7815
your memory page size is 4096 bytes
detected max file descriptor number: 1024
lock engine: pthread robust mutexes
thunder lock: disabled (you can enable it with --thunder-lock)
uWSGI http bound on 0.0.0.0:5000 fd 4
spawned uWSGI http 1 (pid: 25209)
uwsgi socket 0 bound to TCP address 127.0.0.1:41177 (port auto-assigned) fd 3
Python version: 3.5.2 (default, Jul 17 2016, 17:43:04)  [GCC 4.6.3]
PEP 405 virtualenv detected: /srv/www/em/venv
Set PythonHome to /srv/www/em/venv
*** Python threads support is disabled. You can enable it with --enable-threads ***
Python main interpreter initialized at 0x18f2ba0
your server socket listen backlog is limited to 100 connections
your mercy for graceful operations on workers is 60 seconds
mapped 72768 bytes (71 KB) for 1 cores
*** Operational MODE: single process ***
WSGI app 0 (mountpoint='') ready in 1 seconds on interpreter 0x18f2ba0 pid: 25208 (default app)
*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI worker 1 (and the only) (pid: 25208, cores: 1)

访问服务器,可以看到 uwsgi 收到的 get 请求:

[pid: 25208|app: 0|req: 1/1] 119.97.214.98 () {36 vars in 913 bytes} [Wed Jul 27 20:31:56 2016] GET / => generated 1349 bytes in 68 msecs (HTTP/1.1 200) 2 headers in 81 bytes (1 switches on core 0)

最后,可以把标准输出记录到 log 中,在 uwsgi.ini 中加入(记得要把 logs 文件夹的写权限给 www-data 账户):

[uwsgi]
; .....
logto = /srv/www/em/logs/uwsgi.log

这样,再次启动 uwsgi 的时候,显示如下:

#uwsgi uwsgi.ini
[uWSGI] getting INI configuration from uwsgi.ini

所有其他的输出都被记录到日志文件中去了。

配置 supervisor + uwsgi

增加一个 supervisor 配置文件: /etc/supervisor/conf.d/em.conf ,内容如下:

[program:em]

; 启动命令行。uwsgi 已经在系统 PATH 中,因此直接调用,参数使用了绝对路径。
command=uwsgi /srv/www/em/uwsgi.ini

; 启动文件夹。这里使用 Flask 项目所在文件夹,也就是 uwsgi.ini 所在文件夹。
directory=/srv/www/em

; 启动帐号。和 uwsgi 的配置相同
user=www-data

; 分别保存标准输出和标准错误日志,如果 uwsgi 启动出错,可以去日志里面找原因。
stdout_logfile=/srv/www/em/logs/supervisor_uwsgi_stdout.log
stderr_logfile=/srv/www/em/logs/supervisor_uwsgi_stderr.log

接着进入 supervisorctl 来启动 em 这个程序:

supervisor> start em
em: started
supervisor> status em           RUNNING   pid 26267, uptime 0:00:29

可以使用 help 命令查看 supervisorctl 提示符支持的子命令。

此时可以访问 http://your-domain:5000 来查看 Flask 站点了。

配置 Nginx + uwsgi

最后一步,需要把 Flask 站点置于 Nginx 之后。因为我的服务器上还有 php 程序,要共用 80 端口,就需要 Nginx 来做转发。我使用的是 Nginx 的修改版 OpenResty,它的配置和 Nginx 完全相同。如果你要安装 OpenResty,可以参考这篇: 从 Lighttpd 到 OpenResty

我的 OpenResty 配置文件夹为 /usr/local/openresty/nginx/conf ,所有的虚拟主机放在 vhost 子文件夹之下。ubuntu 和 centos 的配置文件夹路径可能不同,请自行判断。

下面是 /usr/local/openresty/nginx/conf/vhost/em.conf 的内容:

server {
    listen 80;
    server_name your-domain;
    access_log logs/access-em.log;
    error_log logs/error-em.log;

    location / { try_files $uri @em; }
    location @em {
        root    /srv/www/em/app;
        include uwsgi_params;
        uwsgi_pass 127.0.0.1:5000;
    }
}

这部分的配置在 Flask 官方文档 Configuring nginx 中有提及,uwsgi 的官方文档 Virtual Hosting 中也有说明。这些文档写的简洁清晰,比乱七八糟夹缠不清的各种转载文档是要好多了。

重启 Nginx 看效果。此时访问 http://your-domain ,你很可能会碰到 502 错误:

502 Bad Gateway
openresty/1.9.3.1

这是因为 uwsgi 配置文件中,我们使用的是 http 方式,直接把 uwsgi 作为 HTTP 服务器使用。我们需要修改 uwsgi 配置:

[uwsgi]
; ...
; 启动 http 监听,注意不要写成了 socket
; http = 0.0.0.0:5000
; 启动 socket 监听
socket = 0.0.0.0:5000

然后使用 supervisorctl 重启 em 程序:

# supervisorctl                             
em                               RUNNING   pid 32073, uptime 0:04:25
supervisor> restart em
em: stopped
em: started

再次访问 http://your-domain成功!

在这里我多次碰到一个很诡异的问题,也记录一下,或许有用。

root 配置位于 location 块之外的时候,Safari 访问程序的子页面正常,但访问主页 http://your-domain 或者 http://your-domain/ 的时候会被跳转到默认主页(就好像虚拟主机没有配置成功),必须使用这样的链接访问 http://your-domain// (多个斜杠)才能成功访问主页。

有趣的是,Firefox 和 Chrome 都没有这个问题。例如使用下面的配置,Safari 访问会有问题。

server {
    ...
    server_name your-domain;
    access_log logs/access-em.log;
    error_log logs/error-em.log;

    root    /srv/www/em/app;
    location / { try_files $uri @em; }
    location @em {
        include uwsgi_params;
        uwsgi_pass 127.0.0.1:5000;
    }
}

当把 root 放在 location 之内,就没有这个问题了。Safari 和其他浏览器都正常。

使用子目录提供 Flask app 服务

有时候我们脑袋中会冒出一些 BT 的需求,例如,使用 /em 这个 URL 来提供对 EM 项目的服务,使用 /sexyzone 这个 URL 提供另一个毫不相干的项目的服务。就像这样:

http://your-domain/em
http://your-domain/sexyzone

那么,使用 http://your-domain/em 这个路径来提供 EM 这个项目的服务,应该如何做?

首先,需要在注册 Blueprint 的时候提供 url_prefix 参数。

app = Flask(__name__)
app.register_blueprint(main_blueprint, url_prefix='/em')

这样处理之后,在使用 url_for() 命令的时候,url 会自动加上 /em 前缀。

例如之前的源码为:

<ul class="nav navbar-nav">
    <li><a href="/">Home</a></li>
    <li><a href="/persons/">Persons</a></li>
    <li><a href="/teams/">Teams</a></li>
</ul>

修改之后会变为:

<ul class="nav navbar-nav">
    <li><a href="/em/">Home</a></li>
    <li><a href="/em/persons/">Persons</a></li>
    <li><a href="/em/teams/">Teams</a></li>
</ul>

其次,需要考虑 static 文件的路径,并没有跟随 Blueprint 的变化而变化,现在 static 内容依然没有前缀:

<script src="/static/bootstrap/jquery.min.js?bootstrap=3.3.6.0"></script>
<script src="/static/bootstrap/js/bootstrap.min.js?bootstrap=3.3.6.0"></script>

要解决这个问题,需要另一个参数,在创建 Flask 实例的时候增加 static_url_path 参数即可。

app = Flask(__name__, static_url_path='/em/static')

错误解决

Flask 502 错误解决:upstream sent too big header and invalid request block size

使用 Nginx 访问静态文件

(未完待续)