【Ubuntu / Python】Nginx + uWSGI + Flask + Supervisorの環境構築

Flask アプリケーションを Nginx でデプロイする時のメモです。

  • Flask: 軽量WAF (Web Application Framework)
  • uWSGI: WSGI (Web Server Gateway Interface)
  • Nginx: リバースプロキシ (Reverse Proxy)
  • Supervisor: プロセス監視

uWSGI

環境は Ubuntu14.04(LTS) ですが, 基本的には CentOS の yum でも提供されているかと思います。詳しくは Installing uWSGI を参照して下さい。

$ sudo pip install flask
# dependent packages
$ sudo apt-get install libxml2-dev libxslt1-dev
$ sudo pip install uwsgi

先に uWSGI で Flask アプリを起動しておきます。 以下の yourapp には Python スクリプト名が入ります。(e.g. yourapp.py)
ログ出力先を /var/log/uwsgi.log としていますが, stdout/stderr も出力したい場合は Flaskで PROPAGATE_EXCEPTIONS=True を設定します。

$ nohup uwsgi -s /tmp/uwsgi.sock \
  --module yourapp \
  --callable app \
  --chmod-socket=666 \
  --logto /var/log/uwsgi.log \
  --processes 4 &
$ ps aux | grep uwsgi

オプションが多くなる場合, iniファイルにまとめておくと便利です。ちなみに, python の引数は pyargv = –debug True のように指定します。

[uwsgi]
master = true
python-path = /your/project/path
wsgi = yourapp:app
chmod-socket = 666
callable = app
http-socket = :8888
socket = /tmp/uwsgi.sock
pidfile = /tmp/yourapp.pid
logto = /var/log/uwsgi.log
processes = 4
threads = 2

iniファイルからの起動コマンドは以下です。

$ uwsgi --ini uwsgi.ini

また, 停止コマンドは以下で, pidfile の path を指定します。

$ uwsgi --stop /tmp/yourapp.pid

Nginx

Nginx をインストールします。

$ sudo apt-get install nginx
$ nginx -v
nginx version: nginx/1.4.6 (Ubuntu)

設定を変更します。8080 portで公開, 内部では UNIX domain socket で uWSGI に繋ぎます。
設定は Flask の Docs を参考にしました。また, リバースプロキシで Header を書き換えられたくないので, 今回は proxy_set_header を設定しています。

$ sudo vim /etc/nginx/nginx.conf.d/server.conf
server {
    listen 8080;
    server_name xxxxxxxx;

    location / {
        try_files $uri @yourapplication;
        proxy_set_header Host $host;
    }
                
     location @yourapplication {
        include uwsgi_params;
        uwsgi_pass unix:/tmp/uwsgi.sock;
     }
}

ついでに, gzip を有効にします。

$ sudo vim /etc/nginx/nginx.conf
...
        gzip on;
        gzip_proxied any;
        gzip_types text/css text/javascript application/javascript application/x-javascript application/json;
        expires 30d;
...

再起動します。

$ sudo service nginx restart
$ sudo service nginx status

デフォルトのlogの場所は以下です。

$ tail -f /var/log/nginx/access.log
$ tail -f /var/log/nginx/error.log 

実際に Client のリクエストが Accept-Encoding gzip, deflate の場合, http-response-header で Content-Encoding gzip になっていることを確認できます。

Supervisor

Supervisor でプロセス管理を行います。

$ sudo apt-get install supervisor

service コマンドでデーモンが起動していることを確認します。

$ sudo service supervisor status
 * supervisor is running

Supervisor の設定ファイルは /etc/supervisor/supervisord.conf です。

; supervisor config file

[unix_http_server]
file=/var/run/supervisor.sock   ; (the path to the socket file)
chmod=0700                       ; sockef file mode (default 0700)

[supervisord]
logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)
pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
childlogdir=/var/log/supervisor            ; ('AUTO' child log dir, default $TEMP)

; the below section must remain in the config file for RPC
; (supervisorctl/web interface) to work, additional interfaces may be
; added by defining them in separate rpcinterface: sections
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL  for a unix socket

; The [include] section can just contain the "files" setting.  This
; setting can list multiple files (separated by whitespace or
; newlines).  It can also contain wildcards.  The filenames are
; interpreted as relative to this file.  Included files *cannot*
; include files themselves.

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

各プロセスの設定は /etc/supervisor/conf.d 以下に書きます。section values は Configuration File を参照して下さい。

$ vim /etc/supervisor/conf.d/uwsgi.conf
[program:uwsgi]
directory=/your/project/path
command=/usr/local/bin/uwsgi --ini uwsgi/uwsgi.ini
numprocs=1
autostart=true
autorestart=true
user=root
redirect_stderr=true
stdout_logfile=/var/log/supervisor/uwsgi.log

supervisorctl start でプロセスを起動します。

$ sudo supervisorctl start uwsgi
uwsgi: started
$ sudo supervisorctl status
uwsgi                        RUNNING   pid 7790, uptime 0:00:01

試しに kill して, restart されるか確認してみます。

$ sudo killall uwsgi
$ ps aux | grep uwsgi
root     27901  0.0  2.4  98380 25304 ?        S    23:34   0:00 /usr/local/bin/uwsgi --ini uwsgi/uwsgi.ini

設定変更時は, supervisorctl reload で再起動します。

Anaconda 環境での uWSGI / Supervisor

Anaconda 環境で uwsgi コマンドを実行すると libpcre.so.1 を共有ライブラリを探せないため, 環境変数 LD_LIBRARY_PATH に /opt/conda/lib を指定します。OS は Ubuntu16.04 です。 (2018-12-01 追記)

$ source activate py27
$ uwsgi
uwsgi: error while loading shared libraries: libpcre.so.1: cannot open shared object file: No such file or directory
$ export LD_LIBRARY_PATH=/opt/conda/lib
$ uwsgi --version
2.0.17.1

/etc/supervisor/conf.d 以下のプロセスの設定を以下のように変更します。

[program:uwsgi]
directory=/your/project/path
command=/bin/bash -c "source /opt/conda/bin/activate py27 && /opt/conda/envs/py27/bin/uwsgi --ini uwsgi.ini"
numprocs=1
autostart=true
autorestart=true
user=root
redirect_stderr=true
stdout_logfile=/var/log/supervisor/uwsgi.log

また, Python 実行時に以下のエラーが発生する場合, Ubuntu の /usr/lib/python2.7 の不具合である可能性があります。[6]

ImportError: No module named _sysconfigdata_nd

回避策として /opt/conda/envs/py27/lib/python2.7 へのシンボリックリンクを張ります。

$ sudo ln -s /usr/lib/python2.7/plat-*/_sysconfigdata_nd.py /opt/conda/envs/py27/lib/python2.7

Ubuntu の TimeZone 変更

対話形式で TimeZone を Tokyo に変更します。

$ sudo dpkg-reconfigure tzdata

FlaskでCookieの属性設定

Cookieを扱う際, http と https が混在しているサイトでは http 通信時に Cookie を平文で送信してしまいます。Secure属性を追加することで https 通信時のみ Cookie を送信します。また, HttpOnly属性を追加することで通信時のHTTPヘッダ以外, JavaScript から Cookie にアクセスすることを禁止します。

Flaskでは下記のように設定します。

app.config.update(
	SESSION_COOKIE_SECURE=True,
	SESSION_COOKIE_HTTPONLY=True
)

Chrome developer tools > Resources > Cookies で確認できます。
下記で HTTP が HttpOnly属性, Secure が Secure属性 です。

flask-cookie-secure

余談ですが uWSGI 起動時に if __name__ == “__main__”: の中で app.run()しないと, unable to load configuration from uwsgi というエラーとなります。

Flask logging

logging.handlers.rotatingfilehandler を用いた Flask アプリケーションのログ管理の例です。

import logging
from logging.handlers import RotatingFileHandler

from flask import Flask
from flask import request

app = Flask(__name__)

@app.route('/')
def hello():
    app.logger.info('request args: {0}'.format(request.args))
    return "hello"

if __name__ == '__main__':
    formatter = logging.Formatter(
        "[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s"
    )
    handler = RotatingFileHandler('flask.log',
                                  maxBytes=10000,
                                  backupCount=10)
    handler.setFormatter(formatter)
    handler.setLevel(logging.INFO)
    app.logger.addHandler(handler)
    app.logger.setLevel(logging.INFO)

    app.run()

localhost:5000/?foo=bar にアクセスすると, 以下のようにファイルへのログ出力を確認できます。

$ tail -f flask.log
[2019-09-01 16:06:17,102] {app.py:11} INFO - request args: ImmutableMultiDict([('foo', 'bar')])

[1] Flask-Viewsのmodule化のアイデア
[2] Flask and uWSGI – unable to load app 0 (mountpoint=”) (callable not found or import error)
[3] supervisorctlについて調べてみた
[4] SupervisorでPythonのWebアプリをデーモン化する
[5] PCRE issue when setting up WSGI application
[6] _sysconfigdata_nd.py missing in /usr/lib/python2.7/
[7] Flask logging example