简介
在本系列的前三篇文章中,我们使用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
|
不知道为什么里面会包含一些不需要的依赖,例如click
、itsdangerous
、Jinja2
、MarkupSafe
、Werkzeug
,我们可以手动删除这些依赖。最后,我们的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包
-
我们将原来的app.py
文件重命名为__init__.py
,并将其移动到learn_flask/app
目录下。
-
然后我们可以在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
|
-
我们在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_db
和drop_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
|
然后我们可以使用如下命令部署应用:
部署应用(生产版)
根据上面的步骤完成部署后,我们的程序仍然是在开发模式下运行的,查看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
|