使用Nginx+Gunicorn部署Flask项目

2021-02-26 15:42:59 浏览数 (1)

Flask Web 项目开发完成后,开发人员只是在开发环境运行,只有本地可以访问到项目。如果要让用户访问到项目,需要将项目部署到生产环境上,在服务器运行项目。

本文就使用阿里云服务器(CentOS 7.7 64位)来演示部署一个简单的 Flask 项目。

一、阿里云服务器配置

要将项目部署到服务器上,首先要有服务器。阿里云需要实名认证登录,刚注册时可以领一台免费使用的服务器,试用一个月,如果认证用户是24周岁以下可以买学生服务器,比较便宜,实在不行可以只买一个星期来试用。

拥有服务器后,需要在服务器上配置一些规则,才能完成项目的部署。

1. 登录阿里云,点击用户名旁边的“控制台”,然后在控制台点击左上角的菜单,展开菜单后,再点击“云服务器 ECS”(Elastic Compute Service,简称ECS),进入自己的云服务器界面。当前用户有哪些服务器都会在这里显示。

2. 点击“实例”,然后点击服务器信息右边的“管理”,进入服务器管理界面。

3. 在服务器管理界面点击“本实例安全组”,然后点击右边的“配置规则”,就会进入配置安全组规则的界面(页面的按钮是随阿里云的前端界面变化的,仅供参考)。

4. 如果之前配置过安全组规则,在此页面会保留着,如果没有配置过,就点击右上角的“添加安全组规则”按钮,弹出如下的配置框,然后按照下图的方式配置。

下面是按照 Flask 的默认服务器端口5000配置的,也可以配置多个端口,方法完全一样。

5. 我配置了三个端口,5000,7777,8888,配置成功后,结果如下。

配置完成后,后面部署项目时可以设置配置好的端口作为 Flask Web 项目的访问端口。

二、环境搭建和代码部署

配置好阿里云服务器的访问端口后,服务器上还没有安装项目需要使用的软件和库,也没有项目代码,所以需要搭建好项目运行的环境和部署代码。

1. 安装 MySQL

最好先下载好 MySQL 的安装包,然后使用远程连接工具将安装包上传到服务器,这样会快一点。

可以使用 WinSCP 或 FileZilla,都非常方便,将下载好的安装包(如 mysql-5.7.27-1.el7.x86_64.rpm-bundle.tar)上传到服务器。

然后使用 ssh 连接阿里云服务器,如使用 Xshell ,按以下步骤安装 MySQL 和创建需要的用户和数据库。

解压 MySQL 安装包。

代码语言:javascript复制
tar -xvf mysql-5.7.27-1.el7.x86_64.rpm-bundle.tar

安装 MySQL5.7 。

代码语言:javascript复制
yum install mysql-community-* -y

查询 root 用户默认的密码。

代码语言:javascript复制
grep password /var/log/mysqld.log

使用 root 用户登录 MySQL ,然后修改 root 用户的密码(密码自己设,不用跟我一样)。

代码语言:javascript复制
mysql -u root -p
# 修改root的密码
alter user 'root'@'localhost' identified by 'Mysql!123';

创建需要使用的用户 admin。

代码语言:javascript复制
grant all privileges on *.* to 'admin'@'%' identified by 'Mysql!123';

退出 root 用户,使用 admin 用户登录 MySQL ,创建需要使用的数据库 MyDB_one 。

代码语言:javascript复制
mysql -u admin -p
# 创建 MyDB_one 数据库
create database MyDB_one character set utf8;

安装好 MySQL,并创建好用户和数据库,代码中可以正常连接和使用数据库,数据库就准备好了。

2. 安装 Python3.6

正常项目部署时,最好先安装一个虚拟环境,好让当前项目与其他项目隔离开,运行环境不会相互干扰。不过当前的服务器只有一个项目,后续也不会再部署其他的项目,所以不安装虚拟环境了。

在 CentOS 中默认的 Python 版本是2.7 ,如果后面用 Python2.7 运行代码,最新版本的 gunicorn 是不支持 Python2.7 的,可以指定较旧的版本安装,如 gunicorn==19.9.0 ,新版本的 Flask 使用时也有兼容问题,可以指定较旧的版本安装,如 flask==0.10.1 。

当然,安装 Python3 ,就不用担心兼容问题了。

代码语言:javascript复制
yum install python36 -y

删除 /usr/bin/ 下的 python ,创建一个软连接指向 python3.6 ,这样默认的 Python 版本就是 python3.6 了。

代码语言:javascript复制
rm /usr/bin/python
ln -s /usr/bin/python3.6 /usr/bin/python

3. 安装 Flask 和 Flask-SQLAlchemy

执行如下命令安装 Flask 和 Flask-SQLAlchemy ,如果是 Python2.7 就将 pip3 改成 pip。

代码语言:javascript复制
pip3 install Flask
pip3 install flask-sqlalchemy

因为使用的是 MySQL,要使用 Flask-SQLAlchemy 连接数据库,还要安装 flask-mysqldb 。

代码语言:javascript复制
pip3 install flask-mysqldb

安装时会报如下错误,是因为在 python3.6 中找不到 Python.h ,需要安装 python3-devel 。

代码语言:javascript复制
MySQLdb/_mysql.c:38:20: fatal error: Python.h: No such file or directory
     #include "Python.h"
                        ^
    compilation terminated.
    error: command 'gcc' failed with exit status 1

所以执行命令安装 python3-devel 。

代码语言:javascript复制
yum install python3-devel -y

但是执行后会报如下错。

代码语言:javascript复制
File "/usr/bin/yum", line 30
    except KeyboardInterrupt, e:
                            ^
SyntaxError: invalid syntax

因为 yum 是使用 Python2 实现的,默认的 python 已经被改成 python3.6 了(上面创建软链接时) ,需要将 yum 使用的 Python 版本改回 python2.7 。

代码语言:javascript复制
vim /usr/bin/yum

将第一行的 /usr/bin/python 改为 /usr/bin/python2.7,重新运行还会报如下错误。

代码语言:javascript复制
Downloading packages:
  File "/usr/libexec/urlgrabber-ext-down", line 28
    except OSError, e:
                  ^
SyntaxError: invalid syntax

原因与上面相同,继续修改。

代码语言:javascript复制
vim /usr/libexec/urlgrabber-ext-down

将第一行的 /usr/bin/python 改为 /usr/bin/python2.7 。

然后重新执行 yum install python3-devel -y 和 pip3 install flask-mysqldb 就可以安装成功了,如果还有其他问题,可以使用类似方法解决。

4. 将项目代码部署到服务器上

我准备了一个 ProjectOne 的项目文件夹,项目中包含了一个 Flask 代码文件 flask_project.py ,一个 templates 模板文件夹,在模板文件夹中有一个 flask_project.html 模板文件。

使用 WinSCP 或 FileZilla 将项目代码上传到服务器,使用 tree 命令查看,项目的目录结构如下。

如果没有 tree 命令 ,可以先安装。

代码语言:javascript复制
yum install tree -y

flask_project.py 中的代码如下:

代码语言:javascript复制
# coding=utf-8
from flask import Flask, render_template, request, redirect, flash
from flask_sqlalchemy import SQLAlchemy
import random

app = Flask(__name__)

app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://admin:Mysql!123@127.0.0.1:3306/MyDB_one'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
app.config['SQLALCHEMY_ECHO'] = True
app.config['SECRET_KEY'] = 'NFAIOSDFHASOGHAOSPIGAOWE'
db = SQLAlchemy(app)


class Phone(db.Model):
    __tablename__ = 'Phone_tb'
    pid = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(32))
    person_id = db.Column(db.Integer, db.ForeignKey('Person_tb.mid'))

    def __repr__(self):
        return 'Phone_name: {}'.format(self.name)


class Person(db.Model):
    __tablename__ = 'Person_tb'
    mid = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    age = db.Column(db.Integer)
    phones = db.relationship('Phone', backref='person', lazy='dynamic')

    def __repr__(self):
        return 'Person_name: {}'.format(self.name)


# db.drop_all()
# db.create_all()


@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'GET':
        persons = Person.query.all()
        return render_template('flask_project.html', persons=persons)
    if request.method == 'POST':
        person_name = request.form.get('person')
        if not person_name:
            return redirect('/')
        submit = request.form.get('search')
        if submit == '查询':
            persons = Person.query.filter_by(name=person_name)
            return render_template('flask_project.html', persons=persons)
        submit = request.form.get('add')
        if submit == '添加':
            phone_name = request.form.get('phone')
            person = Person.query.filter_by(name=person_name).first()
            if person:
                if not phone_name:
                    return redirect('/')
                phone = Phone.query.filter(Phone.name == phone_name, Phone.person_id == person.mid).first()
                if phone:
                    flash('数据已存在!')
                else:
                    phone = Phone(name=phone_name, person_id=person.mid)
                    add_data(phone)
            else:
                person = Person(name=person_name, age=random.randint(18, 25))
                add_data(person)
                if not phone_name:
                    return redirect('/')
                phone = Phone(name=phone_name)
                phone.person = person
                add_data(phone)
            return redirect('/')


def add_data(obj):
    try:
        db.session.add(obj)
        db.session.commit()
    except Exception as e:
        print(e)
        db.session.rollback()
        flash("添加失败")


@app.route("/update_person/<id>", methods=['GET', 'POST'])
def update_person(id):
    person_name = request.form.get('person_{}'.format(id))
    if not person_name:
        flash("请输入修改后的人名")
        return redirect('/')
    person = Person.query.get(id)
    if not person:
        flash("人名不存在")
    else:
        person.name = person_name
        try:
            db.session.merge(person)
            db.session.commit()
        except Exception as e:
            print(e)
            db.session.rollback()
    return redirect('/')


@app.route("/update_phone/<id>", methods=['GET', 'POST'])
def update_phone(id):
    phone_name = request.form.get('phone_{}'.format(id))
    if not phone_name:
        flash("请输入修改后的手机")
        return redirect('/')
    phone = Phone.query.get(id)
    if not phone:
        flash("手机不存在")
    else:
        phone.name = phone_name
        try:
            db.session.merge(phone)
            db.session.commit()
        except Exception as e:
            print(e)
            db.session.rollback()
    return redirect('/')


@app.route("/delete_person/<id>")
def delete_person(id):
    person = Person.query.get(id)
    if not person:
        flash("人名不存在")
    else:
        try:
            Phone.query.filter(Phone.person_id == person.mid).delete()
            db.session.delete(person)
            db.session.commit()
        except Exception as e:
            print(e)
            db.session.rollback()
    return redirect('/')


@app.route("/delete_phone/<id>")
def delete_phone(id):
    phone = Phone.query.get(id)
    if not phone:
        flash("手机不存在")
    else:
        try:
            db.session.delete(phone)
            db.session.commit()
        except Exception as e:
            print(e)
            db.session.rollback()
    return redirect('/')


if __name__ == '__main__':

    # per_one = Person(name='You', age=18)
    # per_two = Person(name='Me', age=81)
    # per_three = Person(name='JackMa', age=60)
    # per_four = Person(name='Panshiyi', age=50)
    # per_five = Person(name='DingLei', age=40)
    # db.session.add_all([per_one, per_two, per_three, per_four, per_five])
    #
    # phone_one = Phone(name='IPhone', person_id=1)
    # phone_two = Phone(name='Mi', person_id=3)
    # phone_three = Phone(name='NOKIA', person_id=2)
    # phone_four = Phone(name='HUAWEI', person_id=4)
    # phone_five = Phone(name='OPPO', person_id=5)
    # phone_six = Phone(name='VIVO', person_id=1)
    # db.session.add_all([phone_one, phone_two, phone_three, phone_four, phone_five, phone_six])
    # db.session.commit()

    app.run(debug=True)

flask_project.html 中的代码如下:

代码语言:javascript复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Person</title>
</head>
<body>
<form method="post">
    <label>人名:</label> <input id="person" name="person" type="text" value=""><br/>
    <label>手机:</label> <input id="phone" name="phone" type="text" value=""><br/><br/>
    <input id="search" name="search" type="submit" value="查询">
    <input id="add" name="add" type="submit" value="添加"><br/>
</form>

<br/>
{% for message in get_flashed_messages() %}
    {{ message }}
{% endfor %}

<ul>
     {% for person in persons %}
        <li>{{ person.name }} <a href="/delete_person/{{ person.mid }}">删除</a>
        <form method="post" action="/update_person/{{ person.mid }}"><input id="person_{{ person.mid }}" name="person_{{ person.mid }}" type="text" value=""> <input id="update_{{ person.mid }}" name="update_{{ person.mid }}" type="submit" value="修改"></form></li>
        <ul>
        {% for phone in person.phones %}
            <li>{{ phone.name }} <a href="/delete_phone/{{ phone.pid }}">删除</a>
            <form method="post" action="/update_phone/{{ phone.pid }}"><input id="phone_{{ phone.pid }}" name="phone_{{ phone.pid }}" type="text" value=""> <input id="update_{{ phone.pid }}" name="update_{{ phone.pid }}" type="submit" value="修改"></form></li>
        {% else %}
            <li>无</li>
        {% endfor %}
        </ul>
    {% endfor %}
</ul>

</body>
</html>

5. 创建数据表和添加数据

将代码中 db.drop_all() 和 db.create_all() 的注释取消,将添加数据的代码注释也取消,然后 python flask_project.py 运行代码,会在数据库中创建两张数据表 Person_tb 和 Phone_tb ,并分别在两张表中添加几条数据。

同时,程序会自动运行在 127.0.0.1:5000 上,开启监听。先将程序停掉。

停掉之后,如果 5000 端口还被占用着,可以 kill -9 thread id 将进程关掉。

添加成功数据后,注释掉代码中的 db.drop_all() 和 db.create_all() ,将添加数据的代码也全部注释掉。

三、Nginx 安装和配置

1. 安装 Nginx

在 CentOS 中安装 Nginx 很简单,使用如下命令即可。

代码语言:javascript复制
yum install nginx -y

安装成功后,nginx 还没有启动,要先开启 nginx 服务。

代码语言:javascript复制
systemctl start nginx

然后使用 ps 命令查看 nginx 服务是否成功开启。

代码语言:javascript复制
ps -ef | grep nginx

开启 nginx 成功后,在 windows 浏览器上访问服务器的 80 端口(阿里云上已经配置好80端口了,访问 ip:port,ip是服务器ip,port默认就是80),页面如下,说明 nginx 安装和开启成功。

2. 修改 Nginx 配置

nginx 已经安装成功了,但 nginx 默认监听的是80端口,要部署自己的项目,还需要修改配置,增加监听端口和路由转发规则。

nginx 的配置文件是 /etc/nginx/ 下的 nginx.conf ,vim nginx.conf 修改配置文件。

默认 user 是 nginx 用户(需要先创建这个用户才行),先将 user 修改为 root (实际项目中一般不会使用 root) ,然后参照默认监听80端口的配置增加一份 server 配置。这份配置是监听7777端口,这个端口在阿里云上配置好了,当服务器监听到7777端口的请求时,会将请求转发到 127.0.0.1:5000/ (服务器本地运行的Flask项目)。

代码语言:javascript复制
    server {
        listen       7777;
        listen       [::]:7777 default_server;
        server_name  flask_project;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        location / {
            proxy_pass http://127.0.0.1:5000/;
        }
    }

增加后的完整 nginx.conf 如下。

代码语言:javascript复制
# For more information on configuration, see:
#   * Official English Documentation: http://nginx.org/en/docs/
#   * Official Russian Documentation: http://nginx.org/ru/docs/

user root;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;
    server {
        listen       80 default_server;
        listen       [::]:80 default_server;
        server_name  _;
        root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        location / {
        }

        error_page 404 /404.html;
            location = /40x.html {
        }

        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }
    }
    server {
        listen       7777;
        listen       [::]:7777 default_server;
        server_name  flask_project;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        location / {
            proxy_pass http://127.0.0.1:5000/;
        }
    }

# Settings for a TLS enabled server.
#
#    server {
#        listen       443 ssl http2 default_server;
#        listen       [::]:443 ssl http2 default_server;
#        server_name  _;
#        root         /usr/share/nginx/html;
#
#        ssl_certificate "/etc/pki/nginx/server.crt";
#        ssl_certificate_key "/etc/pki/nginx/private/server.key";
#        ssl_session_cache shared:SSL:1m;
#        ssl_session_timeout  10m;
#        ssl_ciphers HIGH:!aNULL:!MD5;
#        ssl_prefer_server_ciphers on;
#
#        # Load configuration files for the default server block.
#        include /etc/nginx/default.d/*.conf;
#
#        location / {
#        }
#
#        error_page 404 /404.html;
#            location = /40x.html {
#        }
#
#        error_page 500 502 503 504 /50x.html;
#            location = /50x.html {
#        }
#    }

}

修改完成配置文件后需要重启 nginx ,使配置生效。

代码语言:javascript复制
systemctl restart nginx

四、Gunicorn 安装和配置

在运行 Flask 程序时,默认使用的是 Flask 的 runserver 服务器,现在直接 python flask_project.py 运行 Flask 项目,在 windows 上用浏览器访问 http://120.77.235.113:7777/ (我使用的阿里云服务器ip是120.77.235.113,记得换成自己部署的ip)就可以正常访问到 Flask 项目了。

但是,runserver 只是一个供开发者调试的微型服务器,实际部署时不会这样使用。

通常使用的 HTTP 服务器有 Gunicorn 或 uWsgi ,两个都是满足 Python WSGI 协议的HTTP服务器。使用 uWsgi 需要再配置一份 uWsgi 的配置文件,使用 Gunicorn 会简单些,直接用命令运行代码就可以了,接下来就介绍 Gunicorn 的部署方法。

先安装 Gunicorn 。

代码语言:javascript复制
pip3 install gunicorn

然后使用如下命令运行 Flask 服务器。

代码语言:javascript复制
gunicorn -w 1 -b 127.0.0.1:5000 flask_project:app

-w 表示启动的进程数量,-b 表示服务运行的 ip 和端口,与 nginx 配置文件中转发的地址保持一致,后面跟启动文件和 Flask 实例名称,启动文件不带后缀 .py,与实例名称中间用冒号连接。如果需要以守护进程运行项目的话,再加一个 -D 参数,关于 gunicorn 的更多参数,可以使用 -h 查看帮助信息。

运行之后,(如果需要的话)可以查看 gunicorn 是否开启成功,也可以查看服务器是否在监听 7777 和 5000 端口。

代码语言:javascript复制
ps -ef | grep gunicorn
netstat -ntlp

现在,项目运行起来了,在 windows 上访问 http://120.77.235.113:7777/ ,功能正常,部署成功。

0 人点赞