部署之前

当我们在本地开发完一个django项目后,如果需要供其他人来访问,我们则必须将其部署到服务器上(一般是linux),而要提供外部服务,则需要服务器能提供静态文件服务动态资源处理等服务,而在我们调试阶段使用的runserver,则无法提供这些功能。

因此,我们需要静态文件服务器(nginx)以及应用服务器(gunicorn、uwsgi服务器)。

这里以nginxgunicorn为例。

先看一张关系图:

如图所示,当浏览器发起一个请求,nginx服务器会先判断,该请求是否需要静态文件(即htmlcssjs等文件),如果需要就直接返回给前端静态文件,如果还需要一些动态生成的数据,那么这个时候nginx表示它自己处理不了了,但是它知道gunicorn服务器能处理,此时,便会将该请求反向代理gunicorn应用服务器上,gunicorn收到该请求后,会将其请求报文(请求头、请求行、请求体)封装好,发送给django框架来处理,django拿到这个请求后,将其封装为HttpRequest类,然后去查找路由表,匹配上路由后,调用对应的视图,再根据请求方法,调用对应的实例方法,来完成对数据的处理。数据处理完成后,将响应结果返回给gunicorngunicorn拿到响应结果将其封装(响应头、响应行、响应体)发送给nginx,最后在由nginx返回到前端展示。

因此,我们需要在linux服务器上安装配置gunicornnginx,这里我们采用docker部署的方式。

部署准备

首先,我们的目标是要准备3个容器:

  • nginx容器:提供前、后端(在线接口文档平台)静态文件服务以及反向代理到应用服务器。
  • gunicorn容器:应用服务器,后端代码需要放在容器中。
  • mysql容器:数据库。

nginx容器

1、修改前端代码

api.js

将后端地址改为服务器地址。

2、生成静态文件

执行命令npm run build,将vue的组件转换为静态js文件,执行之后,会生成一个dist目录:

将该目录拷贝出并放到nginx_docker文件夹下(本地新建一个)。

3、修改后端代码

settings.py

关闭调试模式

修改数据库连接信息,将数据库名改为mysql容器的名字db(因为容器在桥接模式下,可以互相通过名称进行通信)。

image-20210224172713452

执行pip freeze > requirements.txt将项目依赖包导出。

4、收集静态文件

由于需要提供在线接口文档平台,所以还需要收集后端的静态文件。

在项目根目录下创建一个static目录,然后在setting.py中指明static文件配置:

然后命令行执行python manage.py collectstatic收集静态文件,收集成功后,将static目录下的rest_framwork拷贝到步骤2中生成的dist目录下的static目录下,如图:

5、修改nginx配置文件

首先修改nginx业务配置

default.conf

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
# 定义反向代理服务器
upstream app_server {
server django_app:8000;
}

server {
# 提供后端服务
listen 8000;
# 指定后端接口api的域名
server_name 121.5.140.33;

# 接口文件平台
# 指定/static/路由
location /static {
alias /var/www/html/static;
}

# 指定路由条目
location / {
try_files $uri @proxy_to_app;
}

location @proxy_to_app {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://app_server;
}

}

server {
# 提供前端服务
# 指定监听的端口
listen 80;
# 指定使用的域名
server_name 121.5.140.33;

# 指定静态文件的根路径
root /var/www/html;

# 指定日志文件
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log;

# 指定路由条目
location / {
try_files $uri $uri/ /index.html;
}

error_page 404 /404.html;
location = /404.html {
root /usr/share/nginx/html;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}

location ~ /\.ht {
deny all;
}
}

为了保证nginx服务挂掉后能自动重启,所以使用了守护进程supervisord,在supervisord.conf文件中添加监听nginx服务的配置。

supervisord.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[unix_http_server]
file=/tmp/supervisor.sock ; (the path to the socket file)

[supervisord]
logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=5MB ; (max main logfile bytes b4 rotation;default 50MB)
pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
user=root
nodaemon=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0

[rpcinterface:supervisor]
supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface

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

[program:nginx]
command=/usr/sbin/nginx
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stdout_events_enabled=true
stderr_events_enabled=true

nginx主配置文件nginx.conf

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
user  nginx;
worker_processes 1;

error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

events {
worker_connections 1024;
}


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

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;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;

index index.html index.htm;
include /etc/nginx/conf.d/*.conf;

}

将上述三个配置文件放到一个文件夹configs中(在本地新建一个)。

6、编写Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
FROM alpine:latest
LABEL maintainer='AcientOne'
LABEL description='Install nginx'

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories && \
apk update && \
apk add --allow-untrusted ca-certificates bash curl iputils supervisor nginx && \
apk upgrade && \
rm -rf /var/cache/apk/* && \
mkdir /tmp/nginx && \
mkdir -p /var/www/html && \
chown -R nginx:nginx /var/www/html

COPY dist/ /var/www/html/
COPY configs/default.conf /etc/nginx/conf.d/
COPY configs/nginx.conf /etc/nginx/nginx.conf
COPY configs/supervisord.conf /etc/supervisord.conf

VOLUME /var/log/nginx/
EXPOSE 80 8000 443
CMD ["supervisord"]

最后,nginx容器构建所需文件目录如下:

gunicorn容器

1、编写gunicorn_config.py配置文件

gunicorn_config.py

1
2
3
4
5
6
7
8
# 应用服务器监听的端口
bind = '0.0.0.0:8000'
# 修改后端代码后会立即刷新
reload = True
# 注意这里/usr/src/app/logs/目录是和Dockerfile中映射出去的目录有依赖的
pidfile = '/usr/src/app/logs/gunicorn.pid'
accesslog = '/usr/src/app/logs/gunicorn_acess.log'
errorlog = '/usr/src/app/logs/gunicorn_error.log'
2、编写启动脚本docker-entrypoint.sh
1
2
3
4
5
6
7
8
9
10
#!/bin/sh
#set -e之后出现的代码,一旦出现了返回值非零,整个脚本就会立即退出
set -e
# 执行迁移
python manage.py makemigrations
python manage.py migrate
# 创建超级用户
echo "from django.contrib.auth import get_user_model; User = get_user_model(); User.objects.create_superuser('test', 'test@qq.com', 'test123')" | python manage.py shell &> /dev/null
# 启动gunicorn服务,指定gunicorn配置文件和后端项目中的wsgi文件
/usr/local/bin/gunicorn -c /usr/src/app/configs/gunicorn_config.py ancient_test.wsgi
3、编写Dockerfile
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
FROM python:3-alpine
LABEL maintainer='AncientOne'
LABEL description='deploying django project'

# 切换到容器/usr/src/app目录
WORKDIR /usr/src/app
# 将后端项目代码拷贝到容器/usr/src/app/ancient_test/目录下
COPY ./ancient_test ./ancient_test/
# 将gunicorn配置文件拷贝到容器/usr/src/app/configs/目录下
COPY ./gunicorn_config.py ./configs/
# 将启动脚本拷贝到容器/usr/src/app/docker-entrypoint.sh文件中
COPY ./docker-entrypoint.sh /docker-entrypoint.sh
# 需要安装Python依赖包,切换到/usr/src/app/ancient_test/后端项目根目录下
WORKDIR ancient_test/
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories && \
apk update && \
# 安装mysqlclient必须安装mariadb-connector
apk add --allow-untrusted build-base mariadb-connector-c-dev curl iputils && \
pip install --no-cache-dir -i https://pypi.douban.com/simple -r requirements.txt && \
pip install -i https://pypi.douban.com/simple gunicorn && \
apk add ca-certificates bash && \
apk update && apk upgrade && \
# 删除安装包缓存文件
rm -rf /var/cache/apk/* && \
# 修改启动脚本权限
chmod u+x /docker-entrypoint.sh
# 映射出日志目录和项目目录
VOLUME /usr/src/app/logs/
VOLUME /usr/src/app/ancient_test/
# 指定映射端口
EXPOSE 8000
# 容器启动后,运行启动脚本来创建超级用户和启动gunicorn服务
ENTRYPOINT ["/docker-entrypoint.sh"]

最后,gunicorn容器构建所需的文件如下:

然后将DjangoDeploy目录上传到linux服务器上,准备部署。

docker单容器启动

在所有容器构建前,先要创建一个容器桥接网络,让三个容器之间能互相通信。

为什么需要互相通信?

因为三个容器之间的关系是:前端访问–>nginx容器提供该服务(静态文件)–>如果需要数据,则反向代理给gunicorn服务–>gunicorn服务从数据库服务器拿数据。

而由于是桥接网络,所以三个容器之间可以通过容器名称互相访问,这里重命名三个容器的名称为:

  • nginx容器名:web
  • gunicore容器名:django_app
  • mysql容器名:db

需要注意的是,容器之间有依赖关系,所以容器的启动是有顺序的,mysql容器->gunicore容器->nginx容器。

1、执行命令创建网络,网络名为:django_app_net。

1
docker network create django_app_net

2、启动mysql容器。

1
docker run --name db -v mysql_db:/var/lib/mysql --restart=always -e MYSQL_ROOT_PASSWORD=xxxx -e MYSQL_DATABASE=ancient -d --network django_app_net mariadb --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

注:没有mariadb镜像会在容器启动前自动拉取。

若要进入数据库容器内部,执行:

1
docker run -it --network django_app_net --rm mariadb mysql -hdb -uxx -pxx -A ancient

3、构建gunicorn镜像

先进入gunicorn的Dockerfile所在目录下,然后执行构建命令:

1
docker build -t ancientone/django_app:v1 .

4、构建nginx镜像

先进入nginx的Dockerfile所在目录下,然后执行构建命令:

1
docker build -t ancientone/front_end:v1 .

5、启动gunicorn容器

1
docker run --name django_app -v logs:/usr/src/app/logs/ -v django_code:/usr/src/app/ancient_test/ --restart=always -d --network django_app_net -p 8080:8000 ancientone/django_app:v1

6、启动nginx容器

1
docker run --name web -v logs:/var/log/nginx/ -p 8444:80 -p 8440:8000 --restart=always -d --network django_app_net ancientone/front_end:v1

容器启动完成后,分别访问前后端,验证是否正常。

如图,表示前后端已能正常提供访问。

docker-compose批量启动

上面的容器启动方法,需要一个个的去启动,略显繁琐,有没有办法能批量启动呢?当然可以,使用docker-compose

安装docker-compose

分别执行:

1
sudo curl -L "https://github.com/docker/compose/releases/download/1.28.1/docker-compose-(uname -s) -(uname -m)" -o /usr/local/bin/docker-compose
1
sudo chmod +x /usr/local/bin/docker-compose

编写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
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
# 指定版本信息
version: '3'

# 定义服务(容器)
services:
# 具体服务(容器)
# 具体容器名为:docker-compose.yaml所在目录名小写_服务名_索引号
db:
# 指定镜像
image: mariadb
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
# 指定数据持久化映射
volumes:
- mysql_db:/var/lib/mysql
# 容器重启策略
restart: always
# 指定环境变量
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: ancient
# 指定加入的网络
networks:
- django_app_net

django_app:
# 依赖于容器db,db正常启动后才能启动该容器
depends_on:
- db
# 可以使用build来指定Dockerfile所在的目录
build: ./django_app_docker
# 指定自动构建之后的容器名称
image: ancientone/django_app:v1
restart: always
volumes:
- logs:/usr/src/app/logs/
- django_code:/usr/src/app/ancient_test/
networks:
- django_app_net

web:
# 依赖于容器django_app,django_app正常启动后才能启动该容器
depends_on:
- django_app
build: ./nginx_docker
image: ancientone/front_end:v1
restart: always
# 指定端口映射
ports:
# 用于前端
- "8444:80"
# 用于后端接口文档平台
- "8440:8000"
volumes:
- logs:/var/log/nginx/
networks:
- django_app_net

# 指定网络
networks:
# 网络名
django_app_net:
# one_net:

# 指定数据卷
volumes:
mysql_db:
django_code:
logs:

启动

docker-compose.yml文件放到DjangoDeploy目录下,然后执行

1
docker-compose up -d

注:-d表示容器后台运行

启动后,会自动去构建镜像-启动容器,等待它自动执行完即可。

如果要停止所有启动的容器,执行

1
docker-compose down -v

注:-v表示删除数据卷

shell脚本一键启动

继续偷懒,写个shell脚本来一键启动项目。

start.sh

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
#!/bin/bash
#Author: AncientOne
#Description: start run script

# to project directory.
PROJECT_DIR="/root/DeployDjango"

function start_run
{
cd "${PROJECT_DIR}"
docker-compose up -d &> /dev/null
echo -e "构建中,请等待1分钟..."
sleep 1m &> /dev/null
echo -e "开始导入测试数据"
bash import_test_data.sh &> /dev/null
echo -e "构建完成!"
}


function main
{
cd "${PROJECT_DIR}"
docker ps -a | grep -E 'deploydjango_.*' &> /dev/null
if [[ $? -eq 0 ]]
then
echo -e "项目已经启动!"
read -p "要重启项目吗? (y/n) " confirm
if [[ "${confirm,,}" == "n" ]]; then
echo -e "无需重启项目!\nBye!"
exit 0
fi
echo -e "准备重启项目..."

# uninstalled this project
echo -e "正在卸载项目..."
docker-compose down &> /dev/null
echo -e "正在删除数据卷..."
docker volume rm -f `docker volume ls | awk '/deploydjango_.*/{print $2}'` &> /dev/null
echo -e "开始重启项目..."
start_run
else
echo -e "准备启动项目..."
start_run
fi
}

main

import_test_data.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash
#Author: AncientOne
#Description: import test data into db container

container_name="db"
new_container_name="deploydjango_${container_name}_1"
db_username="root"
db_password="root"
db_name="ancient"
docker exec -i ${new_container_name} sh -c "exec mysql -u${db_username} -p'${db_password}' -A ${db_name}" < $PWD/datas/01_tb_projects.sql
docker exec -i ${new_container_name} sh -c "exec mysql -u${db_username} -p'${db_password}' -A ${db_name}" < $PWD/datas/02_tb_interfaces.sql
docker exec -i ${new_container_name} sh -c "exec mysql -u${db_username} -p'${db_password}' -A ${db_name}" < $PWD/datas/03_tb_testcases.sql
docker exec -i ${new_container_name} sh -c "exec mysql -u${db_username} -p'${db_password}' -A ${db_name}" < $PWD/datas/04_tb_configures.sql
docker exec -i ${new_container_name} sh -c "exec mysql -u${db_username} -p'${db_password}' -A ${db_name}" < $PWD/datas/05_tb_testsuits.sql
docker exec -i ${new_container_name} sh -c "exec mysql -u${db_username} -p'${db_password}' -A ${db_name}" < $PWD/datas/06_tb_reports.sql
docker exec -i ${new_container_name} sh -c "exec mysql -u${db_username} -p'${db_password}' -A ${db_name}" < $PWD/datas/07_tb_debugtalks.sql
docker exec -i ${new_container_name} sh -c "exec mysql -u${db_username} -p'${db_password}' -A ${db_name}" < $PWD/datas/08_tb_envs.sql

将上述两个脚本放到部署目录Deploydjango下,直接sh start.sh启动。

大功告成,welldone~