Featured image of post Python Web开发学习(四):使用Docker、Gunicorn和Nginx部署网站

Python Web开发学习(四):使用Docker、Gunicorn和Nginx部署网站

使用Docker、Gunicorn和Nginx部署用Python开发的网站

简介

在本系列的前三篇文章中,我们使用Python的Flask框架开发了一个简单的天气预报网站,到目前为止,我们的网站还仅限于在自己的电脑上访问。在这篇文章中,我们将使用Docker、Gunicorn和Nginx部署这个网站,使得我们的网站可以在公网上访问。

前置条件

项目结构(开发版)

到上一篇文章“Python Web开发学习(三):使用输入框和API”为止,我们的项目结构如下所示:

1
2
3
4
5
6
learn_flask
├── app.py
├── static
│   └── style.css
└── templates
    └── weather.html

在这篇文章中,我们将使用Docker和Gunicorn部署这个网站,因此我们需要添加一些文件。为了方便起见,我们将项目调整为如下所示的结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
learn_flask
├── docker-compose.yml
├── .env
├── .env.db
└── learn_flask
    ├── app
    │   ├── config.py
    │   ├── __init__.py
    │   ├── static
    │   │   └── style.css
    │   └── templates
    │       └── weather.html
    ├── Dockerfile
    ├── manage.py
    └── requirements.txt

接下来我们将逐步创建这些文件,并介绍这些文件的作用。

添加依赖

我们在开发这个网络应用前,已经定义了一个Python虚拟环境,其中包含了我们需要的依赖。我们可以使用如下命令将当前环境中的依赖导出到requirements.txt文件中:

1
pip freeze > requirements.txt

不知道为什么里面会包含一些不需要的依赖,例如clickitsdangerousJinja2MarkupSafeWerkzeug,我们可以手动删除这些依赖。最后,我们的requirements.txt文件如下所示:

1
2
3
4
5
Flask==3.0.0
Flask-SQLAlchemy==3.1.1
Jinja2==3.1.2
gunicorn==20.1.0
psycopg2-binary==2.9.9

将应用包装为Python包

  1. 我们将原来的app.py文件重命名为__init__.py,并将其移动到learn_flask/app目录下。

  2. 然后我们可以在learn_flask/app目录下创建一个config.py文件,用来存储我们的数据库配置信息:

    1
    2
    3
    4
    5
    6
    7
    
    import os
    
    basedir = os.path.abspath(os.path.dirname(__file__))
    
    class Config(object):
        SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite://")
        SQLALCHEMY_TRACK_MODIFICATIONS = False
    

    这样我们可以删去__init__.py中的数据库配置信息:

    1
    2
    
    app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://test:test_password@localhost:5432/weather_db'
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    
  3. 我们在app目录的外面创建一个manage.py文件,用来管理我们的应用程序。我们可以在manage.py中添加如下代码:

     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
    
    from flask.cli import FlaskGroup
    from sqlalchemy.exc import ProgrammingError
    from app import app
    
    cli = FlaskGroup(app)
    
    @cli.command("create_db")
    def create_db():
        from app import db
        from app import Weather
        with app.app_context():
            try:
                db.create_all()
                db.session.commit()
            except ProgrammingError:
                pass
    
    @cli.command("drop_db")
    def drop_db():
        from app import db
        from app import Weather
        with app.app_context():
            try:
                db.drop_all()
                db.session.commit()
            except ProgrammingError:
                pass
    
    if __name__ == "__main__":
        cli()
    

    这里我们使用FlaskGroup来管理我们的应用程序,其中create_dbdrop_db命令用来创建和删除数据库。这里我们使用ProgrammingError来判断数据库是否存在,如果数据库不存在,则不会删除数据库。

    然后我们可以删除__init__.py中的数据库创建和删除的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    
    if __name__ == '__main__':
        with app.app_context():
            try:
                db.create_all()
                db.session.commit()
            except ProgrammingError:
                pass
        app.run()
    

使用Docker部署应用

创建Dockerfile

我们在learn_flask目录下创建一个Dockerfile文件,用来构建我们的应用程序。我们可以在Dockerfile中添加如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# pull official base image
FROM python:3.11.4-slim-buster as builder

# set work directory
WORKDIR /app

# Copy the current directory contents into the container at /app
COPY . /app

# Install any needed packages specified in requirements.txt
COPY ./requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Run app.py when the container launches
CMD ["gunicorn", "-b", "0.0.0.0:5001", "app:app"]

其中各个命令的作用已经标注在了注释中。

创建docker-compose.yml

我们在最外层的learn_flask目录下创建一个docker-compose.yml文件,用来管理我们的应用程序。我们可以在docker-compose.yml中添加如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
version: "3.8"

services:
  learn_flask:
    build: ./learn_flask
    command: python manage.py run -h 0.0.0.0
    volumes:
      - ./learn_flask:/app/
    ports:
      - 5001:5000
    env_file:
      - ./.env
    depends_on:
      - db
  db:
    image: postgres:13
    volumes:
      - ./postgres_data_prod:/var/lib/postgresql/data/
    env_file:
      - ./.env.db

volumes:
  postgres_data:

在这个docker-compose.yml文件中,我们定义了两个服务,一个是应用程序learn_flask,另一个是数据库db

创建.env文件

上面的docker-compose.yml文件中,我们使用了.env文件和.env.db文件,我们可以在最外层的learn_flask目录下创建这两个文件,用来存储我们的环境变量。我们可以在.env文件中添加如下环境变量:

1
2
3
4
5
6
FLASK_APP=app/__init__.py
FLASK_DEBUG=1
DATABASE_URL=postgresql://your_postgre_user_name:your_postgres_password@db:5432/weather_db
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres

.env.db文件中添加数据库的环境变量:

1
2
3
POSTGRES_USER=your_postgre_user_name
POSTGRES_PASSWORD=your_postgres_password
POSTGRES_DB=weather_db

部署应用(开发版)

构建镜像

第一次构建镜像时,我们的应用程序还没有创建数据库,因此我们需要先创建数据库。需要将docker-compose.yml中的command改为如下代码:

1
2
3
4
command: >
    sh -c "python manage.py drop_db &&
    python manage.py create_db && 
    python manage.py run -h 0.0.0.0"    

然后我们可以使用如下命令构建镜像:

1
docker-compose up -d --build

如果一切正常,在浏览器中输入http://localhost:5001,就可以看到我们的网站了。

部署应用

在第一次构建成功后,我们可以暂时停止应用程序,然后将docker-compose.yml中的command改回原来的命令:

1
command: python manage.py run -h 0.0.0.0

然后我们可以使用如下命令部署应用:

1
docker-compose up -d

部署应用(生产版)

根据上面的步骤完成部署后,我们的程序仍然是在开发模式下运行的,查看docker里面的日志,我们可以看到如下的提示信息:

1
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.

这提示我们上面的部署对于生产环境来说是不够的,因此我们还需要再做一些调整,使得我们的应用程序可以在生产环境下运行。

docker-compose

首先我们为生产环境的部署创建一个新的docker-compose.yml文件,我们可以在最外层的learn_flask目录下创建一个docker-compose.prod.yml文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
version: "3.8"

services:
  learn_flask:
    build:
      context: ./learn_flask
      dockerfile: Dockerfile.prod
    command: gunicorn --bind 0.0.0.0:5000 manage:app
    ports:
      - 5001:5000
    env_file:
      - ./.env.prod
    depends_on:
      - db
  db:
    image: postgres:13
    volumes:
      - postgres_data_prod:/var/lib/postgresql/data/
    env_file:
      - ./.env.prod.db

volumes:
  postgres_data_prod:

注意,这里我们不再为应用程序指定volumes,因为我们不再需要将应用程序的代码挂载到容器中,而是将应用程序打包到镜像中。

Dockerfile

上面的docker-compose.prod.yml文件中,我们使用了一个新的Dockerfile.prod文件,我们可以在learn_flask/learn_flask目录下创建一个Dockerfile.prod文件,用来构建我们的应用程序镜像:

 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
###########
# BUILDER #
###########

# pull official base image
FROM python:3.11.3-slim-buster as builder

# set work directory
WORKDIR /usr/src/app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# install system dependencies
RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc

# lint
RUN pip install --upgrade pip
# RUN pip install flake8==6.0.0
COPY . /usr/src/app/
# RUN flake8 --ignore=E501,F401 .

# install python dependencies
COPY ./requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt


#########
# FINAL #
#########

# pull official base image
FROM python:3.11.3-slim-buster

# create directory for the app user
RUN mkdir -p /home/app

# create the app user
RUN addgroup --system app && adduser --system --group app

# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/learn_flask
RUN mkdir $APP_HOME
WORKDIR $APP_HOME

# install dependencies
RUN apt-get update && apt-get install -y --no-install-recommends netcat
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install --upgrade pip
RUN pip install --no-cache /wheels/*

# copy entrypoint-prod.sh
COPY ./entrypoint.prod.sh $APP_HOME

# copy project
COPY . $APP_HOME

# chown all the files to the app user
RUN chown -R app:app $APP_HOME

# change to the app user
USER app

# run entrypoint.prod.sh
ENTRYPOINT ["/home/app/learn_flask/entrypoint.prod.sh"]

entrypoint

上面的Dockerfile.prod文件中,我们使用了一个entrypoint.prod.sh文件,我们可以在learn_flask/learn_flask目录下创建一个entrypoint.prod.sh文件,用来运行我们的应用程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/sh

if [ "$DATABASE" = "postgres" ]
then
    echo "Waiting for postgres..."

    while ! nc -z $SQL_HOST $SQL_PORT; do
      sleep 0.1
    done

    echo "PostgreSQL started"
fi

if [ "$FLASK_DEBUG" = "1" ]
then
    echo "Creating the database tables..."
    python manage.py create_db
    echo "Tables created"
fi

exec "$@"

注意:在创建好entrypoint.prod.sh文件后,我们需要将其权限改为可执行,否则之后运行docker时会报错:

1
chmod +x entrypoint.prod.sh

环境变量

和在开发环境中部署类似,我们需要创建.env.prod.env.prod.db文件,用来存储用于生产环境的环境变量。 我们可以在.env.prod文件中添加如下环境变量:

1
2
3
4
5
6
FLASK_APP=app/__init__.py
FLASK_DEBUG=0
DATABASE_URL=postgresql://your_postgre_user_name:your_postgres_password@db:5432/weather_db_prod
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres

.env.prod.db文件中添加数据库的环境变量:

1
2
3
POSTGRES_USER=your_postgre_user_name
POSTGRES_PASSWORD=your_postgres_password
POSTGRES_DB=weather_db_prod

构建镜像

我们可以使用如下命令构建镜像:

1
sudo docker-compose -f docker-compose.prod.yml up -d --build

这时我们只构建了镜像,数据库还是空的,因此我们需要创建数据库。我们可以使用如下命令在容器中创建数据库:

1
sudo docker-compose -f docker-compose.prod.yml exec learn_flask python manage.py create_db

然后我们可以在浏览器中输入http://localhost:5001,就可以看到我们的网站了。

生产版项目结构

最终我们用于生产环境的目录结构如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
learn_flask
├── docker-compose.prod.yml
├── env.prod
├── env.prod.db
└── learn_flask
    ├── app
    │   ├── config.py
    │   ├── __init__.py
    │   ├── static
    │   │   └── style.css
    │   └── templates
    │       └── weather.html
    ├── Dockerfile.prod
    ├── entrypoint.prod.sh
    ├── manage.py
    └── requirements.txt

使用Nginx反向代理

完成上面的部署后,我们仍然只能在本地访问我们的网站,如果我们想要在公网上访问,我们需要使用Nginx反向代理。由于我的服务器上已经安装了Nginx,因此我只需要在服务器上添加一个配置文件即可。我们可以在服务器上的/etc/nginx/sites-available目录下创建一个learn_flask.conf文件,用来存储我们的配置信息。我们可以在learn_flask.conf文件中添加如下代码:

1
2
3
4
5
6
7
8
server {
    listen 80;
    server_name your_domain_name;

    location / {
        proxy_pass http://localhost:5001;
    }
}

然后我们还可以用acme.sh生成一个SSL证书,用来支持HTTPS。acme.sh的安装和使用可参见个人网站的建立过程(二):使用Hugo框架搭建个人网站

上面的nginx配置文件也需要做一些修改,我们可以在learn_flask文件中添加如下代码:

 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
upstream learn_flask.jinli.io {  
    server 127.0.0.1:5001; 
}

server {
    listen 80;
    server_name  learn_flask.jinli.io;
    return 301 https://learn_flask.jinli.io$request_uri;
}

server {
    listen 443 ssl;
    server_name  learn_flask.jinli.io;

    ssl_certificate /media/lijin/learn_flask/cert/cert.pem;
    ssl_certificate_key /media/lijin/learn_flask/cert/key.pem;

    location / {
        proxy_redirect off;
        proxy_pass http://learn_flask.jinli.io;

        proxy_set_header  Host                $http_host;
        proxy_set_header  X-Real-IP           $remote_addr;
        proxy_set_header  X-Forwarded-Ssl     on;
        proxy_set_header  X-Forwarded-For     $proxy_add_x_forwarded_for;
        proxy_set_header  X-Forwarded-Proto   $scheme;
        proxy_set_header  X-Frame-Options     SAMEORIGIN;

	    client_max_body_size 100m;
    }
}

然后我们可以使用如下命令激活这个配置文件:

1
sudo service nginx reload
Licensed under CC BY-NC-SA 4.0
最后更新于 Nov 22, 2023 00:00 UTC
comments powered by Disqus