我们平时在写完Django项目后到生产环境的部署还是比较复杂的,OS环境搭建、项目依赖、版本控制等等,其中任何一个环节稍不小心可能就要从头来过,因此使用Docker容器可以大大简化Django项目的部署操作并提升应用的可移植性,通过Docker可以让我们把自己的项目代码和依赖打包到一个可移植的镜像中,然后发布到多个平台中运行。这里将详细介绍如何使用Docker及Docker-Compose部署Django+Vue+Uwsgi+Nginx+Mysql项目。

注意:本文主要侧重于Docker技术在Django部署的实际应用,而不是Docker基础教程,建议初学者先自行储备一些Docker和Docker-Compose基础知识。

环境准备

  • Docker及Docker-Compose安装

# 这里以CentOS7.6为例
# 在安装前关闭Selinux和Firewalld
​
# 下载Docker-ce源
sudo wget -O /etc/yum.repos.d/docker-ce.repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
​
# 安装Dcker
sudo yum -y install docker-ce
​
# 检查Docker
sudo docker -v
​
# 启动Docker并设置为开机自启
sudo systemctl start docker
sudo systemctl enable docker
​
# 设置阿里云Docker镜像,注意修改URL为自己的镜像链接
sudo mkdir -p /etc/docker 
sudo tee /etc/docker/daemon.json <<-'EOF' 
{
  "registry-mirrors": ["https://docker.nju.edu.cn"],
   "default-address-pools":
    [
      {"base":"192.168.0.0/16","size":24}
    ]
}
EOF 
sudo systemctl daemon-reload 
sudo systemctl restart docker
​
# 下载Docker-Compose
sudo wget https://github.com/docker/compose/releases/download/v2.23.1/docker-compose-linux-x86_64
sudo mv docker-compose-linux-x86_64 /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
​
# 检查Docker-Compose
docker-compose -v

容器组合示意图

本次架构为Django+Uwsgi+Nginx+Vue+Mysql,我们将使用Docker-compose编排以下三个容器:

  • Django + Uwsgi容器(别名web):Django应用程序,处理动态请求

  • Mysql容器(别名db):数据库服务

  • Nginx容器(别名nginx):前端项目+静态资源请求+反向代理

依赖关系为web容器依赖db容器,nginx容器依赖web容器。每个容器再将各自的项目、配置及日志目录映射至宿主机中,这样方便我们随时调试。

示意图如下:

image-kvzd.png

项目目录树形图

在项目部署目录中,我新建了一个compose目录,用于存放构建其他容器的Dockerfile和配置文件。然后是我们的Django项目目录,如果有使用gitlab或者github管理代码,可以直接git clone到项目目录下,与compose目录平级,这样做的好处是不同的django项目可以共享compose文件夹。

myproject_docker            # 项目根目录
├── compose                 # 存放各项容器服务的Dockerfile和配置文件
│   ├── mysql 
│   │   ├── conf
│   │   │   └── my.cnf      # MySQL配置文件
│   │   └── init
│   │       └── init.sql    # MySQL初始化脚本
│   ├── nginx
│   │   ├── dist            # 挂载至nginx容器内的dist目录,存放前端打包项目
│   │   ├── Dockerfile      # 构建Nginx容器的Dockerfile
│   │   ├── log             # 挂载至nginx容器内的log目录
│   │   ├── nginx.conf      # Nginx配置文件
│   │   └── ssl             # Nginx证书文件
│   └── uwsgi               # uwsgi日志目录
├── docker-compose.yml      # 核心编排文件
└── myproject               # Django项目代码目录
    ├── .env                # 环境变量文件
    ├── api                 # Django项目的app或者apps
    ├── backend             # Django项目配置文件
    │   ├── asgi.py
    │   ├── __init__.py
    │   ├── settings.py     # Django核心配置文件
    │   ├── urls.py
    │   └── wsgi.py
    ├── Dockerfile          # 构建Django + Uwsgi镜像的Dockerfile
    ├── manage.py
    ├── media               # 用户上传的媒体资源,如果没有需要手动创建
    ├── pip.ini             # pip配置文件,用于设置镜像源,非必须
    ├── requirements.txt    # Django项目依赖文件
    ├── start.sh            # Django + Uwsgi容器的项目启动脚本
    ├── static              # 项目静态资源文件夹,如果没有需要手动创建
    └── uwsgi.ini           # uwsgi配置文件

下面开始正式进入部署。

一、编写docker-compose文件

在docker-compose.yml中,我们定义了三个数据卷,用于挂载各个容器内动态生成的数据,比如Mysql存储数据、web容器中的静态文件数据和用户上传的媒体数据。这样即使删除容器,数据也不会丢失。

此外还定义两个网络,nginx_network用于nginx和web容器间的通信,db_network用于mysql容器和web容器间的通信。

然后就是三个容器服务,别名分别为dbnginxweb

version: "3"
​
volumes:  # 自定义数据卷
  db_vol:   # 定义数据卷同步存放容器内mysql数据
  media_vol:  # 定义数据卷同步存放web项目用户上传到media文件夹的数据
  static_vol:  # 定义数据卷同步存放web项目static文件夹的数据
​
networks: # 自定义网络(默认桥接), 不使用links通信
  nginx_network:
    driver: bridge
  db_network:
    driver: bridge
​
services:
  db:
    image: mysql:8.0    # 使用mysql8.0镜像
    env_file:
      - ./myproject/.env # 使用了环境变量文件
    networks:
      - db_network
    volumes:
      - db_vol:/var/lib/mysql:rw # 挂载数据库数据, 可读可写
      - ./compose/mysql/conf/my.cnf:/etc/mysql/my.cnf # 挂载配置文件
      - ./compose/mysql/init:/docker-entrypoint-initdb.d/ # 挂载数据初始化sql脚本
    ports:
      - "3306:3306" 
    restart: always
​
  web:
    build: ./myproject
    expose:
      - "8000"
    volumes:
      - ./myproject:/var/www/html/myproject # 挂载项目代码
      - static_vol:/var/www/html/myproject/static # 以数据卷挂载容器内static文件
      - media_vol:/var/www/html/myproject/media # 以数据卷挂载容器内用户上传媒体文件
      - ./compose/uwsgi:/tmp # 挂载uwsgi日志
    networks:
      - nginx_network
      - db_network
    depends_on:
      - db
    restart: always
    tty: true
    stdin_open: true
​
  nginx:
    build: ./compose/nginx
    ports:
      - "80:80"
      - "443:443"
      - "8000:8000"
    expose:
      - "80"
    volumes:
      - ./compose/nginx/nginx.conf:/etc/nginx/conf.d/nginx.conf # 挂载nginx配置文件
      - ./compose/nginx/ssl:/usr/share/nginx/ssl # 挂载ssl证书目录
      - ./compose/nginx/log:/var/log/nginx # 挂载日志
      - ./compose/nginx/dist:/usr/share/nginx/html/dist # 挂载Vue项目打包目录
      - static_vol:/usr/share/nginx/html/static # 挂载静态文件
      - media_vol:/usr/share/nginx/html/media # 挂载用户上传媒体文件
    networks:
      - nginx_network
    depends_on:
      - web
    restart: always

二、Web镜像和容器文件

myproject/Dockerfile内容如下

# 建立 python 3.9环境
FROM python:3.9.10
​
# 安装netcat
RUN apt-get update && apt install -y netcat
​
# 镜像作者
MAINTAINER xinchen.luan
​
# 设置 python 环境变量
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
​
# 可选:设置镜像源为国内
COPY pip.ini /root/.pip/pip.ini
​
# 容器内创建 myproject 文件夹
ENV APP_HOME=/var/www/html/myproject
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME
​
# 将当前目录加入到工作目录中(. 表示当前目录)
ADD . $APP_HOME
​
# 附件目录增加权限
RUN chgrp -R www-data media/
RUN chmod -R g+w media/

# 更新pip版本
RUN /usr/local/bin/python -m pip install --upgrade pip
​
# 安装项目依赖
RUN pip install -r requirements.txt
​
# 移除\r in windows
RUN sed -i 's/\r//' ./start.sh
​
# 给start.sh可执行权限
RUN chmod +x ./start.sh
​
# 执行项目启动脚本:数据迁移,并使用uwsgi启动服务
ENTRYPOINT /bin/bash ./start.sh

myproject/start.sh启动脚本内容如下:

#!/bin/bash
# 等待mysql容器启动
while ! nc -z db 3306 ; do
    echo "Waiting for the MySQL Server"
    sleep 3
done
​
# 收集静态文件
python manage.py collectstatic --noinput&&
​
# django的数据库迁移
python manage.py makemigrations&&
python manage.py migrate&&
​
# uwsgi启动项目
uwsgi --ini /var/www/html/myproject/uwsgi.ini&&
​
# 这里是为了保持容器状态
tail -f /dev/null
​
echo "Your Django application is now running!"
exec "$@"

myproject/uwsgi.ini配置如下:

[uwsgi]
​
project=myproject
uid=www-data
gid=www-data
base=/var/www/html
​
chdir=%(base)/%(project)
# 注意:这里的backend要改成自己的django项目名称
module=backend.wsgi:application
master=True
processes=2
​
socket=0.0.0.0:8000
chown-socket=%(uid):www-data
chmod-socket=664
​
vacuum=True
max-requests=5000
​
pidfile=/tmp/%(project)-master.pid
daemonize=/tmp/%(project)-uwsgi.log
​
#设置一个请求的超时时间(秒),如果一个请求超过了这个时间,则请求被丢弃
harakiri = 60
post buffering = 8192
buffer-size= 65535
#当一个请求被harakiri杀掉会,会输出一条日志
harakiri-verbose = true
​
#开启内存使用情况报告
memory-report = true
​
#设置平滑的重启(直到处理完接收到的请求)的长等待时间(秒)
reload-mercy = 10
​
#设置工作进程使用虚拟内存超过N MB就回收重启
reload-on-as= 1024

三、Nginx镜像和容器文件

compose/nginx/Dockerfile文件如下:

FROM nginx:1.24.0
​
# 删除原有配置文件,创建静态资源文件夹和ssl证书保存文件夹
RUN rm /etc/nginx/conf.d/default.conf \
&& mkdir -p /usr/share/nginx/html/static \
&& mkdir -p /usr/share/nginx/html/media \
&& mkdir -p /usr/share/nginx/ssl
​
# 设置Media文件夹用户和用户组为Linux默认www-data, 并给予可读和可执行权限,
# 否则用户上传的图片无法正确显示。
RUN chown -R www-data:www-data /usr/share/nginx/html/media \
&& chmod -R 775 /usr/share/nginx/html/media
​
# 添加配置文件
ADD ./nginx.conf /etc/nginx/conf.d/
​
# 关闭守护模式
CMD ["nginx", "-g", "daemon off;"]

compose/nginx/nginx.conf文件如下:

upstream django {
    ip_hash;
    server web:8000; # web容器+端口
}
​
# 前端
server {
    listen 80;
​
    server_name 127.0.0.1;  # nginx容器所在ip地址或127.0.0.1
​
    root /usr/share/nginx/html/dist;
    index index.html;
​
    location / {
        try_files $uri $uri/ /index.html;
    }
​
    location ~ ^/(js|css|img|fonts|static)/ {
        expires max;
        add_header Cache-Control "public, must-revalidate, proxy-revalidate";
    }
​
    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
        root /usr/share/nginx/html;
    }
​
    access_log /var/log/nginx/frontend_access.log;
    error_log /var/log/nginx/frontend_error.log;
}
​
​
# 后端
server {
    listen 8000;
    server_name 127.0.0.1; # nginx容器所在ip地址或127.0.0.1
​
    charset utf-8;
    client_max_body_size 10M; # 限制用户上传文件大小
​
    access_log /var/log/nginx/backend_access.log main;
    error_log /var/log/nginx/backend_error.log warn;
​
    location /static {
        alias /usr/share/nginx/html/static; # 静态资源路径
    }
​
    location /media {
        alias /usr/share/nginx/html/media; # 媒体资源,用户上传文件路径
    }
​
    location / {
        include /etc/nginx/uwsgi_params;
        uwsgi_pass django;         # 使用uwsgi通信,而不是http_pass
        uwsgi_read_timeout 600;
        uwsgi_connect_timeout 600;
        uwsgi_send_timeout 600;
        
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_redirect off;
        proxy_set_header X-Real-IP  $remote_addr;
    }
}

四、Mysql配置

这里Mysql我们可以直接使用官方镜像,编写我们自定义的配置文件和初始化脚本即可

compose/mysql/conf/my.conf文件如下:

[mysqld]
user=mysql
default-storage-engine=INNODB
character-set-server=utf8
# mysql 8 新增下面两行配置
secure-file-priv=NULL                                
default-authentication-plugin=mysql_native_password
​
port            = 3306 # 端口与docker-compose里映射端口保持一致
# 一定要注释掉,mysql所在容器和django所在容器不同IP
# bind-address= localhost 
​
basedir         = /usr
datadir         = /var/lib/mysql
tmpdir          = /tmp
pid-file        = /var/run/mysqld/mysqld.pid
socket          = /var/run/mysqld/mysqld.sock
# 这个参数是禁止域名解析的,远程访问推荐开启skip_name_resolve。
skip-name-resolve         
​
[client]
port = 3306
default-character-set=utf8
​
[mysql]
no-auto-rehash
default-character-set=utf8

然后编写mysql服务启动后的初始化脚本,注意这里的dbuser用户名和password必须和myproject下的.env环境变量保持一致

compose/mysql/init/init.sql文件如下:

Alter user 'dbuser'@'%' IDENTIFIED WITH mysql_native_password BY 'password';
GRANT ALL PRIVILEGES ON myproject.* TO 'dbuser'@'%';
FLUSH PRIVILEGES;

myproject/.env文件如下:

MYSQL_ROOT_PASSWORD=123456
MYSQL_DATABASE=myproject
MYSQL_USER=dbuser
MYSQL_PASSWORD=password

五、修改Django项目settings文件

准备好各容器编排工作后,还需要修改django项目的配置文件,其中几项核心配置如下:

# 生产环境设置 Debug = False
Debug = False
​
# 允许跨域请求,不能为空,否则会报400错误
ALLOWED_HOSTS = ['公网IP', '域名','或者*']
​
# 设置STATIC ROOT 和 STATIC URL
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATIC_URL = "/static/"
​
# 设置MEDIA ROOT 和 MEDIA URL
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = "/media/"
​
# 设置数据库,这里用户名和密码必需和docker-compose.yml里mysql环境变量保持一致
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'myproject', # 数据库名
        'USER': 'itops',  # 用户名
        'PASSWORD': '123456', # 用户密码
        'HOST': 'db', # 注意:这里使用的是db别名,docker会自动解析成ip
        'PORT': '3306',
    }
}

六、部署上线

进入部署根目录,输入以下命令构建镜像

# 构建镜像
sudo docker-compose build
​
# 查看已生成的镜像,应该可以看到web、nginx、mysql三个镜像
sudo docker images
​
# 然后启动容器组服务
sudo docker-compose up

启动容器组后,一切顺利的话你将看到Your Django application is now running!,这时候再分别访问80和8000端口校验服务是否正常,打开浏览器就能看到你的网站了。

七、排错

在部署过程中我们可能会遇到各类错误,排错的思路首先要分清是哪个容器出了问题,其次对容器内的日志进一步分析。例如浏览器打开80端口显示端口拒绝,这表示nginx容器没有正常启动,或网站打开后,请求4XX/5XX错误,这一般表示后端错误也就是web容器异常。下面我们就几个典型的情况进行排错。

Nginx容器排错

容器显示运行,但网站浏览器无法打开,可以在宿主机的/compose/nginx/log目录里直接查看相关日志。

# 进入nginx日志目录,一个access.log, 一个error.log
cd compose/nginx/log
# 查看日志文件
sudo cat error.log 

Nginx日志显示nginx: connect() failed (111: Connection refused) while connecting to upstream或Nginx 502 gateway的错误都不是因为nginx自身的原因,而是Web容器中Django程序有问题或则uwsgi配置文件有问题。

在进入Web容器排错前,首先要检查下Nginx转发请求的方式(proxy_passuwsgi_pass)以及转发端口与uwsgi里面的监听方式以及端口是否一致。

uWSGI和Nginx之间有3种通信方式unix socket,TCP socket和http如果Nginx以proxy_pass方式转发请求,uwsgi需要使用http协议进行通信。如果Nginx以uwsgi_pass转发请求,uwsgi建议配置socket进行通信。

Web容器排错

Web容器也就是Django+UWSGI所在的容器,是最容易出现错误的容器。如果Nginx配置没问题,你应该进入web容器查看运行脚本命令时有没有报错,并检查uwsgi的运行日志。uwsgi的日志非常有用,它会记录Django程序运行时发生了哪些错误或异常。一旦发生了错误,uwsgi的进程虽然不会停止,但也无法正常工作,自然也就不能处理nginx转发的动态请求从而出现nginx报错了

# 查看web容器日志
$ docker-compose logs web
# 进入web容器执行启动命令,查看有无报错
$ docker-compose exec web /bin/bash start.sh
# 或则进入web柔情其,逐一执行python manage.py命令
$ docker-compose exec web /bin/bash 
# 进入web容器,查看uwsgi是否正常启动
$ ps aux | grep uwsgi 
# 进入uwsgi日志所在目录,查看Django项目是否有报错
cd /tmp

另外我们在上传附件时,有可能会遇到[Errno 13] Permission denied错误,这个通常发生在多容器直接权限没有正确传递的错误,我们可以先进入web容器,然后手动添加权限测试

chgrp -R www-data media/
chmod -R g+w media/

如果接口正常,那么我们则修改web容器的Dockerfile文件即可

RUN chgrp -R www-data media/
RUN chmod -R g+w media/

另一个常发生的错误是 docker-compose生成的web容器执行脚本命令后立刻退出(exited with code 0), 这时的解决方案是在docker-compose.yml中包含以下2行, 另外脚本命令里加入tail -f /dev/null是容器服务持续运行。

stdin_open: true
tty: true

有时web容器会出现不能连接到数据库的报错,这时需要检查settings.py中的数据库配置信息是否正确(比如host为db),并检查web容器和db容器是否通过db_network正常通信(比如进入db容器查看数据表是否已经生成)。在进行数据库迁移时web容器还会出现if table exists or failed to open the referenced table ‘users_user’, inconsistent migration history的错误, 可以删除migrations目录下文件并进入MySQL容器删除django_migrations数据表即可。

DB容器排错

我们还需要经常进入数据库容器查看数据表是否已生成并删除一些数据,这时可以使用如下命令:

# 进入db容器
$ docker-compose exec db /bin/bash
# 登录
mysql -u username -p;
# 选择数据库
USE dbname;
# 显示数据表
SHOW tables;
# 清空数据表
DELETE from tablename;
# 删除数据表
DROP TABLE tablename;

小结

本文详细地介绍了如何使用docker-compose工具分八步在生产环境下部署Django + Uwsgi + Nginx + MySQL。过程看似很复杂,但很多Dockerfile,项目布局及docker-compose.yml都是可以复用的。熟练利用Docker基本上可以10分钟内完成一个正式Django项目的部署,而且可以保证在任何一台Linux机器上顺利地运行。