一、前言
在上章内容中,简单介绍了使用docker部署项目的优势以及如何在Linux环境下安装docker,本章内容将主要讲解如何构建容器来部署项目。
二、Docker的组成
- 镜像: 一个只读模板,其中配置了容器运行所需要的环境数据
- 容器: 使用镜像创建的实例,可以简单理解为是一个虚拟机,要注意的是容器与容器之间是相互隔离的,容器与外界操作环境之间也是相互隔离的
- 仓库: 类似Nginx、Mysql这样的常用组件,如果我们每次部署项目时都得自己编写镜像配置文件来构建镜像,那不是很麻烦?所以早有人将这些常用组件镜像上传至公共的远程仓库中,我们只要拉取就行,当然我们也可以上传自己的镜像到仓库中,是不是感觉和Github很类似!目前docker默认的公共仓库是docker官方维护的Dockerhub。
上述镜像和容器的关系,我们可以简单的理解为:镜像是一个对象类型,容器是这个对象类型的实例。
三、构建Django的容器
在上章内容中我们提到过,如果项目由多个组件组成,我们最好是为每个组件都构建一个容器,使其能单独运行,方便复用。此处我们需要部署的项目由Django+Nginx+Mysql
三个组件组成,我们需要为这三个组件分别单独构建一个容器。首先是为Django
构建容器。
构建镜像
我们先创建一个Django
项目,这个项目我们就暂时命名为room
,我们在其中创建一个叫做deployment
(可以随意命名)的文件夹,用于存放我们部署使用的配置文件,因为我们一般开始时会区分测试与正式环境,所以我们再在deployment
文件夹下创建一个dev
文件夹,用于存放测试环境下的部署配置文件,项目的最终目录结构如下:
|—— room
| |——deployment # 存放部署使用的配置文件
| | |——dev # 存放部署测试环境使用的配置文件
| | | |——app # django相关
| | | | |——Dockerfile # django项目的镜像配置文件
| | | | |——startapp.sh # 启动django项目的脚本文件
| | | |——gunicron # gunicorn相关
| | | | |——gunicorn.conf.py # gunicorn的配置文件
| | | |——nginx # nginx相关
| | | | |——nginx.conf # nginx的配置文件
| | | |——docker-compose.yml # 容器的配置文件
| | |——wait-for-it.sh # 控制服务顺序执行的脚本文件
| |——room
| |——static
| |——templates
| |——manage.py
可以看到,目录结构中存在两个sh文件,这两个sh文件的作用如下:
-
startapp.sh:
编写了启动Django项目的命令行,因为运行项目时需要执行多条命令,我们将这些命令都写入一个shell脚本中,我们后面会通过执行这个sh文件来运行项目 -
wait-for-it.sh:
有些服务启动需要一定的时间,而其他服务需要等这些服务启动完成后才能执行,此文件的作用就是确保服务可以按顺序启动
编写Django项目的镜像配置文件
我们在项目中的Dockerfile
的文件中写入如下内容:
# 从仓库拉取带有python3.8的Linux环境
FROM python:3.8
# 镜像维护者的姓名和邮箱地址
MAINTAINER albertlii <albertlii@163.com>
# 因为墙的关系,有时安装库可能会很慢或者失败,所以推荐使用国内镜像源
# 此处是创建一个指向阿里云镜像源的参数,以便后面引用
ARG pip_url=http://mirrors.aliyun.com/pypi/simple/
ARG pip_host=mirrors.aliyun.com
# 定义一个变量,其值为项目在docker容器中的工作目录
ARG work_home=/room
# 设置python环境变量
ENV PYTHONUNBUFFERED 1
# 容器默认的时间是UTC时间,此处设置为上海时间
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
# 在容器中创建room文件夹
RUN mkdir ${work_home}
# 将容器中的room文件夹设置为工作目录
WORKDIR ${work_home}
# 将当前目录内容复制到容器的room目录,因为build中指定的context环境目录为project这一层,所以当前目录即项目目录
COPY . ${work_home}
# 更新pip
RUN pip install pip -U
# 安装所有依赖库
RUN pip install -r requirements.txt -i ${pip_url} --trusted-host ${pip_host}
# 将deployment/dev下的startapp.sh 文件拷贝到容器的根目录下
COPY deployment/dev/app/startapp.sh /startapp.sh
# 将deployment下的wait-for-it.sh文件拷贝到容器的根目录下
COPY deployment/wait-for-it.sh /wait-for-it.sh
# 修改文件权限 ,为sh文件增加可执行
RUN chmod +x /startapp.sh
RUN chmod +x /wait-for-it.sh
下面对上述的指令语句做一些简单介绍:
-
FROM python:3.8:
从仓库拉取一个包含Python3.8的Linux操作环境 -
WORKDIR:
这个指令是针对容器的,功能是将容器中的指定目录设置为工作目录 -
COPY:
将宿主机中的指定目录拷贝到容器中的指定目录中,docker中还有一个ADD
指令与COPY
指令功能相似,但是ADD
指令在拷贝后会自动解压压缩包,COPY
指令不会 -
RUN:
这个指令是针对容器的,主要是在容器中执行你想要执行的指令
Dockerfile
是默认的配置文件名称,docker在创建镜像时会默认去指定路径(即指定的上下文环境目录)中寻找名为Dockerfile
的配置文件。当然,我们也可以修改名称,只需要我们重新手动指定一下配置文件的路径即可,在下面内容中我们会讲到。
startapp.sh
文件内容如下:
#!/bin/sh
python manage.py migrate
python manage.py collectstatic --noinput
gunicorn -c ./deployment/dev/gunicorn/gunicorn.conf.py room.wsgi:application
wait-for-it.sh
是直接从Github上下载的,链接如下:
https://github.com/vishnubob/wait-for-it
至此,Django
相关的镜像配置到此结束,我们还缺少Nginx
与Mysql
的镜像配置。但是,我们在文章开头就提到过,类似Nginx
与Mysql
这类常用的镜像,早就有人帮我上传到公共仓库中,我们只需要直接从公共仓库中拉取即可,不需要在自己手动编写,所以我们真正需要自己自定义的镜像文件,只有我们的Django
项目。
使用Docker-compose
前面我们多次提到不要将多个组件放在一个容器中,而是分别为每个组件创建一个容器。而当我们要构建和运行多个组件的容器时,我们如果每个容器都去使用命令单独去构建和运行岂不是很麻烦?此时我们就可以使用Docker-compose
来解决这一问题。Docker-compose
可以同时编排多个容器,将多个容器的构建与启动操作放到一起执行。我们在docker-compose.yml
文件中写入如下内容:
version: "3"
# 定义用于共享的数据卷
volumes:
# 定一个名称为static-volumn的数据卷
static-volume:
# 定一个名称为media-volumn的数据卷
media-volume:
# 定义两个网络,只有处于同一网络下的容器才能通讯
networks:
web_network:
driver: bridge
db_network:
driver: bridge
services:
app:
# 除正常工作外,容器会在任何时候重启,比如遭遇 bug、进程崩溃、docker 重启等情况
restart: always
build:
# 构建镜像的上下文环境所在目录,相对当前docker-compose.yml的位置
context: ../../
# 指定用于构建镜像的Dockerfile(ps: 命名不需要一定为Dockerfile)
dockerfile: ./deployment/dev/app/Dockerfile
# 设置容器名字
container_name: room_dev
image: room_dev
command: "/wait-for-it.sh mysql:3306 -- /startapp.sh"
# 数据卷,使宿主机的内容可以映射到容器中,当宿主机指定目录的内容更新时,容器中的指定目录中的内容也会相应更新
# 所以使用数据卷时,一定要注意路径是否正确,否则会出现文件找不到或不存在的情况
volumes:
- ../../:/room
- static-volume:/room/collected_static
- media-volume:/room/media
# 暴露端口,但不暴露给宿主机,只给连接的服务访问
expose:
- "8000"
networks:
- web_network
- db_network
mysql:
image: mysql:5.7
restart: always
container_name: mysql
hostname: mysql # host的名字,在django中需要使用
environment:
- MYSQL_HOST=localhost # mysql的主机
- MYSQL_PORT=3306 # mysql的端口号
- MYSQL_ROOT_PASSWORD=123456 # root用户的密码
- MYSQL_DATABASE=room_dev # 数据库的名字
- MYSQL_USER=dev # 用户名
- MYSQL_PASSWORD=123456 # dev用户的密码
- MYSQL_ALLOW_EMPTY_PASSWORD=no # 设置是否允许root用户的密码为空
command:
--default-authentication-plugin=mysql_native_password
--character-set-server=utf8mb4
--collation-server=utf8mb4_general_ci
--explicit_defaults_for_timestamp=true
volumes:
# mysql的数据目录
- ../../../room_dev_db:/var/lib/mysql
ports:
- "3306:3306"
networks:
- db_network
nginx:
image: nginx:latest
restart: always
container_name: nginx
# 宿主机和容器的端口映射
ports:
- "8001:8001"
volumes:
- static-volume:/collected_static
- media-volume:/room_media_dev
- ./nginx:/etc/nginx/conf.d # 修改nginx的配置
networks:
- web_network
Docker-compose的安装请参考上章内容
上述文件内容有些多,我们逐一分析。在文件开头,有个version
,代表docker-compose.yml的版本,目前最新版为3,我们不需要改动它。然后文件下面的内容大致分为volumes
、networks
、app
、mysql
、nginx
五个模块。其中app
、mysql
、nginx
都是容器的配置,app
容器使用的镜像是我们上述的自定义镜像,mysql
与nginx
容器使用的镜像则是直接从公共仓库中拉取的镜像,我们先看下容器中的配置项:
- restart: 除正常工作外,容器会在任何时候重启,比如遭遇 bug、进程崩溃、docker 重启等情况。
-
command: 运行容器所需要执行的操作,这边是直接执行一个shell脚本,当然也可以直接执行命令,比如
python manage.py runserver 127.0.0.1:8000
- container_name: 设置容器的名字
-
image: 镜像的名字,在
app
这种使用自定义镜像的情况中,表示的是设置自定义镜像的名字。在mysql
和nginx
这种从仓库中拉取镜像的情况中,表示的是要从仓库中获取的镜像的名字,而不是重新设置镜像名字 - expose: 容器对外暴露端口,因为容器与外界隔离是连端口一起隔离的,但是web服务是需要调用端口的,因此需要将容器的端口对外暴露,但是要注意此命令不会对宿主机(即外界的系统操作环境)暴露端口,仅仅是对其他容器暴露端口
-
ports:定义了宿主机与容器之间的端口映射,写法是
宿主机的端口号:容器的端口号
,例如8000:5000
即表示访问宿主机的8000
端口就是访问容器的5000
端口,与expose
一样起到对外暴露端口的作用,但是ports
是针对宿主机而expose
是针对其他容器的 -
build: 用于指定构建镜像的上下文路径
-
context: 指定构建镜像的上下文环境所在目录,是相对配置文件
docker-compose.yml
的所在目录,../../
表示docker-compose.yml
所在目录的上级目录的上级目录,即最外层的room
目录,注意上述镜像配置文件中COPY
命令所涉及到的宿主机目录就是由context
指定的,例如COPY deployment/dev/app/startapp.sh /startapp.sh
中,因为context
指向最外层的room
目录,所以deployment/dev/app/startapp.sh
指的是最外层room
目录下的对应文件 -
dockerfile: 指定用于构建镜像的配置文件,即我们的前面编写的
Dockerfile
文件,其根路径就是上述context
指定的路径,即最层的room
,./deployment/dev/app/Dockerfile
即表示最外层room
目录下对应的文件
-
context: 指定构建镜像的上下文环境所在目录,是相对配置文件
-
depends_on: 一个容器需要依赖另一个容器,例如上述配置中
app
容器需要依赖mysql
容器,只有mysql
容器启动后,app
容器才能正常使用。但是mysql
容器第一次启动后,初始化需要一定的时间,而depends_on
并不会去等待mysql
初始化完成,再去启动app
,这时app
启动会因为mysql
没有初始化完成而失败,所以这时我们可以使用上述提到的wait-for-it.sh
脚本,使用方法是/wait-for-it.sh 容器名:容器端口号 -- 后续执行的命令
,例如/wait-for-it.sh mysql:3306 -- /startapp.sh
因为我们使用到了mysql
,所以django
中的配置需要做如下修改:
# *************************
# 数据库相关
# *************************
DATABASES = {
# 默认使用mysql数据库
"default": {
"ENGINE": "django.db.backends.mysql", # 数据库引擎
"NAME": "room_dev", # 数据库名
"USER": "dev", # 用户名
"PASSWORD": "123456", # 密码
"HOST": "mysql", # HOST,配置文件中的hostname
"POST": 3306, # 端口
"OPTIONS": {"charset": "utf8mb4"},
},
}
在上述容器配置项介绍中,除了volumes
与networks
外,对于其他配置项我们已经有了一些了解。下面我们单独讲解一下volumes
与networks
,尤其是volumes
,其在容器中起到至关重要的作用。
volumes
volumes
我们一般称为容器的卷。因为容器与容器以及容器与宿主机是相互隔离的,但有时我们又需要它们可以互相连通,例如当宿主机上的项目代码更新了,容器中的项目代码也需要更新,如果我们每次都去手动更新,岂不是很麻烦?另外,当容器被删除时,容器中的数据也会丢失,如果数据库的容器被误删,这导致的结果也不是我们可以接受的。而卷正好解决这些问题!
卷大致可以分为两种使用形式,
第一种:使用绝对路径
app:
......
volumes:
- ../../:/room
上述配置中,:
是分割符,:
的左边../../
是宿主机的目录(这个路径是相对docker-compose.yml
所在的目录,此处为docker-compose.yml
所在的目的上级目录的上级目录,即最外层的room
目录),:
的右边/room
是容器中的目录,此时表示宿主机的../../
与容器的/room
目录是连通的,当宿主机中的指定目录中的项目代码更新,容器中的/room
目录中的代码也会更新。
mysql:
......
volumes:
- ../../../room_dev_db:/var/lib/mysql
我们再看容器mysql
中的配置,../../../room_dev_db
表示在最外层room
目录外新建了一个room_dev_db
文件夹与mysql
容器中的/var/lib/mysql
(mysql存放数据的地方)目录连通。因为一开始room_dev_db
目录是空的,所以/var/lib/mysql
中的数据会映射到room_dev_db
中,这样在宿主机中就持有了数据库中的数据,即使我们不小心误删了mysql
容器,我们在宿主机中也有数据库的数据,不会造成数据丢失,当我们再次创建mysql
容器时,数据会自动恢复。
第二种:使用卷标
# 定义用于共享的数据卷
volumes:
# 定一个名称为static-volumn的数据卷
static-volume:
# 定一个名称为media-volumn的数据卷
media-volume:
在文件的头部,我们定义了两个卷,分别名为static-volume
与media-volume
。这两个卷目前是空的,没有任何数据。然后在app
容器中有一段配置:
app:
......
volumes:
......
- static-volume:/room/collected_static
static-volume:/room/collected_static
中,:
左边的static-volume
是我们在一开始在宿主机中定义的卷,:
右边的/room/collected_static
指的是容器中的目录,但此时的static-volume
只是个卷的名字,不是具体的宿主机中的路径,此处将容器中的/room/collected_static
的内容映射到宿主机的原来的空static-volume
卷中,使得static-volume
持有/room/collected_static
中的数据,而static-volume
的具体存储,则默认由宿主机上的docker统一管理。
nginx:
......
volumes:
- static-volume:/collected_static
在nginx
容器中有一段static-volume:/collected_static
,因为在上面app
容器中已经使static-volume
卷拥有了数据,使其不再是空的状态,此处是将static-volume
卷中的数据映射到nginx
容器的/collected_static
目录中,使得nginx
容器也可以访问static
资源,起到了数据共享的作用,这样app
与nginx
就是共用static-volume
这个卷,起到数据共享的作用。
在上述介绍数据卷时,我们几次提到数据卷的空状态与非空状态,其实从中可以看出,挂载空卷和非空卷是有区别的,所以我们需要记住以下数据卷的重要特性:
- 容器启动时,如果挂载一个空的数据卷到容器中的一个非空目录中,那么这个目录下的文件会被复制到数据卷中;
- 如果挂载一个非空数据卷到容器中的一个目录中,那么容器中的目录中会显示数据卷中的数据;如果原来容器中的目录中有数据,那么这些原始数据会被隐藏掉
举个例子,就以collected_static
的数据卷static-volume
来说,只要卷初始化完成后,容器原始的 collected_static
目录就被隐藏起来不再使用了,新增的文件也只存在于static-volume
卷中,容器中是没有的。
networks
# 定义两个网络,只有处于同一网络下的容器才能通讯
networks:
web_network:
driver: bridge
db_network:
driver: bridge
app:
……
networks:
- web_network
- db_network
mysql:
……
networks:
- db_network
nginx:
……
networks:
- web_network
在docker中可以给每个容器定义其工作网络,定义网络后可以隔离容器的网络环境,容器只有在相同的网络之中才能进行通讯。此处一共定义了两个网络web_network和db_network。在配置文件中可以看出,app
与nginx
都处于web_network
网络,且app
与mysql
处于db_network
网络,nginx
与mysql
因为所处网络不同,无法通讯。
四、总结
关于使用Docker+Django+Gunicorn+Mysql+Nginx部署项目的介绍就到此结束了,博主对于Docker的使用也只是停留在基础层面,如有不足之处,欢迎指正。有兴趣的同学可以关注公众号「Code满满」或者是博客「李益的小站」。另外此处推荐一个比较详细的系列文章:《Django+Docker容器化部署》,本文亦参考了此系列文章。
最后附上gunicorn
与nginx
的配置文件内容:
- gunicorn
# -*- coding: utf-8 -*-
import multiprocessing
from pathlib import Path
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent.parent.parent
debug = False
# ============================================================
# gunicorn要切换到的目的工作目录
# ============================================================
chdir = str(BASE_DIR)
# ============================================================
# server socket相关
# ============================================================
# 指定绑定的ip和端口
bind = "0.0.0.0:8000"
# 服务器中排队等待的最大连接数,建议值64-2048,超过2048时client连接会得到一个error
backlog = 2048
# ============================================================
# 调试相关
# ============================================================
# 当代码有修改时,自动重启workers,适用于开发环境
reload = False
# 以守护进程形式来运行Gunicorn进程,其实就是将这个服务放到后台去运行
daemon = False
# ============================================================
# worker进程相关
# ============================================================
# 用于处理工作的进程数
workers = multiprocessing.cpu_count() * 2 + 1
# worker进程的工作方式,有sync、eventlet、gevent、tornado、gthread, 默认是sync,
# django使用gevent容易造成阻塞, 使用gthread的方式好一些
worker_class = 'gthread'
# 指定每个工作进程开启的线程数
threads = multiprocessing.cpu_count() * 2
# 访问超时时间
timeout = 30
# 接收到restart信号后,worker可以在graceful_timeout时间内,继续处理完当前requests
graceful_timeout = 60
# server端保持连接时间
keepalive = 30
# ============================================================
# 日志相关
# ============================================================
"""日志文件格式,其每个选项的含义如下:
h remote address
l '-'
u currently '-', may be user name in future releases
t date of the request
r status line (e.g. ``GET / HTTP/1.1``)
s status
b response length or '-'
f referer
a user agent
T request time in seconds
D request time in microseconds
L request time in decimal seconds
p process ID
"""
access_log_format = '%(t)s %(h)s "%(r)s" %(s)s %(b)s "%(f)s" "%(L)s"'
LOG_DIR = Path(BASE_DIR, 'log')
if not LOG_DIR.exists():
LOG_DIR.mkdir(parents=True)
# 错误日志输出等级,访问日志的输出等级无法设置
loglevel = "error"
# 正常的日志文件路径,'-'表示输出到终端
accesslog = '-'
# 错误日志文件路径,'-'表示输出到终端
errorlog = str(LOG_DIR / 'gunicorn_error.log')
# ============================================================
# 进程名相关
# ============================================================
# 设置进程名称,默认是gunicorn
proc_name = 'gunicorn_room'
- nginx
#user root;
# 将请求分发到upstream池中的服务器中
upstream app {
ip_hash;
server app:8000;
}
server {
# nginx监听的端口
listen 8001;
server_name localhost;
# 限制用户上传文件大小
client_max_body_size 5M;
location /media {
alias /room_media_dev;
}
# 访问一些其他静态文件,直接交给nginx处理,可以指向django项目中配置的STATIC_ROOT路径
# 这里的 /static 的意思就是你的域名加上/static/就访问里面的那个路径/项目了
# alias:直接查询指定路径
location /static {
alias /collected_static/;
}
location / {
proxy_pass http://app/; # 将指定的ip或者域名的http请求转发到upstream池中
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}