docker

软件开发面临的问题之一就是环境配置问题,当前的软件环境可以运行程序,但是换一台服务器由于环境的不同可能导致原先能正常运行的程序跑不起来,也就意味着换一台机器,就要重新配置一遍环境,非常费时费力。于是就想着可不可以安装软件的时候把之前的环境也带着。如果把一台服务器看做是一艘货船,每个软件及依赖的环境看做是一个个集装箱,就可以很好的理解docker在服务器中的作用。

docker 与 虚拟机的区别

虚拟机 :在一种操作系统里运行另外一种操作系统,比如在windows系统中运行linux,虚拟机上的应用程序是毫无感知的,因为看上去跟真实的系统一模一样,而对于底层的系统来说,虚拟机就是一个普通的文件。但这个解决方案有几个缺点。

(1) 占用资源多 。虚拟机会占用一部分内存和磁盘空间,虚拟机运行的时候其他程序就不能使用这些资源。哪怕虚拟机里面的应用程序真正使用的内存只有1MB, 虚拟机依然需要几百MB才能运行。

(2) 步骤多 。 虚拟机是完整的操作系统,一些系统级别的步骤无法跳过。

(3) 启动慢 。启动操作系统需要多久,启动虚拟机就需要多久。

linux容器 :linux容器不是模拟一个完整的操作系统,而是对进程进行隔离,可以理解为在正常的进程外面套了一个保护层,对于容器里面的进程来说,它接触到的资源都是虚拟的,从而实现了与底层操作系统的隔离。与虚拟机相比它的优势在于

(1) 启动快 。 容器里面的应用就是底层操作系统的一个进程,不同于虚拟机里面的进程。所以启动容器相当于启动本机一个进程,速度就会快很多。

(2) 资源占用少 。容器只会占用需要的资源,而虚拟机是完整的操作系统不可避免要占用所有资源。另外多个容器可以共享资源,虚拟机都是独享资源。

(3) 体积小。 容器只包含用到的组件,虚拟机是整个操作系统的打包,所以容器文件比虚拟机文件小很多。总之容器有点像轻量级的虚拟机,能够提供虚拟化的环境,但是成本开销会小很多。

docker :docker属于linux容器的一种封装,提供接口。docker将应用程序与该程序的依赖打包在一个文件里面,运行这个文件,就会生成一个虚拟容器,程序在这个虚拟容器里面运行就像在真实的物理机上面运行一样。

docker 镜像文件,容器,仓库

上面解释了docker是linux容器的封装,docker会把应用程序及其依赖打包到image文件里面,只有通过这个image文件才能生成docker容器,所以image文件可以看做是容器的模板,docker根据image文件生成容器的实例。同一个image文件可以同时生成多个容器实例。由于image文件是通用的,我们可以用docker镜像仓库中获取基本的镜像加工。也可以将自己写的镜像上传到仓库。

# 从docker仓库获取hello-world镜像
docker image pull hello-world
# 列出本机的docker镜像
docker images
# 根据image文件生成一个正在运行的容器实例
docker run --name=hello-world fce289e99eb9
# docker container run 命令具有自动抓取 image 文件的功能。如果发现本地没有指定的 image 文件,就会从仓库自动抓取。
# 输出下面这段提示后,hello world就会停止运行,容器终止,有些不会终止,因为提供的是服务。

docker部署react项目 nginx docker部署django nginx_mysql

Image 文件生成的容器实例本身也是一个文件,叫做容器文件,也就是说,容器一旦生成,就会同时存在两个文件,image文件和容器文件。而且关闭容器不会删除容器文件,只会让容器停止运行。

# 列出本机正在运行的容器
docker ps
# 列出本机所有容器,包括终止运行的容器
docker ps -a
# 删除本机镜像
docker image rm [imageID]
# 删除本机容器
docker rm [containID]

docker部署react项目 nginx docker部署django nginx_docker_02

dockerfile

在知道如何使用image文件后,接下来就是如何制作自己的镜像,这时候就需要用到dockerfile,它是一个文本文件用来构建镜像。镜像构建的过程中有一个非常重要的概念就是分层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。

在项目的根目录下新建一个Dockerfile文件,写入下面内容

# 基础镜像使用官方的python image : 后面表示标签,3.6也就是3.6版本
FROM python:3.6
# 在镜像中创建目录,用来存放本机中的django项目
RUN mkdir /usr/src/app
# 将本机 . 也就是当前目录下所有文件都拷贝到image文件中指定目录
COPY . /usr/src/app
# 将/usr/src/app指定为工作目录
WORKDIR /usr/src/app
# 在image中安装运行django项目所需要的依赖
RUN pip install -r requirements.txt
# 开放容器的8080端口,允许外部链接这个端口
EXPOSE 8080
# 执行django启动命令
CMD ["python", "manage.py", "runserver", "0.0.0.0:8080"]
# 执行构建image命令,-t参数用来指定image文件的名字,后面还可以用:指定标签,如果不指定默认标签就是latest
# 最后的点儿 . 表示dockerfile文件所在路径,也就是当前路径。
docker image build -t django-docker .

docker部署react项目 nginx docker部署django nginx_django_03

生成容器

既然已经制作好了镜像,那下一步就是根据镜像生成容器。

docker run -p 8000:8080 -it -d django-docker
# -p 参数:容器的8080端口映射到本机的8000端口
# -it 参数:容器的shell映射到当前的shell,在本机窗口输入命令,就会传入容器
# -d 参数:在后台运行程序
# django-docker 参数: image文件的名字
docker container logs [containID]
# 打印指定容器的日志

docker部署react项目 nginx docker部署django nginx_mysql_04

宿主机与容器间的通讯

在上面的例子中,访问宿主机8000端口,宿主机成功的把网络请求转发到了容器的8080上,下面我们来看一下宿主机是如何与容器进行通讯的

docker目前支持五种网络模式,在使用docker run创建docker容器时,可以用-net指定网络模式

host模式:与宿主机共享网络,此时容器没有使用网络的namespace,宿主机的所有设备,如Dbus会暴露到容器中,因此存在安全隐患
container模式:指定与某个容器实例共享网络。
none模式:不设置网络,相当于容器内没有配置网卡,用户可以手动配置。
bridge模式:默认设置。此时docker引擎会创建一个veth对,一端连接到容器实例并命名为eth0,另一端连接到指定的网桥中(比如docker0),因此同在一个主机的容器实例由于连接在同一个网桥中,它们能够互相通信。容器创建时还会自动创建一条SNAT规则,用于容器与外部通信时。如果用户使用了-p或者-Pe端口端口,还会创建对应的端口映射规则。
自定义模式:使用自定义网络,可以使用docker network create创建,并且默认支持多种网络驱动,用户可以自由创建桥接网络或者overlay网络
# 产看指定容器的网络环境
docker inspect [containID]

"Networks": {
	"bridge": {
  	"IPAMConfig": null,
    "Links": null,
    "Aliases": null,
    "NetworkID": "8b278f6188d8dd6f8b5654f158a69b911415b3821c07ac1b2fc6d4346c5ba404",
    "EndpointID": "840cbf80021f622b854344c1bea9213dab8b77540b91ebbcf5fc7e513189c953",
    "Gateway": "172.17.0.1",
    "IPAddress": "172.17.0.2",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "MacAddress": "02:42:ac:11:00:02",
    "DriverOpts": null
}
我们可以看到这个容器默认使用了bridge桥接模式的方式通信,这个容器的ip地址是172.17.0.2 网关地址是172.17.0.1


[root@ecs-s2-medium-2-linux-20190723145945 ~]# ifconfig docker0
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        inet6 fe80::42:75ff:fe21:7b9e  prefixlen 64  scopeid 0x20<link>
        ether 02:42:75:21:7b:9e  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 8  bytes 656 (656.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
        
使用ifconfig查看网卡,发现一块docker0网卡,而这个网卡地址是172.17.0.1,跟容器的网关地址相同,我们可以大胆的猜测容器就是通过这张docker0网卡进行通信

docker服务在启动时会创建一块名为docker0的虚拟网卡,在docker初始化的时候系统会分配一个ip地址绑定在这个网卡上,docker0的角色就是一个宿主机与容器之间的网桥,作为一个二层交换机,负责数据包的转发,当使用docker创建一个时,如果使用了bridge模式,docker就会创建一个vet对,一端绑定在docker0上,另一端则作为容器的eth0虚拟网卡

docker部署react项目 nginx docker部署django nginx_docker-compose_05

我们如果把上面的图看做是修路搭桥,那么真正实现端口转发的nat规则就是数据包传递的导航,当容器使用-p指定映射端口时,docker会通过iptables创建一条nat规则,把宿主机发送到映射端口的数据包转发到docker0网关,docker0网关通过广播找到对应ip的目标容器,把数据包转发到容器端口上。反过来如果docker要跟外部的网络进行通信,也是通过docker0和iptables的nat进行转发,再由宿主机的物理网卡进行处理,使得外部可以感知不到容器的存在

docker部署react项目 nginx docker部署django nginx_nginx_06

两个容器的连接

在实际项目中不可能只有单独一个django进程,它可能包括redis,mysql,nginx,等等程序进程,docker镜像的构建建议是一个容器只跑一个进程,所以这就涉及到了容器之间的连接,我们先来看一下两个容器之间的连接。我们给之前的django项目增加一个功能,统计访问页面的次数,使用redis来统计数量。

  1. 首先拉取并启动一个redis容器

docker部署react项目 nginx docker部署django nginx_mysql_07

  1. 修改之前的django代码,在requirements中添加redis依赖,重新生成镜像
from django.shortcuts import render
 from django.http import HttpResponse
 import redis
 # 注意这里的redisdb
 rds = redis.StrictRedis('redisdb', 6379)
 
 def hello(request):
     rds.incr('count', 1)
     cnt = rds.get('count')
     cnt = b'0' if cnt is None else cnt
     return HttpResponse(cnt.decode())
  1. 使用新的生成容器的语句来创建容器并且与之前拉取的redis容器建立关联
docker run -it -d --name=djangoapp -p 8000:8080 --link=redis:redisdb docker-django

# --name=djangoapp 是这个容器的名字
# -p 宿主机8000端口与容器8080端口建立映射
# --link=redis:redisdb 与redis这个容器建立联系,redis别名叫做redisdb,在django程序中用到
# docker-django 基于docker-django这个镜像构建的容器
  1. 测试

docker部署react项目 nginx docker部署django nginx_django_08

多个容器之间的连接docker-compose

上面说了两个容器之间相互关联的方法,假如上面的django项目中用到了mysql,nginx和其他别的服务,每个服务都单独创建一个容器,每次构建启动都要重复相同的动作,就会变得很繁琐,而且此时这几个docker都是分散独立的,很不方便管理,既然这些个docker容器都是为了一个网站项目服务,就应该把他们放到一起,于是就有了docker-compose项目。

#指定yml文件和项目名称
docker-compose -f docker-compose.yml -p dongodng up -d
#进行所需的服务镜像构建
docker-compose build
#打印出详细的config文件
docker-compose config
#创建容器但是不运行
docker-compose create
#停掉服务,删除容器,不删除镜像
docker-compose down
#接受服务之间的互动事件,如进行健康检查等
docker-compose events
#对某个容器执行命令
docker-compose exec 容器名称 命令
#对某个服务查看日志
docker-compose logs -ft mysql
#查看服务状态
docker-compose ps
#重启服务
docker-compose restart/start/stop [服务名称]
#运行某个服务
docker-compose run [服务名称]
#查看服务中使用的镜像
docker-compose images [服务名称]
#强制停止容器,删除
docker-compose kill
#删除停止的容器
docker-compose rm

#想要重启单个服务容器
#先进行停掉服务
docker-compose stop test1
#然后删除容器
docker-compose rm
#再次启动该服务
docker-compose up -d  test1

####使用docker-compose 编写部署django + redis + mysql + nginx项目

使用不同的容器部署项目的时候首先要考虑容器之间的依赖关系,比如nginx要依赖web服务器,如果web服务器不工作,nginx就不能正常工作,web服务器要依赖于数据库等,其次还要设置好容器间的数据共享,比如web应用的静态资源,怎么样让nginx实现反向代理。

  1. 项目目录

docker部署react项目 nginx docker部署django nginx_docker-compose_09

  1. 我们首先看django_docker中的Dockerfile
FROM python:3.6

RUN mkdir /usr/src/app

COPY . /usr/src/app

WORKDIR /usr/src/app
# 解决mysql报错RuntimeError: cryptography is required for sha256_password or caching_sha2_p
RUN pip install cryptography

RUN pip install -r requirements.txt

EXPOSE 8080
  1. 将启动命令写成脚本
#!/bin/bash

python manage.py collectstatic --noinput &&
python manage.py makemigrations &&
python manage.py migrate &&
python manage.py runserver 0.0.0.0:8080
  1. nginx的Dockerfile
# 使用docker仓库中的基础nginx镜像
FROM nginx
# 开放端口
EXPOSE 80 8000
# 删除nginx原有配置文件
RUN rm /etc/nginx/conf.d/default.conf
# 将自己写的配置文件添加到容器
ADD nginx.conf /etc/nginx/conf.d/
# 创建web应用的静态文件存储
RUN mkdir -p /usr/share/nginx/html/static
RUN mkdir -p /usr/share/nginx/html/media
  1. nginx.conf
server {
    listen      80;
    server_name localhost;
    charset     utf-8;

    error_log /tmp/nginx_error.log;
    access_log /tmp/nginx_access.log;


    location /media {
        alias /usr/share/nginx/html/media;
    }

    location /static {
        alias /usr/share/nginx/html/static;
        }

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_pass http://django:8080;
    }

}

# location 静态文件配置,该目录下的静态文件要与web容器中的同步,通过docker-compose中的volumes
# proxy_pass 这里的host不能写成ip,要写在docker-compose中的web服务名(django)
  1. docker-compose的配置
version: '3'
services:
  db:
    image: mysql
    expose:
      - "3306"
    volumes:
      - ./mysql:/var/lib/mysql:rw
    environment:
      - MYSQL_DATABASE=demodb
      - MYSQL_ROOT_PASSWORD=123456

  redis:
    image: redis:3.2.1
    ports:
      - "6379:6379"

  django:
    build:
      context: ./django_docker
      dockerfile: Dockerfile
    volumes:
      - ./django_docker:/usr/src/app
      - /tmp/logs:/tmp
    command: bash start.sh
    ports:
      - "8080:8080"
    links:
      - redis:redisdb
    depends_on:
      - redis
      - db
    restart: always

  nginx:
    build: ./nginx
    ports:
      - "80:80"
    volumes:
      - ./django_docker/static:/usr/share/nginx/html/static:ro
      - ./django_docker/media:/usr/share/nginx/html/media:ro
    links:
      - django
    depends_on:
      - django
    restart: always
  1. Docker-compose是用yaml语言写的,简单说一下书写规则
大小写敏感
使用缩进表示层级关系
缩进时不允许使用Tab键,只允许使用空格。
缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
  1. 我们可以看到docker-compose定义了五个服务,我们解释一下每个服务
# db
image: mysql 从docker仓库中获得基础的mysql镜像
expose: 开放3306端口
volumes: 数据卷,为了实现备份用的,将容器中的/var/lib/mysql挂载在宿主机下的./mysql文件夹下
environment: 环境变量,设置了数据库名字和密码,会在settings.py用到

# redis
image: 也是使用docker仓库中的基础镜像
ports: 端口映射绑定容器6379端口与主机的6379端口,而expose只会把端口暴露给其他容器,主机无法访问。

# django
build: 指定dockerfile所在目录以及名称一般都叫Dockerfile
volumes: 跟db中作用一样也是为了备份,将容器中的文件与宿主机中的同步
command: 执行启动命令
ports: 与redis中作用相同
links: 与redis容器相关连。redisdb这个名字将在django项目中被识别
depends_on: 指定了djano服务启动需要依赖的服务,也给服务的启动指定了顺序
restart: 出现错误会自动重启

# nginx
这里面的配置的功能在与django中的大体相似,就不在介绍了。重点说说这里面的volumes,也就是如何实现nginx容器和django容器之间的数据共享。也就是静态文件共享的问题。首先我们已经在django容器中设置了volumes数据备份,就是将容器中的项目同步到了主机上的./django_docker中,然后主机就可以充当一个中间者,nginx容器再从主机同步静态文件。nginx <> 主机 <> django
  1. django 项目中settings.py model.py views.py index.html的代码
# settings.py

DATABASES = {
     'default': {
         'ENGINE': 'django.db.backends.mysql',
         'NAME': 'demodb',       # db环境变量设置的demodb
         'USER': 'root',
         'PASSWORD': '123456',   # db环境变量里设置的密码123456
         'HOST': 'db',
         'PORT': '3306',
     }
 }
 
STATIC_URL = '/static/'
STATIC_ROOT=os.path.join(BASE_DIR,"/static/")

# model.py

class School(models.Model):
     id = models.AutoField(primary_key=True)
     school_name = models.CharField(max_length=255, blank=False, null=False)

     class Meta:
         db_table = 'school'

# views.py

rds = redis.StrictRedis('redisdb', 6379)


def hello(request):
     rds.incr('count', 1)
     cnt = rds.get('count')
     cnt = b'0' if cnt is None else cnt
     res = School.objects.values_list('school_name', flat=True)
     context = {'cnt': cnt, 'res': res}
     return render(request, 'index.html', context)

# index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>访问人数{{cnt}}</p>
<p>学校名称{{res}}</p>
</body>
</html>
  1. 一切准备就绪开始构建
docker-compose build # 开始构建生成镜像
docker-compose up -d # 后台启动容器

docker部署react项目 nginx docker部署django nginx_docker_10

  1. django.core.exceptions.ImproperlyConfigured: mysqlclient 1.3.13 or newer is required; you have 0.9.3.

我们通过下面的图可以看到django容器出错并且在一直重启,我们打印报错信息发现是mysqlclient的错误,解决方法很简单就是将base.py中的36,37行注释掉就可以。但是我们发现由于容器不是启动状态所以我们无法进入容器中修改文件。

docker部署react项目 nginx docker部署django nginx_django_11

我目前想到的解决方法是这样的,将容器中的base.p文件copy到主机,然后在主机上修改完后再copy回去将源文件覆盖。

docker部署react项目 nginx docker部署django nginx_nginx_12

至此整个项目就成功启动了,但是由于我们mysql数据库没有数据所以我们进入到mysql容器中手动插入两条数据测试

docker部署react项目 nginx docker部署django nginx_mysql_13

  1. 访问127.0.0.1:80

docker部署react项目 nginx docker部署django nginx_docker-compose_14

项目的github地址https://github.com/zhao-dapeng/docker_django 跟着一步步搭建起来的记得点个start