Django 架设 Restful API(七)项目部署:构建docker容器

1. 部署准备

1.1 测试和生产环境隔离

参见:https://docs.djangoproject.com/zh-hans/3.1/howto/deployment/checklist/#critical-settings

django settings.py中有几个设置量通常要区分测试环境和生产环境:

(1)DEBUG

生产环境下,必须将DEBUG标志量设置为False,否则可能引起重大安全问题,这是因为开启debug时,如果遇到错误会在前端网页显示详细的堆栈信息因此在settings.py中这样修改:

import socket
DEBUG = False

if socket.gethostname() == '本机名称':
    DEBUG = True
else:
    DEBUG = False

即默认将DEBUG设置为False,当django项目运行的主机为开发环境运行的机器时,开启调试。

(2)ALLOWED_HOSTS

这个字段指定django项目允许服务的域名或者主机IP,当DEBUG=False时,必须指定这个字段,否则django不会工作。在settings.py中修改:

import socket
DEBUG = False

if socket.gethostname() == '本机名称':
   ....
   ALLOWED_HOSTS = []
else:
   .....
    # 获取允许访问主机环境变量
    host_list = os.environ.get('DJANGO_ALLOWED_HOSTS')
    # 变量检查
    if host_list is None:
        raise Exception('ALLOWED_HOSTS is invalid.')
    # 分割成列表
    ALLOWED_HOSTS = str(host_list).strip().split(',')

测试环境中使用默认配置即可,生产环境允许主机列表从环境变量获取,在docker容器构建时指定,方便修改。如果不知道开发环境的本机名称,可以运行print(socket.gethostname())查看。

(3)DATABASES

这个字段指定数据库配置,生产环境数据库和测试环境数据库应该要加以区分,修改成如下:

database_settings.py

# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
# 生产环境
import os
PRODUCT_DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': os.environ.get('MY_DATABASE_NAME'),
        'USER': os.environ.get('MY_DATABASE_USER'),
        "PASSWORD": os.environ.get('MY_DATABASE_USER_PASSWORD'),
        HOST": os.environ.get('MY_DATABASE_HOST'),
        "OPTIONS": {
            # sql 模式
            'sql_mode': 'STRICT_ALL_TABLES',
            # 隔离级别
            'isolation_level': 'read committed',
        }
    }
}
# 测试环境
DEBUG_DATABASE = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': "debug_database",
        'USER': "debug_database_user",
        "PASSWORD": "12345",
        "OPTIONS": {
            # sql 模式
            'sql_mode': 'STRICT_ALL_TABLES',
            # 隔离级别
            'isolation_level': 'read committed',
        }
    }
}

注意

为了数据安全,生产环境的数据库用户和密码不能和调试环境的数据库用户和密码相同,其次,生产环境的数据库用户和密码不在代码中明文保存,而是设置成docker容器的系统环境变量,在容器构建时自动设定,这样可以避免源码泄露导致数据库连接密码泄露以及快速更换数据库连接密码,提升数据安全性。

然后在settings.py中引入:

from .database_settings.database_settings import DEBUG_DATABASE, PRODUCT_DATABASES
DEBUG = False

if socket.gethostname() == '本机名称':
   ....
   DATABASES = DEBUG_DATABASE
else:
   .....
   DATABASES = PRODUCT_DATABASES

(4)CACHES

若使用了缓存,开发环境的连接参数和生产环境可能不同,因此应该加以区分,修改成如下:

cache_settings.py

import os

PRODUCT_CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": os.environ.get('MY_REDIS_LOCATION'),
        'TIMEOUT': 3600,
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "PASSWORD": os.environ.get('MY_REDIS_PASSWORD'),
            'MAX_ENTRIES': 1000
        }
    }
}

DEBUG_CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379",
        'TIMEOUT': 3600,
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "PASSWORD": "mypassword",
            'MAX_ENTRIES': 1000
        }
    }
}
from .cache_settings.cache_settings import DEBUG_CACHES, PRODUCT_CACHES
DEBUG = False

if socket.gethostname() == '本机名称':
    .......
    CACHES = DEBUG_CACHES
else:
    ......
    CACHES = PRODUCT_CACHES

注意

缓存数据库的连接位置LOCATION字段和连接密码PASSWORD同样通过容器的环境变量指定,一个是方便修改,另一个是能提高缓存数据安全性。

(5)SECRET_KEY

settings.py中有一个SECRET_KEY字段,这个值用来给敏感数据进行签名,防范数据篡改的风险,必须保证这个值的安全。为了防止源码泄露导致数字签名密钥也泄露,因此将该值指定为my_django容器的环境变量,修改settings.py如下:


if socket.gethostname() == '本机名称':
    ......
    SECRET_KEY = 'debug_secret_key'

else:
    ......
    # SECURITY WARNING: keep the secret key used in production secret!
    SECRET_KEY = os.environ.get('SECRET_KEY')

if SECRET_KEY == 'debug_secret_key' and DEBUG is False:
    # 安全检查
    raise Exception('You must change the SECRET_KEY to the production one.')

密钥的值将保存在容器构建文件中, 在容器构建时自动设定。

1.2 创建项目目录

在服务器(这里使用的是Ubuntu)上创建我们的项目文件夹:

mkdir /var/www/my_backend

1.3 pycharm 自动部署配置

打开pycharm部署配置窗口:

然后点击SSH Configuration,配置服务器SSH连接选项:

Host:主机ip,如192.168.34.200(瞎编的ip)。

User name:云主机的用户,可以选root或者其他自己创建的用户。

Authentication Type:SSH 认证类型,一般选择密码或者密钥对。

password: SSH密码。

填完后点击Test Connection 测试是否成功连接主机。接下选择源代码存放的路径,点击Root path 右边的文件夹图标,然后选择源代码存放目录为上边创建的文件夹/var/www/my_backend

对于web server URL 直接填上http://+主机ip就行。

然后还需做目录映射,直接填上根路径就行:

Add New Mapping后直接点击ok,不用理会警告信息。勾选Automatic Upload设置自动上传,开发时Ctrl+S保存时就会自动上传。

然后点击Exclued Paths设置文件上传排除目录,一般排除本地测试文件存储的目录。

1.4 部署检查

部署前在manage.py所在目录下运行这个命令,检查django部署设置是否正确和安全:

# django 部署检查
python manage.py check --deploy

 

2.容器化部署准备

2.1 容器化的意义

首先回答一个问题,容器化的意义是什么,为什么不直接部署在云服务器下,而是要使用容器部署?根据我的理解,有以下几个有点原因:

(1)项目容易移植

django 应用想要在生产环境中提供服务,除了django应用程序,还得配合redismysqlasgi或者wsgiNginx等软件才能运行,通常这些软件是各自独立的,如果想要移植项目到另一台机器,就需要把部署的流程重新走一遍,更糟糕的是,也许还得拷贝数据,耗时耗力容易出错,使用docker后,直接打包成docker镜像发送给另一台机器就行。

(2)测试和生产环境无缝衔接

实际项目中,测试环境和生产环境必须隔离开,以免生产环境数据意外损坏或者泄露,为了检测其中的bug,线上测试环境往往需要无限逼近生产环境,这意味着需要配置一个和生产环境几乎雷同的测试环境,即得把创建生产环境的流程在走一遍,同样耗时费力。使用容器化部署后,直接从生产环境导出一个镜像,剔除敏感数据后(真实用户信息等),就可以得到一个和生产环境几乎相同的测试环境,免去测试环境配置,同时也实现了线上测试环境和生产环境的隔离。测试完成后,删除或者关闭这个测试镜像,不需要做任何的复原操作,更安全。

(3)弹性部署更快速

大型项目通常是由一个服务器集群提供服务的,其中往往会出现有多台服务器提供同样服务的情况。使用容器化部署,需要的时候可以将相同的服务快速拷贝到多台服务器上并运行起来,不需要直接保存镜像并关停服务即可,不需要过多的复原或者备份工作,容器化在应对访问峰值或者进行主备份服务快速切换时特别有用。

(4)协同开发更方便

以往的开发中虽然有了git等版本控制系统,但是常常代码不可直接运行起来,还需要安装依赖,交流不方便。尤其是开发人员和测试人员之间交流时,双方都需要能够运行项目,不采用容器化部署时,开发人员对于代码的任何修改或者更新,测试人员都需要随之修改自己测试环境的代码,产生了不必要的文档编写工作和时间消耗。使用容器化部署后,开发人员可以直接打包成镜像给测试人员,测试人员可以直接把项目运行起来,不需要再做配置。

(5)解决应用配置冲突

比如在一台服务器上部署两个django应用,一个使用django 2.2开发,一个使用django 3.0开发,这时候两个应用之间可能因为django版本不同产生依赖或者配置冲突,使用docker部署后,两个项目之间是相互隔离的,完全不会冲突。

2.2 docker 安装

docker使用参见:https://www.runoob.com/docker/ubuntu-docker-install.html

由于我使用的是Ubuntu服务器,因此这里安装的是Ubuntu下的版本:

curl -sSL https://get.daocloud.io/docker | sh

安装完成输入命令docker看是否会出现以下信息:

如果出现就说明安装成功了。

2.3 ASGI 托管django应用

 

Django是一个需要Web服务器来运行的Web框架,web服务器最重要的角色就是自动根据请求运行对应的python代码。然而由于大多数Web服务器不是用Python编写,我们需要一个接口来实现djangoweb服务器通信。Django现在支持两种接口服务器:WSGIASGIWSGI是主要的应用程序和Web服务器通信的Python接口,但是它只支持同步代码。而ASGI支持异步运行代码,可以方便的利用pythondjango异步特性,例如websocket。因为该项目会用到异步服务(后续的异步邮件发送等服务),因此这里使用ASGI部署django应用。官方推荐了三种ASGI服务器来托管django项目,这里采用Uvicorn托管django项目。

Uvicorn文档

https://www.uvicorn.org/#django-channels

django部署参考

https://docs.djangoproject.com/zh-hans/3.1/howto/deployment/

gunicorn 文档

https://docs.gunicorn.org/en/stable/,注意gunicorn只能在linux/Unix下运行。

gunicorn nginx部署配置

https://docs.gunicorn.org/en/stable/deploy.html

使用Hey来检查gunicorn代理配置是否正常:

https://github.com/rakyll/hey

安装uvicorn和gunicorn

pip install uvicorn gunicorn uvloop httptools mysqlclient

启动django应用的命令,这个命令不直接运行,而是被集成到python容器的dockerfile中,在容器启动时自动运行

gunicorn mybackend.asgi:application -w 4 -k uvicorn.workers.UvicornWorker

mybackend是你的项目名称。它将开启4个工作进程,监听127.0.0.1:8000应该在与manage.py文件相同的路径中运行这个命令

2.4 容器环境配置

问题

docker只是一个容器引擎,为了运行django项目,还需要在其中安装镜像,容器就是运行中的镜像。django项目通常要组合Nginxmysqlredisasgi或者wsgi容器运行,那么是在一个容器中安装这些软件,还是分别为每个应用构建一个容器再将它们组合运行?这需要权衡:

1.单容器应用程序:在一个容器内安装所有软件可以实现完全隔离,但是可能出现软件重复安装,浪费存储空间,更新也不方便,这种方式和使用虚拟机是一样效果。

2.多容器应用程序:web服务器,数据库和缓存使用单独容器运行。构建多个容器组合运行可以有效利用存储空间,因为docker镜像通常很小,且一个镜像可以生成多个容器,也可以使用docker快速更新容器,但是得小心处理容器之间的依赖关系和启动顺序(例如数据库服务要在django应用之前启动),需要编写多个dockerfile。

本次项目采用第2种模式,即多容器应用程序模式,使用docker-compose进行管理。

3.构建docker容器

3.1 概述

(1) 各个容器作用

python 容器:执行django项目代码。

nginx 容器:提供静态资源(网页、图片等)访问服务。

mysql容器:数据存储服务。

redis 容器:数据缓存服务。

容器使用理念

Docker的设计理念是在容器里面不运行后台服务容器本身就是宿主机上的一个独立的主进程,也可以间接的理解为容器里运行服务的应用进程。一个容器的生命周期是围绕这个主进程存在的,所以正确的使用容器方法是将里面的服务运行在前台,即不推荐使用在容器中使用systemd等工具后台运行服务

(2)多个容器之间相互通信

容器之间通信参见

https://my.oschina.net/thinwonton/blog/2993309

 使用 docker network来创建一个桥接网络,在docker run的时候通过--network参数将容器连接到新创建的桥接网络中,这样同一桥接网络中的容器就可以通过互相访问。查看已有桥接网络:

docker network ls

创建桥接网络

docker network create my_network

启动容器时,加入创建的网络示例:

​docker run -itd -p 8000:8000  --network my_network --name kaopu_python kaopu_python:1.0  /bin/bash

注意

网络连接将由docker-compose在容器构建时自动设定,不需要用命令行设定。

(3)进入容器

进入容器的方式有docker attach和exec,推荐使用下面的方式进入容器,因为使用exec进入容器,执行exit退出容器后,容器不会暂停,如果使用attach进入容器,退出时容器将会暂停。例如进入my_mysql容器:

docker  exec -it my_mysql bash

(4)健康检查

参考:https://docs.docker.com/engine/reference/builder/#healthcheck

健康检查指令将在docker-compose.yml中配置,容器运行时健康检查脚本将会定期运行。

健康检查的目的是确保容器内我们需要的服务正确运行。健康检查命令状态码为0表示健康,为1表示不健康,只能使用这两种执行状态码。如果健康检查不通过,可以运行:

docker inspect my_redis

查看Health字段记录的健康检查输出以确定错误,例如上述命令的示例输出:

(5)分层构建 

创建镜像的时候,分层可以让docker只保存我们添加和修改的部分内容。其他内容则基于基础镜像,不需要存储,这样当我们创建多个镜像的时候,所有的镜像共享基础镜像部分,可以节省磁盘空间。

3.2 拉取容器基础镜像

从docker仓库拉取mysql最新基础镜像

docker pull mysql:latest

nginx、redis和python基础镜像也是使用上述命令拉取。

3.3 编写容器dockerfile和配置文件

所有容器的dokcerfile都保存在和manage.py同一目录下

在manage.py所在目录下创建一个deployment文件夹,并创建相应的子目录,保存各个容器的部署配置文件:

3.3.1 my_mysql容器

(1)构建容器的my_mysql.dockerfile

# my_mysql 容器
FROM mysql:latest
# 维护者信息
MAINTAINER anbuqi <1111@qq.com>
# 复制自定义数据库设置到配置目录
COPY ./deployment/mysql/my_mysql.cnf /etc/mysql/conf.d
# 复制健康检查脚本到容器内
COPY ./deployment/mysql/my_mysql_health_check.sh /my_mysql/my_mysql_health_check.sh
# 授予执行权限
RUN chmod +x /my_mysql/my_mysql_health_check.sh

:使用COPY命令复制文件时,如果容器中的目录不存在,会自动创建。

(2)mysql配置文件my_mysql.cnf

[mysqld]
# 版本兼容性问题,django推荐的mysql连接件mysqlclient 暂不支持caching_sha2_password,改成密码认证
default_authentication_plugin=mysql_native_password
# 允许远程连接
bind-address           = 0.0.0.0

(3)mysql容器健康检查脚本my_mysql_health_check.sh

#!/bin/sh
# 运行查看数据库版本
value=$(mysql -u root "-p$MYSQL_ROOT_PASSWORD" -e "select version();" >/dev/null 2>&1)
# shellcheck disable=SC2181
# 检测查看mysql数据库版本的命令退出状态码
if [ $? -ne 0 ]; then
  echo "ERROR! my_mysql is not running."
  exit 1
else
  echo "Ok,my_mysql is running,now!"
  exit 0
fi

健康检查的原理是运行mysql查看数据库版本的命令,然后检查退出命令的状态码(即ubuntu下的$?环境变量),为0表示命令mysql命令执行成功,服务健康。命令中的mysql连接密码环境变量将在容器构建时自动设定。配置文件和健康检查脚本保存在deployment/mysql/文件夹下。

(4)注意

如果在windows平台下进行开发,编写bash shell脚本时必须在pycharm中设置文件换行符遵从unix/mac风格,否则会出现语法错误,具体设置如下:

3.3.2 my_redis容器

(1)配置文件模板redis.conf

Redis可以在不使用内置默认配置的配置文件的情况下启动,但是建议仅将此设置用于测试和开发目的。配置Redis的正确方法是提供一个Redis配置文件,通常称为redis.conf,这个配置文件可以到官网下载:

redis配置文件下载

https://redis.io/topics/config

选择对应的版本

查看版本:

redis-cli -v

大部分配置使用默认值即可,个人认为,可能需要修改的配置如下:

  • timeout:连接空闲多少时间被关闭,这个参数可以避免出现大量空闲连接,导致缓存性能下降。对于本项目,只有一个连接,即来自django容器,为了提高性能,timeout保持默认值0,即始终保持连接;对于存在多个客户端的情况,通常要设置这个值。

因为配置文件很长,因此这里不再展示。

注意

1.为了能够让redis使用这个配置文件,redis要从该配置文件所在位置启动,即:

redis-server /path/to/redis.conf

这在docker-compose.yml中设定,在容器启动时自动运行。

2.启动路径下拓展名为conf的文件会被认为是配置文件加以读取。

(2)构建容器的my_redis.dockerfile

FROM redis:latest
# 维护者信息
MAINTAINER anbuqi <1230@qq.com>
# 复制健康检查脚本到容器内
COPY ./deployment/redis/my_redis_health_check.sh /my_redis/my_redis_health_check.sh
# 复制配置文件
COPY ./deployment/redis/redis.conf /my_redis/redis.conf
# 授权健康检查脚本执行权限
RUN chmod +x /my_redis/my_redis_health_check.sh

(3)容器健康检查脚本my_redis_health_check.sh

#!/bin/bash
# 试探redis列出键命令是否运行成功
keys=$(echo "KEYS *" | redis-cli -a "$REQUIREPASS")
# shellcheck disable=SC2181
if [ $? -ne 0 ]; then
  echo "ERROR! my_redis is not running"
  exit 1
else
  echo "Ok,my_redis is running,now!"
  exit 0
fi

命令中的redis连接密码环境变量MY_REQUIREPASS将在容器构建时自动设置,这个脚本由docker-compose自动运行。

3.3.3 my_nginx 容器

Nginx 配置参考:

https://www.nginx.com/resources/wiki/start/

官网针对django的教程

https://www.nginx.com/resources/wiki/start/topics/examples/djangofastcgi/

(1)使用模板动态生成nginx配置文件

为了能够方便的修改nginx容器设置,我们可以使用envsubst命令通过一个通用模板来动态生成nginx配置文件。使用环境变量指定以下字段:

  • upstream里的server列表:这样方便增减和修改后端节点。
  • listen的端口
  • server_name:方便修改域名。
  • ssl_certificatessl_certificate_key :方便替换ssl证书。
  • location /media/location /static/里的root:方便更换媒体文件和静态文件存储位置。

为了和普通的nginx变量区别以及使用通配符遍历所有要替换的变量,每个环境变量名要加个自定义前缀,例如加个my前缀:my_server_name。

因为环境变量可能会很多,为了保持docker-compose.yml文件整洁,用一个文件保存这些环境变量,在docker-compose.yml设置env_file字段指定该文件位置自动读取环境变量,参见:https://docs.docker.com/compose/compose-file/compose-file-v3/#env_file。

my_nginx_env_file

# 域名
MY_NGINX_SERVER_NAME='www.example.com example.com'
# 健康检查使用的主机名
MY_NGINX_HEALTH_CHECK_HOST='www.example.com'
# 证书路径相对于/etc/nginx
MY_NGINX_SSL_CERT='cert/kaopurui.cn.pem'
MY_NGINX_SSL_CERT_KEY='cert/kaopurui.cn.key'
# 静态和媒体文件存储位置
MY_NGINX_MEDIA_LOCATION='/kaopu_backend_do_not_delete'
MY_NGINX_STATIC_LOCATION='/kaopu_backend_do_not_delete'

配置文件模板my_nginx.conf.template

upstream my_backend{
  #后端节点列表
  ${MY_NGINX_SERVER_LIST}
}
map $http_upgrade $connection_upgrade {
   default upgrade;
   '' close;
}
#django web应用
server {
       listen 443 ssl;   #SSL协议访问端口号为443,此处如未添加ssl,可能会造成Nginx无法启动。
       server_name ${MY_NGINX_SERVER_NAME};  #将localhost修改为您证书绑定的域名,例如:www.example.com。

       root html;
       index index.html index.htm;

       client_max_body_size 1G;

       ssl_certificate ${MY_NGINX_SSL_CERT};   #将domain name.pem替换成您证书的文件名。
       ssl_certificate_key ${MY_NGINX_SSL_CERT_KEY};   #将domain name.key替换成您证书的密钥文件名。
       ssl_session_timeout 5m;
       ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;  #使用此加密套件。
       ssl_protocols TLSv1 TLSv1.1 TLSv1.2;   #使用该协议进行配置。
       ssl_prefer_server_ciphers on;

       location / {
            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_set_header X-Real-IP $remote_addr;
            # we don't want nginx trying to do something clever with
            # redirects, we set the Host: header above already.
            proxy_redirect off;
            #  Comet, Long polling, or Web sockets, you need to turn off the proxy buffering
            proxy_buffering off;
            # upstream
            proxy_pass http://kaopu_backend;
        }
        # nginx连接健康检查
        location /health_check/{
            # 不在连接日志中记录健康检查访问
            access_log off;
            return 200;
        }
         #媒体文件保存位置
         location /media/{
           root ${MY_NGINX_MEDIA_LOCATION};
         }
         #静态文件保存位置
         location /static/{
           root ${MY_NGINX_STATIC_LOCATION};
         }
 }
# 重定向http到https
server{
  listen        80;
  server_name   ${MY_NGINX_SERVER_NAME};
  return        301 https://$server_name$request_uri;
}

(2)生成配置文件的脚本

echo -e "Use\033[34m nginx\033[0m conf template to generate a conf file.It will be put in \033[34m /etc/nginx/conf.d\033[0m."
# 要置换的环境变量字符串
env_str=""
# 提示信息
hint_message=""
# 按照特定前缀输出环境变量到文件中
env|grep "MY_NGINX_*" > my_nginx_env_file.txt
# 按行读取环境变量文件
while read -r line; do
  # 使用=分割字符串,截取环境变量名
  temp="\${${line%%=*}}"
  # 拼接环境变量字符串
  env_str="${env_str},${temp}"
  # 拼接提示信息
  hint_message="${hint_message}\n${temp}"
done <my_nginx_env_file.txt
# 生成配置文件
envsubst "echo ${env_str} | sed 's/_/ /g'" </etc/nginx/conf.d/my_nginx.conf.template >/etc/nginx/conf.d/my_nginx.conf
# 输出所有置换的环境变量列表
echo -e "These environment variable will be used in nginx conf file:\033[34m ${hint_message}\033[0m"

 

(3)容器构建文件my_nginx.dockerfile

# my_nginx 容器
# 基础镜像
FROM nginx:latest
# 维护者信息
MAINTAINER anbuqi <123450@qq.com>
# 复制ssl证书文件
COPY ./deployment/nginx/cert/example.cn.key /etc/nginx/cert/example.cn.key
COPY ./deployment/nginx/cert/example.cn.pem /etc/nginx/cert/example.cn.pem
# 复制配置文件生成模板
COPY ./deployment/nginx/my_nginx.conf.template /etc/nginx/conf.d
# 复制文件生成脚本
COPY ./deployment/nginx/generate_nginx_conf_file.sh /etc/nginx/conf.d
# 覆盖默认nginx配置文件&&赋予配置文件生成脚本执行权限
RUN cd /etc/nginx/conf.d&&mv default.conf default.conf.bak&&chmod +x generate_nginx_conf_file.sh
# 生成配置文件并加载
CMD /bin/sh /etc/nginx/conf.d/generate_nginx_conf_file.sh && nginx -g 'daemon off;' && nginx -s reload

注意

因为我们只允许https连接,因此必须事先申请ssl证书,可以去阿里云申请免费的ssl证书。证书文件需要保存项目的deployment/nginx/cert下,然后通过pycharm上传到部署的服务器。在容器镜像构建时复制到上述nginx配置文件中ssl_certificatessl_certificate_key 指定的路径中。证书文件的命名要和nginx配置文件指定的名称一致。

(4)健康检查脚本my_nginx_health_check.sh

nginx容器健康检查的目的主要是看看nginx是否能正确应答请求,因此在上述的配置文件中增加了一个location /health_check/字段。如果访问该location返回200状态码,则nginx可以正确应答请求。

# 访问连接健康检查url
result_code=$(curl -I -m 10 -o /dev/null -s -w %{http_code} 
"https://${KAOPU_NGINX_HEALTH_CHECK_HOST}/health_check/")
# 检测请求响应状态码
if [ "$result_code" -eq 200 ]; then
  echo "Ok,my_nginx is running,now!"
  exit 0
else
  echo "ERROR! my_nginx is not running"
  exit 1
fi

 这个脚本由docker-compose自动运行。

3.3.4 my_django容器

(1)容器构建my_django.dockerfile

# my_django容器
FROM python:latest
# 维护者信息
MAINTAINER anbuqi <1230@qq.com>
# 复制django源码到python容器内
COPY . my_backend/
# 容器构建时先更换pip源为阿里云的源安装python依赖,授予启动脚本和健康检查脚本执行权限
RUN pip install -i  https://mirrors.aliyun.com/pypi/simple/ -r my_backend/requirements.txt\
&&chmod +x /my_backend/deployment/django/my_django_start.sh /my_backend/deployment/django/my_django_health_check.py\
&&chmod +x /my_backend/wait-for-it.sh
# 容器启动时运行服务启动脚本
CMD ./my_backend/wait-for-it.sh -h $MY_DATABASE_HOST -p $MY_DATABASE_HOST_PORT --strict -- /bin/bash /my_backend/deployment/django/my_django_start.sh

在上述的命令中使用了一个wait-for-it脚本来确保my_django容器环境初始化在my_mysql容器服务可用后才进行,这样my_django容器不用等到my_mysql容器服务正常时(即健康状态为healthy)才启动,可以加快启动速度。该脚本的下载和使用参见:

 wait-for-it.sh

https://codechina.csdn.net/mirrors/vishnubob/wait-for-it?utm_source=csdn_github_accelerator

关于容器启动顺序控制参见

https://docs.docker.com/compose/startup-order/

(2)python环境依赖requirements.txt:

asgiref==3.3.1
certifi==2020.12.5
chardet==4.0.0
Django==3.1.7
django-filter==2.4.0
django-oauth-toolkit==1.4.0
djangorestframework==3.12.2
idna==2.10
mysqlclient==2.0.3
Naked==0.1.31
oauthlib==3.1.0
Pillow==8.1.2
pyasn1==0.4.8
pycryptodome==3.10.1
pytz==2021.1
PyYAML==5.4.1
requests==2.25.1
rsa==4.7.2
shellescape==3.8.1
sqlparse==0.4.1
urllib3==1.26.3
uvicorn
gunicorn
httptools
django_redis
uvloop

可以通过在manage.py所在目录运行:

pip freeze > requirements.txt

导出依赖文件。 

(3)容器环境初始化脚本my_django_start.sh

#!/bin/sh
echo 'Starting my_backend......'
# 切换到容器内manage.py所在目录目录
# shellcheck disable=SC2164
cd /my_backend
# django数据迁移命令,必须最先运行数据迁移命令
python manage.py makemigrations
python manage.py migrate
# 创建一个默认的管理员账号,容器构建完成后务必登录django admin修改密码
echo "from django.contrib.auth.models import User; User.objects.create_superuser('admin', '', 'mypassword')" | python manage.py shell
#静态文件收集,先清除过时文件再收集
python manage.py collectstatic --clear --noinput
# 启动asgi服务器,必须前台运行,否则容器自动关闭
# shellcheck disable=SC2016
gunicorn my_backend.asgi:application

(4)gunicorn配置文件gunicorn.conf.py

必须是这个文件名才能被gunicorn自动读取,并且要存放在gunicorn命令运行的目录下,这里就是manage.py所在的目录。否则需要在运行命令时显式指定gunicorn配置文件路径。

import os

# 每次都以重启的方式运行,以加载django代码变化
reload = True
# 开启四个工作线程
workers = 4
# 工作类使用uvicorn提供的类
worker_class = "uvicorn.workers.UvicornWorker"
# 进程名称,使用linux ps命令将显示这个名称
proc_name = "my_backend"
# 工作进程沉默多少秒将被杀死并重新启动。
timeout = 120
# 错误日志
errorlog = "./my_log/gunicorn_error.log"
# 连接日志,如果使用代理服务器,一般只在调试时开启
# accesslog = "./my_log/gunicorn_access.log"
# 端口
port = os.environ.get('OPEN_PORT')
if port is None:
    print("OPEN_PORT环境变量为空,使用默认端口8000.")
    bind = ['0.0.0.0:8000']
else:
    bind = ['0.0.0.0:' + str(port), ]

(5)健康检查脚本my_django_health_check.py
django服务中的健康检查主要是确定django是否能响应请求,因此使用HEAD方法请求主域名,看看是否能连接成功,如果可以连接上,只要返回500以下状态码,说明django服务在运行:

import sys

import requests
import os

try:
    # 获取asgi服务器监听的端口,OPEN_PORT在容器构建时指定
    port = os.environ.get('OPEN_PORT')
    if port is None:
        print("检查连接端口为空,设置OPEN_PORT环境变量。")
        sys.exit(1)
    # 通过HEAD方法检查服务器是否能连接上
    r = requests.head('http://127.0.0.1:' + port+'/')
    code = int(r.status_code)
    # 状态码在200-400代表服务器正常
    if 200 <= code <= 500:
        sys.exit(0)
    else:
        print("健康检查错误:服务器错误")
        sys.exit(1)
except requests.exceptions.ConnectionError as e:
    print("健康检查错误:连接失败", e)
    sys.exit(1)

 

3.3.5 .dockerignore文件

参见

https://docs.docker.com/engine/reference/builder/#dockerignore-file

这个文件用来指定在容器构建时,使用COPY命令复制文件过程中需要忽略哪些文件。对于本项目来说,一般需要忽略本地调试产生的数据迁移文件,即migrations文件夹的文件,以及构建容器的dokcerfile文件、yml文件和一些包含敏感信息的文件

# 构建容器时忽略的文件和文件夹
# 忽略本地项目中生成的数据迁移文件
*/migrations*
# 忽略构建容器的.dockerfile和docker-compose.yml文件
**/*.dockerfile
**/*.yml
# 忽略代码说明文档.md文件
**/*.md

注意

dockerignore文件要和dockerfile放在同一目录下,并且拓展名是dockerignore。在本次项目中,这个文件和manage.py处于同一目录。

 

3.4 编写docker-compose.yml构建文件

配置文件和容器设置参见:

https://docs.docker.com/compose/

现在我们准备好了构建各个容器需要的配置文件,对于多容器docker应用,一般使用docker-compose进行管理。

(1)docker-compose.yml

version: "3.8"
# 容器网络设置
networks:
  my_backend_network:
    name: my_backend_network
# 上传的媒体文件和静态文件存储卷共享
volumes:
  my_media:
  my_static:

# 安全警告:除了nginx容器,其他容器不允许将端口映射到宿主机,容器间的通信在docker内网完成。
services:
  my_django:
    #    构建生成的镜像名称
    image: my_django:1.0
    #    生成的容器名称
    container_name: my_django
    #    容器失败时重新启动
    restart: on-failure
    environment:
      # django的 ALLOWED_HOSTS,英文逗号分隔
      DJANGO_ALLOWED_HOSTS: www.example.com,api.example.com,example.com,127.0.0.1
      # 容器开放的端口,只能在docker内网使用,用于网络通信和健康检查
      OPEN_PORT: 9000
      # kaopu_mysql连接密码,和kaopu_mysql的MYSQL_ROOT_PASSWORD保持一致
      MY_MYSQL_ROOT_PASSWORD: database_root_password
      # 数据库服务器主机名,和link中的值保持一致
      MY_DATABASE_HOST: my_mysql
      # 数据库服务器连接端口,和kaopu_mysql容器中定义要一致
      MY_DATABASE_HOST_PORT: 3306
      # 业务数据库名,和django设置DATABASE字段中的NAME,kaopu_mysql的MYSQL_DATABASE保持一致
      MY_DATABASE_NAME: MyDataBase
      # 业务数据连接用户,和django设置DATABASE字段中的USER,kaopu_mysql的MYSQL_USER保持一致
      MY_DATABASE_USER: MyDataManager
      # 业务数据库连接密码,和django设置DATABASE字段中的PASSWORD,kaopu_mysql的MYSQL_PASSWORD保持一致
      MY_DATABASE_USER_PASSWORD: database_admin_password
      #  kaopu_redis连接密码和缓存数据库连接位置,连接密码要和kaopu_redis的REQUIRE_PASS保持一致
      MY_REDIS_PASSWORD: cache_password
      MY_REDIS_LOCATION: "redis://my_redis:6379"
      #      django 签名密钥,禁止泄露
      SECRET_KEY: "django生成的SECRET_KEY值"
    build:
      context: .
      dockerfile: my_django.dockerfile
    #    依赖的容器,仅在依赖容器启动后时kaopu_django才会启动
    depends_on:
      my_mysql:
        condition: service_started
      my_redis:
        condition: service_started
    #    网络
    networks:
      - my_backend_network
    #    静态文件存储位置
    volumes:
      - my_static:/my_backend/static
      - my_media:/my_backend/media
      #   容器间连接,容器的networks要保持一致
    links:
      - my_nginx
      - my_mysql
      - my_redis
    #    健康检查
    healthcheck:
      test: python /my_backend/deployment/django/my_django_health_check.py
      retries: 3
      interval: 1m
      timeout: 10s

 my_mysql:
    image: my_mysql:1.0
    container_name: my_mysql
    restart: on-failure
    expose:
      - 3306
    environment:
      MYSQL_ROOT_PASSWORD: database_root_password
      MYSQL_DATABASE: MyDataBase
      MYSQL_USER: MyDataManager
      MYSQL_PASSWORD: database_admin_password
      #      时区
      TZ: Asia/Shanghai
    build:
      context: .
      dockerfile: my_mysql.dockerfile
      #    网络
    networks:
      - my_backend_network
    healthcheck:
      interval: 1m
      timeout: 10s
      retries: 3
      test: /bin/bash /my_mysql/my_mysql_health_check.sh

  my_redis:
    image:my_redis:1.0
    container_name:my_redis
    restart: on-failure
    # 从配置文件所在目录启动,加载配置并设置连接密码为cache_password
    command: redis-server /my_redis/ --requirepass cache_password
    environment:
      MY_REQUIREPASS: cache_password
    expose:
      - "6379"
    #    网络
    networks:
      - my_backend_network
    build:
      context: .
      dockerfile: my_redis.dockerfile
    healthcheck:
      interval: 1m
      timeout: 10s
      retries: 3
      test: /bin/bash /my_redis/my_redis_health_check.sh

  my_nginx:
    image: my_nginx:1.0
    container_name:my_nginx
    restart: on-failure
    ports:
      - "443:443"
      - "80:80"
    environment:
      # server 后端主机和端口要和django容器的设置一致
      MY_NGINX_SERVER_LIST: 'server my_django:8000 max_fails=1 fail_timeout=40s;'
    env_file:
      - ./deployment/nginx/my_nginx_env_file
    build:
      context: .
      dockerfile: my_nginx.dockerfile
    networks:
      - my_backend_network
    volumes:
      - my_static:/my_backend/static
      - my_media:/my_backend/media
    healthcheck:
      interval: 1m
      timeout: 10s
      retries: 3
      test: /bin/bash /etc/nginx/conf.d/my_nginx_health_check.sh

在这个构建配置文件中,定义了一个数据卷,这样做的原因是nginx服务器对外提供静态文件服务,而静态文件由django产生,为了便于进行两个容器之间的文件共享,将django设置中STATIC_ROOT、MEDIA_ROOT指定的目录和nginx配置文件中指定的静态文件存储目录(/static/和/media/两个location字段)通过数据卷共享

docker数据卷共享参见

https://docs.docker.com/compose/compose-file/compose-file-v3/#volumes

注意

所有容器的dockerfiledocker-compose.yml要处于同一目录,否则docker-compose.ymlbuild字段的context值不是(.)而是dockerfile所在目录。这里都和manage.py放在同一文件夹下。

 (2)构建容器

docker-compose.yml所在目录运行:

docker-compose up

将会自动构建容器镜像并运行容器,命令执行成功后不会自动退出,此时处于调试状态,可以捕捉到容器运行中遇到的错误。

如果不想使用调试,可以这样运行:

docker-compose up -d

容器会在后台启动被运行。 

(3)其它常用操作

更多操作参见:

https://docs.docker.com/compose/reference/overview/

停止服务并移除所有容器、网络但不删除构建的镜像:

docker-compose down

停止服务并移除所有容器、网络并同时删除构建的镜像:

docker-compose down --rmi all

按依赖顺序启动所有容器:

docker-compose start

启动单个容器:

docker-compose start <容器名称>

停止所有容器(yml文件 service字段下指定的)运行:

docker-compose stop

停止单个容器:

docker-compose stop <容器名称>

列出所有运行中的容器:

​docker ps

下一节Django 架设 Restful API(八)项目部署:定制django管理后台、日志设置

https://blog.csdn.net/anbuqi/article/details/114491465

热门文章

暂无图片
编程学习 ·

Java输出数组的内容

Java输出数组的内容_一万个小时-CSDN博客_java打印数组内容1. 输出内容最常见的方式// List<String>类型的列表List<String> list new ArrayList<String>();list.add("First");list.add("Second");list.add("Third");list.ad…
暂无图片
编程学习 ·

母螳螂的“魅惑之术”

在它们对大蝗虫发起进攻的时候&#xff0c;我认认真真地观察了一次&#xff0c;因为它们突然像触电一样浑身痉挛起来&#xff0c;警觉地面对限前这个大家伙&#xff0c;然后放下自己优雅的身段和祈祷的双手&#xff0c;摆出了一个可怕的姿势。我被眼前的一幕吓到了&#xff0c;…
暂无图片
编程学习 ·

疯狂填词 mad_libs 第9章9.9.2

#win7 python3.7.0 import os,reos.chdir(d:\documents\program_language) file1open(.\疯狂填词_d9z9d2_r.txt) file2open(.\疯狂填词_d9z9d2_w.txt,w) words[ADJECTIVE,NOUN,VERB,NOUN] str1file1.read()#方法1 for word in words :word_replaceinput(fEnter a {word} :)str1…
暂无图片
编程学习 ·

HBASE 高可用

为了保证HBASE是高可用的,所依赖的HDFS和zookeeper也要是高可用的. 通过参数hbase.rootdir指定了连接到Hadoop的地址,mycluster表示为Hadoop的集群. HBASE本身的高可用很简单,只要在一个健康的集群其他节点通过命令 hbase-daemon.sh start master启动一个Hmaster进程,这个Hmast…
暂无图片
编程学习 ·

js事件操作语法

一、事件的绑定语法 语法形式1 事件监听 标签对象.addEventListener(click,function(){}); 语法形式2 on语法绑定 标签对象.onclick function(){} on语法是通过 等于赋值绑定的事件处理函数 , 等于赋值本质上执行的是覆盖赋值,后赋值的数据会覆盖之前存储的数据,也就是on…
暂无图片
编程学习 ·

Photoshop插件--晕影动态--选区--脚本开发--PS插件

文章目录1.插件界面2.关键代码2.1 选区2.2 动态晕影3.作者寄语PS是一款栅格图像编辑软件&#xff0c;具有许多强大的功能&#xff0c;本文演示如何通过脚本实现晕影动态和选区相关功能&#xff0c;展示从互联网收集而来的一个小插件&#xff0c;供大家学习交流&#xff0c;请勿…
暂无图片
编程学习 ·

vs LNK1104 无法打开文件“xxx.obj”

写在前面&#xff1a; 向大家推荐两本新书&#xff0c;《深度学习计算机视觉实战》和《学习OpenCV4&#xff1a;基于Python的算法实战》。 《深度学习计算机视觉实战》讲了计算机视觉理论基础&#xff0c;讲了案例项目&#xff0c;讲了模型部署&#xff0c;这些项目学会之后可以…
暂无图片
编程学习 ·

工业元宇宙的定义与实施路线图

工业元宇宙的定义与实施路线图 李正海 1 工业元宇宙 给大家做一个关于工业元宇宙的定义。对于工业&#xff0c;从设计的角度来讲&#xff0c;现在的设计人员已经做到了普遍的三维设计&#xff0c;但是进入元宇宙时代&#xff0c;就不仅仅只是三维设计了&#xff0c;我们的目…
暂无图片
编程学习 ·

【leectode 2022.1.15】完成一半题目

有 N 位扣友参加了微软与力扣举办了「以扣会友」线下活动。主办方提供了 2*N 道题目&#xff0c;整型数组 questions 中每个数字对应了每道题目所涉及的知识点类型。 若每位扣友选择不同的一题&#xff0c;请返回被选的 N 道题目至少包含多少种知识点类型。 示例 1&#xff1a…
暂无图片
编程学习 ·

js 面试题总结

一、js原型与原型链 1. prototype 每个函数都有一个prototype属性&#xff0c;被称为显示原型 2._ _proto_ _ 每个实例对象都会有_ _proto_ _属性,其被称为隐式原型 每一个实例对象的隐式原型_ _proto_ _属性指向自身构造函数的显式原型prototype 3. constructor 每个prot…
暂无图片
编程学习 ·

java练习代码

打印自定义行数的空心菱形练习代码如下 import java.util.Scanner; public class daYinLengXing{public static void main(String[] args) {System.out.println("请输入行数");Scanner myScanner new Scanner(System.in);int g myScanner.nextInt();int num g%2;//…
暂无图片
编程学习 ·

RocketMQ-什么是死信队列?怎么解决

目录 什么是死信队列 死信队列的特征 死信消息的处理 什么是死信队列 当一条消息初次消费失败&#xff0c;消息队列会自动进行消费重试&#xff1b;达到最大重试次数后&#xff0c;若消费依然失败&#xff0c;则表明消费者在正常情况下无法正确地消费该消息&#xff0c;此时…
暂无图片
编程学习 ·

项目 cg day04

第4章 lua、Canal实现广告缓存 学习目标 Lua介绍 Lua语法 输出、变量定义、数据类型、流程控制(if..)、循环操作、函数、表(数组)、模块OpenResty介绍(理解配置) 封装了Nginx&#xff0c;并且提供了Lua扩展&#xff0c;大大提升了Nginx对并发处理的能&#xff0c;10K-1000K Lu…
暂无图片
编程学习 ·

输出三角形

#include <stdio.h> int main() { int i,j; for(i0;i<5;i) { for(j0;j<i;j) { printf("*"); } printf("\n"); } }
暂无图片
编程学习 ·

stm32的BOOTLOADER学习1

序言 最近计划学习stm32的BOOTLOADER学习,把学习过程记录下来 因为现在网上STM32C8T6还是比较贵的,根据我的需求flash空间小一些也可以,所以我决定使用stm32c6t6.这个芯片的空间是32kb的。 #熟悉芯片内部的空间地址 1、flash ROM&#xff1a; 大小32KB&#xff0c;范围&#xf…
暂无图片
编程学习 ·

通过awk和shell来限制IP多次访问之学不会你打死我

学不会你打死我 今天我们用shell脚本&#xff0c;awk工具来分析日志来判断是否存在扫描器来进行破解网站密码——限制访问次数过多的IP地址&#xff0c;通过Iptables来进行限制。代码在末尾 首先我们要先查看日志的格式&#xff0c;分析出我们需要筛选的内容&#xff0c;日志…
暂无图片
编程学习 ·

Python - 如何像程序员一样思考

在为计算机编写程序之前&#xff0c;您必须学会如何像程序员一样思考。学习像程序员一样思考对任何学生都很有价值。以下步骤可帮助任何人学习编码并了解计算机科学的价值——即使他们不打算成为计算机科学家。 顾名思义&#xff0c;Python经常被想要学习编程的人用作第一语言…
暂无图片
编程学习 ·

蓝桥杯python-数字三角形

问题描述 虽然我前后用了三种做法&#xff0c;但是我发现只有“优化思路_1”可以通过蓝桥杯官网中的测评&#xff0c;但是如果用c/c的话&#xff0c;每个都通得过&#xff0c;足以可见python的效率之低&#xff08;但耐不住人家好用啊&#xff08;哭笑&#xff09;&#xff09…