目录
前期准备
开发环境
软件安装
创建Django+Vue前后端分离项目
后端工程结构
前端工程结构
docker化工程结构
基于Docker和Docker-compose构建项目
初识Docker
几个常用docker指令
Dockerfile
初识Docker-compose
docker-compose常用命令
docker-compose.yml
其他配置
结语
彩蛋
本文将致力于实现搭建一个容器化的使用nginx做代理中间件的 Django+Vue前后端分离的项目。
整个项目的开发环境是在windows下,但是docker化发行部署是在linux里完成的。
Docker:学习 Docker 当然要安装 Docker 软件了(免费的社区版),安装方法见官方文档。
Docker-compose:这是 Docker 官方推出的用于编排、运行多个容器的工具,安装方法见官方文档。本教程大部分内容都与它有关。
Python3:教程部署的是 Django 项目,那 Python3 是当然要有的了(包括 python 的包管理工具 pip)。
准备就绪后就继续下一步吧。
本文重点是docker化该项目,整个试验项目预先已基本完成。接下来是对项目结构做简要的介绍。
图1中,主要配置信息都在./backEnd/settings.py文件中编写好,其中后端使用的监听端口是8000。
轻省起见,数据库直接使用了默认的sqllite3。
图2中,dist文件夹是执行下行代码,
npm run build
然后发行的前端文件。在docker化构建中,将直接就该文件夹做处理。
事实上,vue的项目docker化,应该是从源码执行打包命令开始的,此处跳过了那一步,降低了整体难度,有时间的话我会重新完整走一遍的。
前端的主要配置信息是在./cms/config/index.js中:
图3所展示的部分中,"port:8080"指定的是前端的监听端口是8080。因为是前后端分离的项目,会涉及到跨域,proxyTable处理的便是跨域问题,当遇到请求中带有"/api"字样的url,便会将其及之前内容替换成后端的ip。
上文说到,在构建前端镜像时,直接使用发行后的dist文件。所以,在docker化构建的过程中,工程目录如图4所示。这样放在一起,而不分开构建镜像的目的是为了使用docker-compose对两个镜像统一编排。
Docker 的整个生命周期由三部分组成:镜像(image)+ 容器(container)+ 仓库(repository)。
容器是由镜像实例化而来,这有点像面向对象的概念:镜像就是类,容器是类实例化之后的对象。
镜像是一个只读的模板,它包括了运行容器所需的数据。镜像可以包含一个完整的 Linux 操作环境,里面仅安装了 Python 或者其他用户需要的程序。
容器是由镜像创建出来的实例,类似虚拟机,里面可以运行特定的应用,并且容器与容器是相互隔离的。
仓库概念与 Git 和 Github 类似,如果你用过它们就非常容易理解。Docker 使用的默认仓库是由官方维护的 Docker hub 公共仓库,从中上传、拉取的操作类似 Git。
我们可以用 docker images
查看本地已有的镜像:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world latest fce289e99eb9 9 months ago 1.84kB
表列分别为镜像名、版本、ID 号、创建时间、大小。
还可以查看本地已有的容器:
$ docker ps -a
CONTAINER ID IMAGE .. CREATED ..
38cb03a96dca hello-world .. 2 minutes ago ..
除此之外还有一些非常有用的基础指令:
docker rmi [images ID] # 删除此 ID 的镜像
docker container stop [container ID] # 停止此 ID 的容器
docker container start [container ID] # 启动此 ID 的容器
docker container rm [container ID] # 删除此 ID 的容器
Docker 允许通过文本格式的配置文件来构建镜像,默认名称为 Dockerfile。因此在项目根目录新建文件 Dockerfile,写入:
1.对dockerfile与Dockerfile,docker-compose不区分大小写;
2.此处的项目跟目录是指后端的根目录,具体位置可参考图4.
FROM python:3.7
ENV PYTHONUNBUFFERED 1
RUN echo \
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ buster main contrib non-free\
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ buster-updates main contrib non-free\
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ buster-backports main contrib non-free\
deb https://mirrors.tuna.tsinghua.edu.cn/debian-security buster/updates main contrib non-free\
> /etc/apt/sources.list
RUN mkdir /code
WORKDIR /code
RUN pip install pip -U -i https://pypi.tuna.tsinghua.edu.cn/simple
ADD ./requirements.txt /code/
RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
ADD . /code/
理解这些 Docker 指令的关键在于,一定要牢记容器里的环境和外界(宿主机)是隔离的,它两是完全不一样的。换句话说,要搞清楚哪些操作是针对宿主机、哪些操作是针对容器。
FROM python:3.7
指令从仓库拉取一个包含 python 3.7 的 Linux 操作系统环境(Linux 版本为 Debian)。
RUN
和 WORKDIR
指令都是针对容器的,功能是在容器里创建目录、并将其设置为工作目录。注意宿主机是没有这个目录的。
ADD
指令出现了两次。ADD requirements.txt /code/
意思是将宿主机当前目录(即 Dockerfile 所在目录)的 requirements.txt
文件复制到容器的 /code
目录中。ADD . /code/
意思是把当前目录所有内容复制到容器 /code/
目录,注意中间那个点。
在线上环境中,通常不会将项目的所有组件放到同一个容器中;更好的做法是把每个独立的功能装进单独的容器,这样方便复用。比如将 Django 代码放到容器A,将 Mysql 数据库放到容器B,将vue代码放到容器C,以此类推。
因此同一个服务器上有可能会运行着多个容器,如果每次都靠一条条指令去启动,未免也太繁琐了。 Docker-compose
就是解决这个问题的,它用来编排多个容器,将启动容器的命令统一写到 docker-compose.yml 文件中,以后每次启动这一组容器时,只需要 docker-compose up
就可以了。因此教程也会用 docker-compose 来管理容器。
Docker Compose 将所管理的容器分为三层,分别是工程(project)、服务(service)、容器(container)
Docker Compose 运行目录下的所有文件(docker-compose.yml)组成一个工程,一个工程包含多个服务,每个服务中定义了容器运行的镜像、参数、依赖,一个服务可包括多个容器实例。
docker-compose up # 重新构建并启动项目中所有容器
docker-compose up -d # 在后台重新构建并启动项目中所有容器
docker-compose ps # 列出项目中目前的所有容器
docker-compose stop # 停止正在运行的容器
docker-compose start # 再次启动已停止的容器
docker-compose logs # 查看服务容器的输出
在项目根目录下创建 docker-compose.yml 并写入:
此处的根目录,我是用./devops当的。常理来说,一般是放在有dockerfile的那个根目录,但是我想同时编排和组织前、后端两个不同的镜像,因为它们本身是前后端分离的,所以抽了出来,放在同时包含backEnd(后端)与cms(前端)文件夹的父目录里。执行时,只需定位到./devops,然后输入docker-compose up即可。至于docker-compose怎么去寻找相应的dockerfile,详见docker-compose.yml代码,可以指明每个镜像的构建路径,docker-compose就可以去相应的路径里寻找相应的dockerfile。
version: "3"
services:
app:
restart: always
build: ./backEnd # '点'代表当前目录; ./backEnd将构建路径指向后端
command: bash -c "python3 manage.py migrate && python3 manage.py makemigrations &&python3 manage.py runserver 0.0.0.0:8000"
# 使用'bash -c'以允许command里同时输入并次第执行多个指令。这批指令作用于后端容器。
volumes:
- ./backEnd:/code
expose:
- "8000"
networks:
- web_network
vue_nginx:
restart: always
image: nginx:latest
ports:
- "80:8080" # 将宿主机的80端口与该容器内的8080端口进行映射
volumes:
- ./cms/dist/:/usr/share/nginx/html/
- ./config/nginx:/etc/nginx/conf.d
depends_on:
- app
networks:
- web_network
networks:
web_network:
driver: bridge
volumes:
static-volume:
让我们来分解一下其中的各项含义。
version 代表 docker-compose.yml 的版本,目前最新版为 3,不需要改动它。
接着定义了一个名叫 app
的容器。下面的内容是 app 容器的相关配置:
restart :除正常工作外,容器会在任何时候重启,比如遭遇 bug、进程崩溃、docker 重启等情况。
build :指定一个包含 Dockerfile 的路径,并通过此 Dockerfile 来构建容器镜像。"./backEnd" 指向后端所在位置。
command :容器运行时需要执行的命令。这里就是我们很熟悉的运行开发服务器了,我让其在运行前先执行数据迁移;。
volumes :卷,这是个很重要的概念。前面说过容器是和宿主机完全隔离的,但是有些时候又需要将其连通;比如我们开发的 Django 项目代码常常会更新,并且更新时还依赖如 Git 之类的程序,在容器里操作就显得不太方便。所以就有卷,它定义了宿主机和容器之间的映射:"./backEnd" 表示宿主机的相对工作目录,":" 为分隔符,"/code" 表示容器中的目录。即宿主机的后端工作目录和容器的 /code 目录是连通的,宿主机后端工作目录的 Django 代码更新时,容器中的 /code 目录中的代码也相应的更新了。这有点儿像是在容器上打了一个洞,某种程度上也是实用性和隔离性的一种妥协。可以看到,在使用sqllite3的情况下,随着后端容器内数据库数据量的增加,实际上宿主机的后端工作目录里的sqlite3也是同步增长的。
严格意义上讲,这里用到的
./backEnd:/code
并不是卷,而是叫挂载,它两是有区别的,只不过 docker-compose 允许将挂载写到卷的配置中。后面会讲到。
expose:将这个容器的8000端口暴露给其他容器,但不暴露给宿主机。
然后定义了一个名为vue_nginx的容器。下面的内容是 vue_nginx 容器的相关配置:
image:如何构建 nginx 镜像?这么常用的镜像官方已经帮我们构建好了,只需要把它从仓库拉取到本地就可以了。我所拉取的是最新版本。
ports :定义了宿主机和容器的端口映射。容器的隔离不止环境,甚至连端口都隔离起来了。但 web 应用不通过端口跟外界通信当然不行,因此这里定义将宿主机的 80 端口映射到容器的 8080 端口,即访问宿主机的 80 端口就是访问到了这个容器的 8080 端口,但要确保端口没有被其他程序占用。
volumes:定义卷(这里实际是挂载),上面已经讲过了,它实现了宿主机和容器目录的映射。这里,"./cms/dist/:/usr/share/nginx/html/",使nginx托管了前端的发行文件,属于前端范畴,故给这个容器命名vue_nginx。
但是因为这是前后端分离项目,而且Django的开发服务器太过脆弱,所以采用Nginx做为代理。"./config/nginx:/etc/nginx/conf.d"将宿主机相应目录里关于前后端使用nginx的配置文件放入nginx容器里的对应位置。关于nginx配置文件,会在篇尾呈现。
epends_on
,意思是此容器需要等待 app容器启动完毕才能够启动
网络 network
Docker 允许用户给每个容器定义其工作的网络,只有在相同的网络之中才能进行通讯。你可以看到 app 和 vue_nginx 容器都处于 采用桥接模式的 web_network 网中,所以二者可以直接通信。
定义网络可以隔离容器的网络环境,也方便运维人员一眼看出网络的逻辑关系。
数据卷
之前我们见识过的用于映射宿主机和容器目录的卷了,实际上称为挂载;现在新出现的 static-volume 才叫卷。它的使用方式像这样:static-volume:/code/collected_static
,冒号后面还是容器内的目录,但冒号前的却不是宿主机目录、仅仅是卷的名称而已。从本质上讲,数据卷也是实现了宿主机和容器的目录映射,但是数据卷是由 Docker 进行管理的,你甚至都不需要知道数据卷保存在宿主机的具体位置。
相比挂载,数据卷的优点是由于是 Docker 统一管理的,不存在由于权限不够引发的挂载问题,也不需要在不同服务器指定不同的路径;缺点是它不太适合单配置文件的映射。
和挂载一样,数据卷的生命周期脱离了容器,删除容器之后卷还是存在的。下次构建镜像时,指定卷的名称就可以继续使用了。
其实在此次试验中并没有用到static-volume。
最后就是关于前后端对nginx的配置文件了。
django_app.conf
# django_app.conf
upstream app {
ip_hash;
server app:8000;
}
server {
listen 8000;
server_name localhost;
# location /static/ {
# autoindex on;
# alias /code/collected_static/;
# }
location / {
proxy_pass http://app/;
}
}
vue_app.conf
# vue_app.conf
upstream vue_nginx {
ip_hash;
server vue_nginx:8080;
}
server {
listen 8080;
server_name localhost;
# location /static/ {
# autoindex on;
# alias /code/collected_static/;
# }
location / {
# root 根目录,默认nginx下的html文件夹,可以指定其他
root /usr/share/nginx/html;
index index.html;
# vue的路由并不是真实物理路由,所以用try_files,路径都指向根目录下的index.html
try_files $uri $uri/ /index.html;
}
# 代理配置,解决跨域问题
location /api {
proxy_pass http://127.0.0.1:8000/;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
在vue_app.conf中,通过设置location /api{…}完成跨域。nginx在匹配到前端发出的携带"http://127.0.0.1:8080/api/……"的请求,会将之重写成"http://127.0.0.1:8080/……",以使nginx代理的8000端口能够收到,并转发给app容器。(请结合本文开头的项目工程结构来读)。
至此就完成了整个项目的构建了。只需回到./devops目录下,输入docker-compose up -d ,待启动成功后,即可使用http://localhost。或者服务器的话,从公网ip访问,记得在安全组中开放80端口。
经过这次实验,整个知识体系就已经走通了。
感谢你坚持读到这里( •̀ ω •́ )✧。
其实还有一些升级打怪的剩余情节。比如: