docker是容器技术的典型代表,大家学习任何的关于容器的相关知识都会涉及到虚拟机和容器的区别。

我们可以通过创建虚拟机的方式让不同的应用运行在不同的虚拟机当中实现隔离,容器技术也可以做到应用的隔离,而且他的虚拟化技术更方便更小巧,因为容器不需要虚拟化,而且不需要安装操作系统,他可以直接在本地的操作系统之上就可以实现容器的隔离。

容器有两大热门技术,第一个是docker第二个是kubernetes,学习docker就不得不了解kubernetes。

kubernetes是一个容器编排的工具,在实际的生产环境中我们有成千上万个容器需要去创建,这就需要容器编排工具去帮我们做这个事,当然容器编排工具不只有kubernetes,docker有自己的容器编排工具。

容器化技术

在很久以前,如果我们想要去部署一个app,我们需要准备一台物理服务器,然后在这台服务器上面安装一个操作系统,然后在操作系统里面去部署这个app。这种方式部署非常慢,而且成本比较高,会有很大的资源浪费,只是部署一个app就要购买一台服务器。也难于迁移和扩展。

随着发展出现了虚拟化的技术,虚拟化技术的原理也很简单,就是基于计算机的基础上,虚拟化一套物理资源,然后基于虚拟的物理资源安装操作系统。这样一台物理机可以部署多个app。

对于虚拟机来说每一个虚拟机都是一个完整的操作系统,要给其分配资源,当虚拟机数量增多的时候操作系统本身消耗的资源也会增多。

容器解决了开发和运维之间的矛盾,在开发和运维之间搭建了一个桥梁,是实现devops的最佳解决方案。

容器是对软件和软件依赖的表转化打包,可以实现应用之间的相互隔离,容器是共享同一个OS Kernel, 不同的容器是在OS Kernel上运行的。

容器是在app层面的隔离,虚拟化是在屋里层面做的隔离。虚拟机的实现首先是服务器上创建了虚拟的屋里设备,然后基于虚拟的屋里设备安装操作系统,在操作系统中部署app。容器是在服务器上安装docker,然后docker创建容器,在容器中部署app。所以虚拟机中每个虚拟机有自己独立的操作系统,容器中所有容器共享一个操作系统。

也可以把虚拟化和容器结合使用。在虚拟器当中安装docker。

docker是容器技术的一种实现,也就是说容器技术不只有docker。

docker是2013年推出的,但是在此之前的2004年和2008,容器技术就已经开始在linux中使用了。docker底层就是基于2008年的LXC实现的。docker分为企业版和社区版,企业版是收费的。

VirtualBox是一款mac系统的虚拟工具,可以创建虚拟机并选择安装的系统,创建之后安装系统就可以了。我们也可以使用vagrant快速创建一台虚拟机。

首先我们要确定已经安装好了VirtualBox,然后再去安装Vagrant。我们首先需要下载他。安装之后可以在命令行使用vagrant命令。

vagrant --help

// 创建linux的虚拟机。
mkdir centos7
cd centos7
# 会初始化一个vagrant file, 描述vagrant配置。
vagrant init centos/7
# 运行虚拟机,第一次会去下载这个系统,会有点慢,后面就会快很多。
vagrant up

安装之后我们可以使用vagrant ssh命令进入到centos7的环境中。

我们可以通过vagrant status查看状态。也可以使用vagrant halt停止运行。

可以使用vagrant destory销毁这个机器。

这里不建议直接在mac或者windows电脑中直接使用docker,更推荐使用vagrant创建虚拟机。这有助于我们安装和删除。保持计算机的干净。

wmware是收费的,所以我们这里才使用VirtualBox,因为VirtualBox是免费的。

可以参考https://docs.docker.com/engine/install/centos/文档安装docker。

# 停止docker
systemctl stop docker
# 启动docker
systemctl start docker
# 查看docker版本, 会分别展示client和server的版本。
docker version
# 验证docker是否安装成功
docker run hello-world

出现下面内容则表明安装成功。

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

我们可以在vagrant file文件中在结尾修改一下,让vagrant启动的时候就自动帮我们安装好docker。通过这种方式我们只要拿到vagrant file就可以一键启动安装好应用的虚拟机。非常的方便。

    config.vm.provision "shell", inline: <<-SHELL
        sudo yum remove docker docker-common docker-selinux docker-engine
        sudo yum install -y yum-utils device-mapper-persistent-data lvm2
        sudo yum-config-manager -y --add-repo https://download.docker.com/linux/centos/docker-ce.repo
        sudo yum install -y docker-ce
        sudo systemctl start docker
    SHELL
end

docker machine是可以自动在虚拟机上安装docker工程的工具, mac系统和windows10系统安装docker就会默认安装docker-machine。

docker-machine version

通过docker-machine create demo这样一个命令就可以创建一台安装好了docker并且非常小巧的linux虚拟机。他非常的类似vagrant.

# 列出
docker-machine ls
# 进入到指定的虚拟机
docker-machine ssh demo
# 删除
docker-machine rm demo
# 停止
docker-machine stop demo
# 查看配置demo
docker-machine env demo
# 可以将demo的server导入到本地,这样就省下了ssh。

Docker的镜像和容器

对于docker的架构和底层技术来讲,随着学习和慢慢理解以后,会对docker的架构和技术会有更深入的了解。

docker是一个platform,提供了一个打包,开发,运行app的平台。这个平台将底层的物理设备和上层的app隔离开了,在docker之上来做事情。

docker engine有一个后台进程,他提供了一个rest的api接口,然后docker engine还有一个cli接口,docker是一个cs的架构,cli和后台进程使用过rest来通信的。

通过可以查看docker后台的进程

ps -ef | grep docker
  1. Image

是文件和meta data的集合,image是分层的,并且每一层都可以添加和删除文件,成为一个新的image。不同的image可以共享相同的layer。image本身是只读的。

查看本机的image

docker image ls

可以通过dockerfile文件定义docker的image,使用dockerbuild来打包一个新的image。

docker build -t 

也可以从Registry中获取一个docker, 比如下载ubuntu:14.04这个image。这和github类似,可以使用pull从hub.docker中拉取image。

docker pull ubuntu:14.04

我们也可以将我们自己的docker文件push到dockerhub。

我们这里以一个简单的Hello World程序为例, 制作一个image。

要运行hello world需要有hello world的程序,这里我们使用C语言来编写这个程序。首先我们需要创建一个目录,这里叫hello-word。然后在这个目录里面创建一个文件,比如叫hello.c。

mkdir hello-word
cd hello-word
vim hello.c

然后我们在这个文件里面书写代码,就是使用printf输出hello world字符串。

# include<stdio.h>

int main() {
    printf("hello world\n");
}

有了这个程序以后我们需要将它编译成一个2进制文件,编译C语言程序需要使用gcc程序。可以使用yum install gcc安装gcc,还需要安装glibc-static -> yum install glibc-static。

通过gcc编译hello.c, -o是输出的文件名,这里叫hello

gcc -static hello.c -o hello

这样在hello-word目录下就会多出一个hello的文件,这是一个可执行的文件。直接运行他就会执行。

./hello

接着我们将这个hello可执行文件制作成一个image,首先我们需要创建一个Dockerfile文件。

cd hello-word
vim Dockerfile

在这个文件的第一行需要书写FROM,表示在什么之上,可以在其他的image之上,这里我们是一个base的image所以在scratch之上安装,表示从头开始。

使用ADD,将hello添加到image的跟目录中。

接着运行CMD,指定运行的文件,比如这里指定/hello。

FROM scratch
ADD hello /
CMD ["/hello"]

这是一个非常简单的dockerfile,有了这个dockerfile以后我们就可以去build一个image了。可以通过docker build -t 指定一个tag,比如这里的tag叫yindong/hello-world, 后面的.表示当前的目录找到dockerfile。

docker build -t yindong/hello-world .

执行完毕就构建完了,这里因为有三行,所以会执行step3步。然后通过docker image ls就可以看到我们构建的imageyindong/hello-world了。

可以使用docker history imageId查看docker的分层。这个id就是docker image ls中出现的id。

这里FROM的scratch,所以这个默认不算一层,所以我们打印出来的只有两层。

docker history id

现在我们可以去运行我们的docker了,通过docker run yindong/hello-world

# docker run image名字
docker run yindong/hello-world

这样就可以打印出hello world了。

docker的image其实就是将可执行文件存储起来,运行的时候是共享了宿主机的硬件环境,不过在运行的时候他是独立运行的。

这就是一个简单的image,实际上nginx,mysql都可以做成image,他们的工作原理和上面演示的也都是一样的。

  1. Container

Container是通过image创建的,也就是说必须现有Image,然后通过Image来创建Container,Container是在Image的基础上增加了一层,叫做Container layer,这层是可读写的。因为我们之前讲过Image是只读的,Container因为要去运行程序和安装软件等所以他需要可写的空间。

类比面向对象的概念,类和实例来说Image就相当于是类,Container就相当于实例。

Image负责app的存储和分发,Container负责运行app。

要基于Image创建Container其实也很简单,就是docker run image的名字就可以了。

可以使用docker container ls查看当前本地正在运行的容器。不过我们上面的hello world程序运行之后就会自动退出,所以这里暂时还没有container。

docker container ls

docker container ls -a可以查看所有的容器包括运行的和退出的。

docker container ls -a

可以通过docker run -it的方式来交互式的运行image。也就是可以在这里运行命令,读写文件之类的。实际上进入到了Container里面。并且在执行exit退出容器的时候,之前的操作也会清除。

docker run -it centos
exit
docker --help

docker的命令分为两部分,第一部分是Management Commands第二部分是 Commands。

Management Commands是对docker里面的对象进行管理的,比如说image和container的获取,删除,查看等等。

docker --help
# 查看命令
docker image
docker container
# 查看container列表
docker container ls -a
# 删除container,id可以不用写全
docker container rm id

Commands提供的是一些简便的方法,比如使用docker container ls -a查看container列表,其实可以使用docker ps -a实现相同的功能。

docker ps -a
# 删除一个container,默认docker的rm就是remove container
docker rm id
# docker image ls
docker images
# 删除 image
docker image rm id
docker rmi id

比如我们使用docker run创建了container,通过docker ps -a会查看到很多的过期container,我们可以使用docker rm挨个删除,也可以使用docker container ls -aq打印出所有container id, 这相当于使用搜索打印第一列。

docker container ls -aq
docker container ls -a | awk {'print$1'}

有了这个id我们可以通过docker rm $(docker container ls -aq)删除所有的。

docker rm $(docker container ls -aq)

如果更复杂的情况比如我们删除所有已经退出的container,通过筛选退出的容器。然后使用docker rm删除掉这些container。

# 列出状态为exited的container
docker container ls -f "status=exited" -q
# 删除指定调教的container
docker rm $(docker container ls -f "status=exited" -q)

docker container commit命令

这个命令是创建一个container,然后在这个container中发生了一些变化,比如说安装了某个软件,这样的话我们可以把这个已经改变的container生成一个新的image,这就是docker container commit命令的用途,他可以简写成docker commit

docker container commit
docker commit

我们来演示一下,首先我们需要有一个container,我们可以基于centos的image来实现一个container。这里我们使用docker run -it centos去交互的运行centos。这样我们就有了一个container。

docker run -it centos

然后我们在这个container里面去做一些变化,我们之后centos的container有vi但是没有vim,我们去安装一个vim。

yum install -y vim

安装完成之后我们推出exit,推出之后通过docker container ls -a找到我们刚刚运行的container。

我们可以通过docker commit命令将这个container打包成一个image,这个image基于centos并且里面安装好了vim。

docker commit接收的第一个参数是要commit的container,第二个参数要Image的REPOSITORY和TAG。

# docker commit container列表中的name 新image的名字。
docker commit hardcore_ishizaka yindong/centos-vim

这样就会将hardcore_ishizaka的container编程一个新的Image

docker image ls

这个新出现的Image大小要比之前的centos大一些,我们可以使用docker history id来对比一下yindong/centos-vim和centos,可以发现他们有很多相同的layer。

这是因为yindong/centos-vim是基于centos的,所以会直接复用centos的Layer,在最后一层中大概有150MB的大小,这是因为安装了vim的原因。

这种创建Image的方式并不十分提倡,因为如果把我们这个Image发布出去别人拿到这个Image并不会知道这个Image怎么产生的,这就会有问题,因为这个Image很可能包含不安全的内容。

大部分情况下我们还是建议通过Dockerfile的方式创建Image。

docker image build命令他的简写就是docker build,用途是通过Dockerfile打包一个image。

我们首先创建一个docker-centos-vim的目录,然后进入到这个目录里面。在这个目录里面创建一个Dockerfile。

mkdir docker-centos-vim
cd docker-centos-vim
vim Dockerfile

在这个Dockerfile中首先使用FROM,之前我们写的是scratch表示没有from,但是这个里面我们基于的是centos,所以这里要from centos。

有了centos的Image以后我们需要安装vim。这里我们要运行yum命令来安装,需要使用RUN来执行。

FROM centos
RUN yum install -y vim

然后我们使用docker build命令基于Dockerfile去build一个新的Image。

docker build -t yindong/centos-vim-new .

这里会产生两层,第一层是引用的centos,第二层会创建一个临时的container用来安装vim,然后在将这个临时的container生成Image,完成之后removing掉这个临时的container,这样就创建了一个新的Image。

使用docker image ls可以看到新生成的Image, 虽然他和之前使用container创建的Image基本一致,但是我们还是推荐使用Dockerfile来创建Image,并且别人拿到了我们的Dockerfile之后也可以生成一个一样的Image。

  1. Dockerfile

这里我们来看一下Dockerfile语法梳理和最佳实践。

首先我们来看一下Dockerfile的语法,在Dockerfile里面定义了很多的关键字,通过这些的关键字去构建自定义的Dockerfile。

用于Dockerfile开头的一个语法,用于指定要选择的build的Image的base image是什么,也就是在哪个Image之上去build我们自己的Image。

可以选择scratch表示从头去只做一个Image。

FROM scratch

更多的时候是依赖别人的Image,比如我们之前使用的centos,表示在centos之上去build这个Image, 同理也可以使用其他的比如说ubuntu:14.04。

FROM centos
FROM ubuntu:14.04

对于FROM来讲尽量使用官方的Image作为Base Image,原因也很简单,因为安全。

LABEL的用途是定义Image的Metadata信息,比如说版本,作者,描述等信息。

LABEL maintainer="yindong@126.com"
LABEL version="1.0"
LABEL description="This is description"

LABEL定义的Metadata不可缺少,因为对于Image来讲是必须存在一些帮助信息,他像是代码里面的注释,来帮助别人使用。

RUN是非常常用的,因为很多时候我们需要运行一些命令,一般需要安装一些软件的时候也经常会使用到RUN,对于RUN来讲要注意一点就是,每运行一次RUN对于Image来讲都会新生成一层,所以对于RUN来说他的最佳实践中要求为了避免无用分层,合并多条命令成一行。

比如说yum install 和 yum update推荐通过&&合并成一层。为了美观如果&&导致行变得越来越长可以通过反斜线\换行,增加美观。

# 使用反斜线换行
RUN yum update && yum install -y vim \
    python-dev

# 注意清理cache
RUN apt-get update && apt-get install -y perl \
    pwgen --no-install-recommends && rm -rf \
    /var/lib/apt/lists/*

RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOME'

设定当前工作目录,这个有点像linux里面通过cd去改变目录,在当前的目录下去做一些事情,比如说运行一些程序或者做一些事情,对于workdir来说如果通过workddir访问一个目录,如果没有这个目录,会自动创建这个目录。

WORKDIR /root
WORKDIR /test
WORKDIR deom
RUN pwd # /test/demo

工作中建议使用WORKDIR不建议使用RUN cd, 使用WORKDIR。

# 将hello文件添加到/跟目录
ADD hello /

如果通过WORKDIR改变了目录,ADD添加的目录是WORKDIR改变之后的目录.

WORKDIR /root
ADD hello test/ # /root/test/hello

大部分情况COPY要比ADD优先去使用,添加远程文件或者目录,多数使用curl或者wget去下载。

设定常量,比如我们设置MYSQL_VERSION为5.6,那么在下面的代码中就可以使用MYSQL_VERSION这个变量了。尽量使用ENV增加可维护性。

ENV MYSQL_VERSION 5.6 # 设置
RUN apt-get install -y mysql-server="${MYSQL_VERSION}" \ # 引用
    && rm -rf /var/lib/apt/lists/*

主要用于存储和网络,后面单独来讲。

RUN是执行命令并创建新的Image Layer。

CMD是设置容器启动后默认执行的命令和参数

ENTRYPOINT是设置容器启动时运行的命令

这里我们来对比一下CMD和ENTRYPOINT,弄懂他们的区别,在此之前我们需要了解两种格式。第一种我们称之为shell格式,第二种我们称之为exec格式。

shell格式将要运行的命令当成一个shell命令来执行。

RUN apt-get install -y vim
CMD echo "hello docker"
ENTRYPOINT echo "hello docker"

exec和shell的区别是要使用特定的格式来指明要运行的命令和命令的参数。

RUN ["apt-get", "install", "-y", "vim"]
CMD ["/bin/echo", "hello docker"]
ENTRYPOINT ["/bin/echo", "hello docker"]

接着我们来看下下面这两个Dockerfile, 首先我们定义了依赖的Image,然后定义了常量name,接着我们使用ENTRYPOINT运行了一个echo命令,这里的命令传入了一个参数name。

FROM centos
ENV name Docker
ENTRYPOINT echo "hello $name"
FROM centos
ENV name Docker
ENTRYPOINT ["/bin/echo", "hello $name"]

实际我们打包之后运行发现通过exec的方式并不会将$name替换为常量,这是因为shell格式运行命令的时候,他执行的是一个shell,所以执行的时候可以识别到ENV常量,但是exec的格式执行echo的时候他执行的是echo,并不是shell也就无法取得变量。

可以通过指定exec是通过shell方式去执行的,就是在exec中指定bash,然后通过-c将后面的echo和$name都作为参数.而且只能是一个命令。

FROM centos
ENV name Docker
ENTRYPOINT ["/bin/base", "-c", "echo hello $name"]

CMD是容器启动的时候默认执行的命令,上面的例子中如果我们将ENTRYPOINT改为CMD结果是一样的。如果在docker run的时候制定了其它命令,CMD命令会被忽略掉,如果定义了多个CMD,只有最后一个会执行。

FROM centos
ENV name Docker
CMD echo "hello $name"

# 指明运行的命令
docker run -it [Image] /bin/base

ENTRYPOINT是让容器以应用程序或者服务的形式运行,不会被忽略,一定会执行。最佳实践是写一个shell脚本作为entrypoint。比如说启动一个数据库服务。

比如下面这个mongod的脚本,首先他copy了一个sh脚本到bin中,然后docker-entrypoint.sh最为启动脚本。

COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 27017
CMD ["mongod"]

实际上我们在官方的很多Dockerfile中都会使用ENTRYPOINT。

在github上有一个docker-library的仓库它里面有很多官方提供的Image,Image里面有很多的Dockerfile比如说mysql,对于mysql来讲有很多的版本,如果我们想要build一个官网的mysql指定版本的Image就可以通过这里的Dockerfile。官方的Image是提供了很多的Dockerfile的,这些Dockerfile值得我们去学习和使用。

大家可以沙宣dockerfile的reference文档,里面提供了Dockerfile的详细语法说明。

  1. Image分发

可以通过hub.docker去拉取别人的Image,同样我们自己的Image也可以发布到hub.docker上。

hub.docker是一个类似github的网站,它里面存储了很多Image,并且我们拉取Image的时候是不需要登录的。但是当我们发布Image的时候是必须要登录的。所以我们需要注册hub.docker的账号。

如果我们需要将Image发布到hub.docker,这个tag需要时hub.docker的账号/名字,否则将会没有权限,因为我们只能向自己的hub中push Image。

首先我们需要使用docker login去登录,输入用户名和密码。

docker login

# Login successed

push的方法很简单,就是使用docker push命令,参数就是Image的名字。会根据我们Image的大小不同push的时间也会不同,push成功就会会返回一个id。这时候去hub.docker就可以看到我们的Image了。

docker push yindong/hello-world

发布成功之后任何一个人都可以拉取我们push的这个Image。

docker pull yindong/hello-world

但是我们这样发布的Image是有问题的,他还缺乏文档。所以我们与其分享Image不如分享Dockerfile。

我们可以在create里面点击create automated build, 这个是要把我们的docker账号link到github上,然后在github上创建一个仓库,然后将本地用于build Image的Dockerfile发布的github上,然后hub.docker和github做一个关联,也就是说github有Dockerfile他就会自动去clone获取到这个Dockerfile然后docker.hub的后台服务器会帮我们去build,这样我们既提供了Dockerfile,而且Image又是docker服务器帮我们build的,安全性有所提升。我们只需要去维护Dockerfile就可以了,hub.docker会帮我们去维护打包Image。

如果我们不想将Image发布到hub.docker, 我们想要搭建一个私有的docker registry,docker帮我们提供了一个Image叫做registry,通过这个Image就可以在我们本地或者linux服务器搭建一台自己的类似于docker hub,只不过这个docker hub是没有UI界面的,而且他是只供公司内部或者个人使用的docker hub。

搭建这个其实是非常简单的, 只需要运行下面这个命令就可以了。通过registry Image去创建一个container,这个container跑起来之后就相当一个web服务器。

docker run -d -p 5000:5000 --restart always --name registry registry:2

然后我们就可以使用docker的push 或者 pull 了。

假设我们有一台linux服务器,在这台机器上执行上面的run代码。

安装之后我们可以使用telnet尝试访问上面的linux, 如果telnet不存在可以使用yum安装。

telnet xx.xx.xx.xx 5000

正常是可以访问通的。使用私有的docker hub需要将我们Image的tag修改,之前的tagname部分需要改为私有docker hub服务的ip:端口号。

docker build xx.xx.xx.xx:5000/hello-world .

这样我们就可以通过docker push去提交xx.xx.xx.xx:5000/hello-world了。不过在push之前我们需要在/etc/docker的目录下创建daemon.json文件。这个文件中写入受信任的ip和域名。

{"insecure-registries": ["xx.xx.xx.xx:5000"]}

有了这个以后需要在docker的server文件中添加一行。```vim /lib/systemd/system/docker.service

ExecStart=/usr/bin/dockerd
EnvironmentFile=-/etc/docker/daemon.json # 添加这条
ExecReload=/bin/kill -s HUP $MAINPID

最后我们重启docker service。

service docer restart

这个时候我们就可以去push我们的Image了。也可以将Image pull到本地。

这里演示使用Image打包一个python应用。首先创建一个app.py文件。

from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello();
    return "hello docker"
if __name__ == '__main__':
    app.run()

EXPOSE会把我们运行程序的端口暴露出来,因为我们这个python程序是运行在127.0.0.1:5000中,所以外部无法访问,我们需要使用EXPOSE将端口暴露出来。

FROM python:2.7
LABEL maintainer="Yin Dong<xxxx@126.com>"
RUN pip install flask
COPY app.py /app/
WORKDIR /app
EXPOSE 5000
CMD ["python", "app.py"]

如果失败了会提示失败的信息,并且每个步骤提供一个id,我们可以单独调试这个id, 就进入到这个目录下了。

docker run -it xxxId /bin/base

生成Image之后我们可以run一个container。这样就运行起来了。我们可以加一个-d参数让这个container在后台运行。

docker run -d image

docker ps # 查看信息

我们可以对container进行一系列的操作,比如说停止状态下的删除,我们也可以对运行状态下的container进行操作。

首先是exec,这个命令是进入到运行中的container里面。后面的参数是要执行什么命令,比如说/bin/bash或者python

docker exec it PORTSID /bin/bash

docker exec it PORTSID python

docker exec it PORTSID ip a # 打印运行容器的ip地址

可以使用stop停止这个container, PORTSID是运行中的容器id。

docker stop PORTSID

启动docker的时候可以使用--name命名。

docker run -d --name=demo image
# 使用名字删除
docker stop demo
# 启动
docker start demo

inspect会显示出container的详细信息,里面包含很多,包括network信息,logs等非常重要。

docker inspect PORTSID

通过docker构建一个工具,测试linux系统的工具。

使用ENTRYPOINT + CMD可以实现参数传递。空的CMD就可以将docker run最后的参数传递给ENTRYPOINT命令中使用。

ENTRYPOINT ["/usr/bin/stress"]
CMD []

对容器的资源进行限制,linux本身资源是有限的,如果运行太多的容器会占用很多,容器会最大的占用主机资源,所以我们需要对容器进行限制。

docker run的时候是可以指定cpu的个数,内存空间的大小。我们这里来演示一下。

# 指定内存200M
docker run --memory=200M Image
# 限制cpu权重, 也就是占比
docker run --cpu-shares=10 Image

Docker的网络

这里需要准备两台安装好了docker的nginx机器,一台也是可以的,推荐使用两台。

我们首先运行一个container,这个Image叫做busybox是一个很小的linux的Image。执行一段无限循环的代码。

docker run -d --name test1 busybox /bin/sh -c "while true; do sleep 3600; done"

我们使用docker exec进入到这个container里面,执行/bin/sh的命令。

docker exec -it id /bin/sh

ip a

通过ip a查看本地的网络接口,一般会有两个,一个是本地ip一个是newwork。他有mac地址和ip地址。这就是一个网络的namespace。

我们可以在本地运行 ip a也可以看到很多的net space, container中的网络和本地是隔离的,而且每次创建的docker都是网络隔离的。并且container之间是可以ping通的,也就是网络是想通的。

网络命名空间netns -> netnamespace

# 查看netns列表
ip netns ls
# 删除指定netns
ip netns delete test1
# 创建netns
ip netns add test1
# 查看名称为test1的netnamespace内容。
ip netns exec test1 ip a
# 查看net端口
ip link
# 让端口up起来。单个端口是没办法up起来的,必须要两个才可以。
ip netns exec test1 ip link set dev lo up

我们这里创建两个test1和test2,然后将他们两个连起来。

首先我们在机器1上添加link

ip link add veth-test1 type veth peer name veth-test2
# 查看的时候可以发现会多出一堆veth链接。
ip link

接着我们将veth-test1这个接口添加到test1里面去。

ip lint set veth-test1 netns test1

然后我们将veth-test2添加到test2里面

ip lint set veth-test2 netns test2

这样我们我们将linux1机器中的test1和test2分别添加了veth-test1和veth-test2, 但是他们的状态都是down,并且没有ip地址。

接下来我们分别给他们配置ip地址,我们可以通过下面的命令给他们分配地址。就是我们在test1和test2这两个netspace中执行后的命令也就是分配地址。

ip netns exec test1 ip addr add 192.168.1.1/24 dev veth-test1

ip netns exec test2 ip addr add 192.168.1.2/24 dev veth-test2

我们将这两个端口启动起来,在test1里面执行ip link set dev veth-test1 up

ip netns exec test1 ip link set dev veth-test1 up

ip netns exec test2 ip link set dev veth-test2 up

现在去查看就可以发现test1和test2已经up起来了,并且有ip地址。并且test1和test2是通了的。可以ping一下test2的ip地址。

ip netns exec test1 ip link
# ping一下test2
ip netns exec test1 ping 192.168.1.2

可以使用docker network ls查看当前机器中container的网络。

本地创建一个nginx的服务器container。

docker run --name web -d nginx

docker ps

这里创建的nginx的container,所以并不能访问到这个服务器,nginx的这个container他的网络空间是一个独立的netspace,有自己的ip地址,nginx默认启动的是80端口。

我们可以看下nginx这个container的ip地址,可以通过下面这几种命令。

# 默认是链接再bridge上面的
docker network inspect bridge
# 我们在外面是可以ping通里面的ip的,因为外面是有docker0bridge的,默认container是连在bridge上的。
ping xx.xx.xx.xx
# 使用telnet访问这个ip加端口,nginx默认是80端口, 是可以访问的。
telnet xx.xx.xx.xx 80
# 通过curl这个网站, 这样就会把nginx的界面拉下来。
curl http://xx.xx.xx.xx

我们不只希望container只在运行的服务上可以访问,我们希望其他机器也可以访问。我们可以端口映射出去。也就是把container的端口映射到服务器的端口上。这样我们在访问127.0.0.1也可以访问到这个docker container。

我们创建container的时候多加一个参数-p, 也就是端口映射,参数格式是容器里:本地, 比如容器里的80映射到这里的80

docker run --name web -d 80:80 -p nginx
# 可以看到ports里面80映射到了80
docker ps

这个时候我们访问本地的80端口就可以访问到里面的服务了。

curl 127.0.0.1

首先我们来看一下none network。首先创建container的时候我们使用--network为none, 创建一个test1的busybox容器。

docker run -d --name test1 --network none busybox /bin/sh -c "while true; do sleep 3600; done"
# 查看一下
docker ps

通过docker inspect none查看一下, 可以看到这个容器的ip地址,mac地址都没有,没有任何网络信息。

docker network inspect none

我们进入到这个容器里面去。可以发现他除了本地的lo以外没有网络接口。

docker exec -it test /bin/sh
ip a

因为他没有网路接口所以他是一个孤立的容器,没有任何方式可以访问到这个容器,除了exec,这样的容器安全性比较高。具体怎么使用我也不知道。

创建一个连接到host的容器,首先删除其他的容器,这里指定network为host。

docker run -d --name test1 --network host busybox /bin/sh -c "while true; do sleep 3600; done"
# 查看一下
docker network inspect host

可以发现这个容器也没有mac地址和ip地址,我们进入到这个docker里面的时候使用ip a可以发现是有网络接口的,而且他的网络接口和主机的接口是相同的。

也就是说通过host创建的容器他是没有独立的net space的,他共享了主机的net space, 这样可能会存在端口冲突。

可以将应用拆分部署,比如说redis和mysql拆分,主应用部署容器,redis部署容器,mysql也部署容器。因为redis和mysql不是给外部使用的,所以不需要使用-p做端口映射,这样更安全,可以通过容器间网络通信进行访问。

因为redis是在容器里面的,外部不知道这个容器的ip地址,并且也不需要知道他的ip地址,可以通过link的方式通过容器的名字访问容器。也就是说redis的名字和端口是固定的,我们只需要告诉新容器这个名字和端口就可以了。

启动第二的时候使用link将前一个的名字传入进去。可以使用-e给容器传入一个环境变量。

docker run -d -p 5000:5000 --link redis --name flask-redis -e REDIS_HOST=redis ImageName

比如我们创建busybox的时候创建一个环境变量YIN,值为yindong

docker run -d --name test1 -e YIN=yindong busybox /bin/sh -c "while true; do sleep 3600; done"

进入这个容器访问一下这个变量。

docker exec it test1 /bin/sh
env # 可以看到我们设置的YIN=yindong

# os.environ.get('YIN', '127.0.0.1')  py获取变量

首先两台机器AB之间是可以通信的,现在我们A机器上有个01的容器,B机器上有个02的容器,我们01和02是不可以通信的,但是我们知道A和B是可以通信的,所以我们可以将01的数据放在A传输给B的数据中,传递给B,B再传递给02,同样B给A的数据也可以存在02的数据,再通过A给到01,这就实现了通信。这种方式一般称为隧道。VXLAN的方式。

我们来演示一下多机通信。首先我们有AB两台机器,每个机器里面有一个容器01和02。docker除了bridge还存在oerlay,我们可以通过创建oerlay的方式进行通信。这里需要依赖一个分布式的存储,因为我们要确定两台机器中的容器ip不能相同,这里就需要一个分布式的存储,来告诉不同的服务相同的东西,这种分布式的工具还是很多的,我们这里选用etcd,开源免费。

我们需要分别在两台机器上安装etcd,安装成功之后将两台机器关联起来,然后开始执行。

在A机器上创建overlay, 执行之后局可以在本地发现一个demo的overlay

docker network create -d overlay demo

这个时候在B机器也可以看到A机器创建的demo,这就是etcd做的。

接着我们要在这网络之上创建一个container,链接到这个网络之上。通过busybox,然后给他一个名字叫做test1,通过--net指定网络是demo。

sudo docker run -d --name test1 --net demo busybox sh -c "while true; do sleep 3600; done"
# 查看容器
docker ps

在B上也创建一个容器

sudo docker run -d --name test2 --net demo busybox sh -c "while true; do sleep 3600; done"
# 查看容器
docker ps

可以查看两个container的ip。这样两个container是可以互相ping通的。

docker netword inspect demo

Docker的持久化存储和数据共享

  1. Data Volume

一般来讲有些容器会产生一些自己的数据,这些数据不想随着container的消失而消失,我们需要保证数据的安全,这种场景一般用在数据库,比如我们使用数据库的容器,数据库会产生一些数据,如果container删除了数据库丢失了,那就有问题了,针对这种问题我们使用Data Volume。

之前在讲dockerfile的时候里面有个关键字是volume,这个关键字就是制定容器某一个目录产生的数据要挂载到主机上的某个目录中,并且我们会创建一个叫做docker volume的对象。

官方docker hub的mysql的dockerfile中就存在volume关键字。当我们启动mysql容器的时候就会在linux的/var/lib/mysql存放,不会随着container消失而消失。

VOLUME /var/lib/mysql

我们创建一个mysql的container, 这里传入参数是不使用密码

docker run -d --name mysql1 -e MYSQL_ALLOW_EMPTY_PASSWORD=true mysql

docker ps

可以查看指定容器启动日志,方便查看报错信息。

docker logs mysql1

查看volume, 这个volume就是我们创建mysql的时候产生的,只要Dockerfile中书写了就会创建。

docker volume ls
# 删除
docker valume rm VOLUME_NAME

我们可以使用inspect来看一下这个volume的详细细节。可以看到这个volume是mount到本地的/var/lib/docker/volumes/…/_data中

docker volume inspect VOLUME_NAME

我们每创建一个container就会新增一个目录。并且我们删除这个container并不会删除掉这个volume目录。这也就实现了数据会丢的问题。

我们可以给volume取一个别名方便查看。就是在创建的时候添加一个-v参数。将名称改为mysql,前面是要改为的名字,后面是VOLUME的路径。

docker run -d -v mysql:/var/lib/mysql --name mysql1 -e MYSQL_ALLOW_EMPTY_PASSWORD=true mysql

这个时候当我们删除了container,再次创建container的时候,如果-v的名字相同,他们会继续使用之前的数据。

我们要在Dockerfile里面指定需要持久化数据的路径,这个路径是容器里面的路径,然后我们在创建容器的时候使用-v将这个路径重新命名

  1. Bind Mouting

他和data volume的区别是data volume需要在Dockerfile中定义,Bind Mouting不需要,他只需要在运行容器的时候去指定本地的目录和容器的目录一一对应的关系,通过这种方式可以做一个同步,也就是说本地路径中的文件和运行容器中的路径文件是同步的。

如果本地本地文件做了修改,那么容器中的文件也会做修改,因为他们就是同一个目录的同一个文件。我们这里演示一下。创建容器的时候使用-v将我们本地的目录映射到container里面的目录。比如我们这里使用pwd将当前目录映射到/usr/share/nginx/html

docker run -d -p 80:80 -v $(pwd):/usr/share/nginx/html --name web nginx

这样两个目录就共享了,无论修改哪一个都会同步到另一个。因为他们就是一个目录。

Docker Compose多容器部署

假设我们创建的应用需要依赖多个container,每次部署的时候都要全部手动启动一遍,停止的时候也需要每台手动停止,这样太麻烦了,Docker Compose就是一个批处理的工具,可以统一启动容器和停止容器。

可以通过这个文件在这个文件中定义app所需要的所有container,定义之后我们可以通过一条命令搞定所有容器的启动,停止操作。

Docker Compose是一个基于Docker的命令行工具,这个工具可以通过一个yml格式的文件去定义多个容器的docker的应用。有了yml文件之后通过一条命令根据文件中的定义去创建和管理容器。

所以这里最重要的就是这个yml文件,我们来看一下这个yml文件如何写。首先这个文件有一个默认的名字叫做docker-compose.yml, 我们可以改成自己需要的。

在这个文件中有三个重要的概念,Services,Networks,Volumes。

Docker Componse是有版本的,所以yml文件也存在版本,目前有3个版本,我们推荐使用version3,不同的版本对应Docker不同的版本。version2只能用于单机,version3可以用于多机,version1已经淘汰了。

  1. Services

一个service代表一个container,这个container可以从dockerhub的image来创建,或者从本地的Dockerfilebuild出来的image来创建。

service的启动类似docker run,我们可以给其指定network和volume,所以可以给service指定network和Volume的引用。

比如我们下面这个services,他的名字叫做db,然后这个db的image是从dockerhub上拉取的postgres:9.4, 接着volumes映射了一个目录到本地,相当于-v db-data:/var/lib/postgresql/data, 定义一个叫做back-tier的network

services:
    db: 
        image: postgres:9.4
        volumes:
            - "db-data:/var/lib/postgresql/data"
        networks:
            - back-tier

这相当于使用下面的命令启动容器

docker run -d --network back-tier -v db-data:/var/lib/postgresql/data postgres:9.4

还有第二种书写方式,这里的Image不是从dockerhub上拉取的, 他是在本地build的,这个build目录就是我们要build的Dockerfile。这里他links了db和redis的容器。一般情况下如果我们连接到同一个bridge中的时候是不需要links的。只有适应默认的th0的时候才需要links。

services:
    worker:
        build: ./worker
        links:
            - db
            - redis
        networks:
            - back-tier
  1. Volumes

我们可以在services同级别定义volumes。

services:
    db: 
        image: postgres:9.4
        volumes:
            - "db-data:/var/lib/postgresql/data"
        networks:
            - back-tier
volumes:
    db-data:
  1. networks

我们可以在services同级别定义networks, 他会创建一个dirver是bridge的名字叫做back-tier的network

services:
    db: 
        image: postgres:9.4
        volumes:
            - "db-data:/var/lib/postgresql/data"
        networks:
            - back-tier
networks:
    front-tier:
        driver: birdge
    back-tier:
        driver: bridge

我们这里基于yml文件定义一个wordpress, 这里第一行是声明版本,我们这里声明3,首先services里面我们定义了两个service,第一个是wordpress,我们端口做了一下映射将容器中的80端口映射到本地的8080端口。可以通过enviroment传递环境变量。networks指定了容器要连接的网络是networks中定义的网络。

mysql中的volumes映射成mysql-data, 同样这里的名字是要在volumes创建的。

version: '3'
services:
    wordpress:
        image: wordpress
        ports:
            - 8080:80
        enviroment:
            WORDPRESS_DB_HOST: mysql
            WORDPRESS_DB_PASSWORD: root
        networks:
            - my-bridge
    mysql:
        image: mysql
        environment:
            MYSQL_ROOT_PASSWORD: root
            MYSQL_DATABASE: wordpress
        volumes:
            - mysql-data:/var/lib/mysql
        networks:
            - my-bridge

volumes:
    mysql-data:

networks:
    my-bridge:
        driver: bridge

这就是一个比较完整的docker compose的file,总体上来说还是比较清晰的。

  1. docker-compose

如果要使用docker compose首先我们需要安装它,他是一个工具,如果我们使用的是mac或者windows在安装docker的时候他默认是已经安装好了的。

docker-compose --version

linux需要手动安装(https://docs.docker.com/compose/install/)

首先下载这个文件

sudo curl -L "https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

下载之后我们要给他赋予一个可执行的权限。

sudo chmod +x /usr/local/bin/docker-compose

最后再设置一下软连接, 就安装完毕了

sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

docker-compose --version

我们基于yml文件来试验一下。docker-compose up的功能是启动yml中的container。默认yml文件的名字是docker-compose.yml。也可以使用-f指定文件。-d会后台执行,如果不加会打印出日志在控制台debug的时候可以查看, 关闭就会停止也就是调试模式。

docker-compose -f docker-compose.yml up -d

这样就会启动起来两个container。

可以使用stop命令停止container,down命令不但会停止,同时也会移除container, network, volume

# 启动
docker-compose start
# 停止
docker-compose stop

docker-compose ps
# 查看定义的container和依赖的image
docker-compose images
# 进入容器,既然怒mysql的base,这里的mysql是services
docker-compose exec mysql base

docker-compose会在我们创建的容器和network上添加一些前缀,我们使用的时候直接用定义的名字就可以,他命令的内部会自动转换。

version: '3'
services:
    redis:
        image: redis
    web:
        build:
            context: .
            dockerfile: Dockerfile
        ports:
            - 8080:5000
        environment:
            REDIS_HOST: redis
  1. scale

这是docker-compose新增的一个功能,可以实现docker容器的扩展,也就是相同的容器启动多少个,用来做负载均衡。比如说我们让web这个容器启动3个。

docker-compose up --scale web=3

但是这里有个问题,我们启动3个服务他们的端口映射到了一个端口,是有问题的,所以我们的做法是不写端口映射。然他们直接启用自己的80端口。然后通过haproxy工具将做负载。因为知道每个容器的端口,所以也就很容易。使用haproxy帮我们负载到所有的80端口服务,然后映射到8080。

version: '3'

services:
    redis:
        image: redis

    web:
        build:
            context: .
            dockerfile: Dockerfile
        environment:
            REDIS_HOST: redis
    lib:
        image: dockercloud/haproxy
        links:
            - web
        prots:
            - 8080:80
        volumes:
            - /var/run/docker.sock:/var/run/docker.sock

我们先创建一个

docker-compose up

docker-compose ps

启动3个web,会加上之前的1个。所以一共还是3个。

docker-compose up --scale web=3 -d

这样就实现了一个负载均衡。

docker-compose build这个命令是如果配置文件中有些Image是需要build的可以先通过这个命令把这个Image生成出来,这样在up的时候会快一些。如果service里面发生了变化也可以使用这个命令打包新的service再去启动。

docker-compose build

注意的是docker-compose是一个用于本地开发的工具,并不是用于部署生产环境的工具,他只是方便在本地开发看到结果。

容器编排Docker Swarm

我们所有docker的操作都是在本地操作的,也就是在一台机器上执行的。但是实际情况是我们的应用可能部署在很多台机器上,也就是在一个集群部署应用,这就涉及很多的容器。

那这种情况怎么去管理这么多的容器,怎么增加一些容器,如果一个容器down掉了怎么自动恢复,怎么样动态更新容器而不影响业务, 如何去监控追踪容器状态,如何去调度容器的创建,怎么去保护隐私数据,等等这些问题都需要我们去处理。

基于这些我们需要有一套容器的编排系统,在这种情况下Swarm就诞生了,Swarm不是唯一做编排的工具,他是docker的工具。集成在docker里面的,如果我们想使用它是不需要安装任何东西的。

Swarm中有两种角色,Manager和Worker,Manager是大脑而且不止一个,既然不止一个就要进行数据同步,所以内置了一个分布式。Worker就是干活的节点,大部分服务都是部署在Worker中。

在Swarm中我们不使用run,使用service,下面我们来创建一个service, 采用的Image是busybox。

docker service create --name demo busybox sh -c "while true..."

创建之后查看创建的service。使用ps可以看到容器是运行在swarm上面。

docker service ls
# 查看容器
docker service ps demo
# 创建5个,这五个平均分布到clust里面的。他可以确保5是有效的,当有一个down掉会重新起一个替代。
docker service scale demo=5

Internal:Container和Container之间的访问通过overlay网络通过VIP虚拟ip进行通信。

Ingress: 如果服务有绑定端口,则此服务可以通过任意swarm节点的相应接口访问。

LVS实现负载均衡,可以自行查看。