Docker的诞生
我们总是会遇到测试对开发说项目又不work了,开发总说:在我电脑上是ok的阿。
项目组加了新人,我们就需要教新人配置各种开发环境,每换一台机器就要配置一次,每来一个新人就要配置一次。
于是我们想,有什么办法可以在安装软件的时候把环境也安装过来?一摸一样复制过来就没这么多问题了。
于是,我们开始用虚拟机,它自己一套系统,然后你在里面配置好环境,复制给队友就好了。根本上虚拟机也是一个文件。
但是有个缺点就是太大了!启动太慢!一些系统的操作完全是多余的。
于是就开始用linux容器。Linux 容器不是模拟一个完整的操作系统,而是对进程进行隔离。容器里面的应用,直接就是底层系统的一个进程,操作系统使用宿主的操作系统。
Docker 就是 Linux 容器的一种封装。
Docker 做什么
- web应用的自动化打包测试
- 微服务
- 提供开发环境
安装docker应用
mac 安装地址https://store.docker.com/editions/community/docker-ce-desktop-mac
通过dmg安装,打开这个应用就可以了。
通过docker -v
来测试有没有安装成功。
第一个docker应用
docker的强大之处,一句开启一个nginx服务。
docker run -d -p 80:80 --name webserver nginx
然后打开 http://localhost 看一下,如果没有问题应该就可以看到nginx的欢迎页了。
docker image
docker的核心概念之一,image,image其实就是镜像。一个容器是由1个或多个image组成的,比如我们nginx的应用,就是下载了docker提供的nginx镜像。
一个web应用可以有多个image,比如nginx,python,db等等,我们可以自由组合image从而生成我们自己的iamge!发布我们自己的image,大家就可以直接下载我们的镜像,从而直接在同一个环境下开发和获取服务啦。
我们可以看一下我们当地的镜像。
docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest b175e7467d66 3 weeks ago 109MB
可以看到每个镜像的一些信息,也可以通过docker image rm nginx
来删除这个镜像。
docker container
docker的核心概念之二,container,container是docker的容器,我们所有运行的程序都需要container来承载。
运行一个程序需要做什么?首先需要从docker hub上面找到我们需要的image文件(也可以根据自己的需求自己构造image文件),然后通过docker run命令生成一个容器来供程序运行,然后我们的程序就可以运行在docker上了。
我们第一个应用的docker container run
命令会从 image 文件,生成一个正在运行的容器实例,我们就可以通过ip来访问这个服务。
我们第一个命令的各个参数含义如下:
-d container做为守护进程在后台运行
-p 本机80端口映射container80短裤
--name 容器的名字叫做webserver。
为什么我们只有run命令而没有拉取nginx的image文件呢?
docker会首先找本地nginx image,如果没有就自动从docker hub上面下载。
当然也可以自己首先下载到本地:docker image pull library/nginx
其中library是 image 文件所在的组,由于 Docker 官方提供的 image 文件,都放在library组里面,所以它的是默认组,可以省略。
image 文件生成的容器实例,本身也是一个文件,称为容器文件。
也就是说,一旦容器生成,就会同时存在两个文件: image 文件和容器文件。
而且关闭容器并不会删除容器文件,只是容器停止运行而已。
# 列出本机正在运行的容器
$ docker container ls
# 列出本机所有容器,包括终止运行的容器
$ docker container ls -a
我们可以看到我们正在运行的nginx服务。
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0b559b5addf8 nginx "nginx -g 'daemon of…" 13 days ago Up 2 seconds 0.0.0.0:80->80/tcp webserver
当然我们也可以通过start,stop,restart来控制服务的情况。
比如
docker container stop webserver
我们通过ps命令来看一下现在的容器情况:
docker ps -a
会看到,我们的webserver的status:Exited (0) About a minute ago。我们再运行docker container start webserver
就可以重启服务。
可以简写为
docker start webserver
和docker stop webserver
,docker rm webserver
。rm命令顾名思义就是删除这个container了。
容器通信
关于容器和image的基础操作其实就几个命令。还有容器的通信也是很重要的,因为我们需要知道容器的情况,debug什么的,就需要对容器进行操作。
这只讲一个命令就是docker exec
。这个命令就允许你进入容器。
比如docker exec -it webserver bash
。
-it参数:容器的 Shell 映射到当前的 Shell,然后你在本机窗口输入的命令,就会传入容器。
bash:容器启动以后,内部第一个执行的命令。这里是启动 Bash,保证用户可以使用 Shell。
这样我们就进入到了webserver的容器内部,我们可以通过修改nginx.conf来操作nginx服务,或者查看nginx的log。
docker log
docker的log有个命令docker logs webserver
,我们可以看到docker的这个服务的访问情况。
有时候会遇到
docker start xxx
然后看一下docker ps -a
,这个xxx还是没有启动。这时候可以看一下docker logs xxx
,他很可能报错了。然后你的容器就启动不了了。
生成自己的image文件
之前提到,你进行了一些操作,容器启动不了,然后你进入这个bash,他会告诉你需要先启动容器才能进入bash。那怎么办呢,此时就需要一些比较麻烦的办法了,比如接下来要说的生成自己的image文件。
docker run -d -p 80:80 --name webserver nginx
,后面指定了nginx做为image,它启动就会首先执行nginx xxx来开启这个nginx服务。但是因为一些原因开启不了。那我们只能以bash的方式‘开启’这个容器。然后修改这个container的内容。再以nginx的方式重启。
我们怎么以bash的方式启动我们这个nginx container呢?我们需要把当前的container生成为一个我们自己的image文件。
我们通过run命令把image文件生成一个container文件,我们也可以通过docker commit
来生成我们的image文件:
docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]
-a :提交的镜像作者;
-c :使用Dockerfile指令来创建镜像;
-m :提交时的说明文字;
-p :在commit时,将容器暂停。
docker commit -a 0b559b5addf8 temp
,我们生成一个临时的temp image。可以通过docker image ls
查看我们生成的image。docker rm webserver
我们删掉没用的container。docker run -it --name webserver -p 80:80 -d temp
。我们根据我们生成的temp来生成一个container容器。
ok,现在我们可以docker exec -it webserver bash
来修改我们的配置了。
修改完之后还需要再次生成一个iamge,因为我们需要通过nginx的方式启动这个iamge。docker commit -a 0c5a9b5asddf8 temp2
。我们再次根据改完之后的container生成了一个temp2。docker rm webserver
我们删掉没用的container。docker run -it --volumes-from temp2 --name webserver -p 80:80 -d nginx
。
这样就完成了。--volumes-from 参数是可以允许我们从另一个容器当中挂载容器中已经创建好的数据卷。
- 0b559b5addf8是我们webserver container的id
- 具体的命令可以看https://jiajially.gitbooks.io/dockerguide/content/chapter_fastlearn/docker_run/--volumes-from.html
构建自己的image
跟之前不同的构建自己的image,我们可以自由组合image来达到我们想要的效果。比如,第一个例子,我们通过docker exec -it webserver bash
进入容器之后,发现这个bash什么都没有!
vi都没有!我们就自己构建一个带vim的image。
很简单就是下载一个vim就可以了。apt-get install vim
,当然你也可以在当前container自己安装vim。但是比较懒,就自己生成一个image吧。
docker提供了docker build
命令。
我们需要写一份配置文件Dockerfile:
FROM openresty/openresty:trusty
RUN apt-get update && apt-get install -y vim
一共就两行。涉及到了两条指令,FROM 和 RUN
FROM 指定基础镜像,RUN 执行命令。也可以FROM scratch不指定基础镜像,就是说你没有依赖。
上面的就是依赖openresty镜像,然后进入这个镜像安装vim。
再比如一份dockerfile
FROM debian:jessie
RUN apt-get update
RUN apt-get install -y gcc libc6-dev make
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install
Dockerfile 中每一个指令都会建立一层,RUN 也是。每一个 RUN 的行为,新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。上面的这种写法,创建了 7 层镜像。
正确写法应该是这样:
FROM debian:jessie
RUN buildDeps='gcc libc6-dev make' \
&& apt-get update \
&& apt-get install -y $buildDeps \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz" \
&& mkdir -p /usr/src/redis \
&& tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
&& make -C /usr/src/redis \
&& make -C /usr/src/redis install \
&& rm -rf /var/lib/apt/lists/* \
&& rm redis.tar.gz \
&& rm -r /usr/src/redis \
&& apt-get purge -y --auto-remove $buildDeps
最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt 缓存文件。
这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。
apt-get install -y vim
不要省略-y,安装过程中会有一些bash的交互,如果没有-y,安装就会不成功。-y默认全选yes
构建
接下来是我们的构建命令:docker build openresty .
我们构建了一个叫openresty的image。后面这个点值的是当前路径。还可以通过-f指定dockerfile的路径。
需要注意的是,docker并不是真的通过这个.来指定dockerfile的路径的。这其实是在指定上下文路径。
首先我们要理解 docker build 的工作原理。Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如 docker 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。
虽然表面上我们好像是在本机执行各种 docker 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。
docker build 命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的。
如何才能让服务端获得本地文件呢?
这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
COPY 这类指令中的源文件的路径都是相对路径。
这就是为什么 COPY ../package.json /app 或者 COPY /opt/xxxx /app 无法工作的原因。
构建好openresty之后,我们就可以通过docker run -d -p 80:80 --name test openresty
来开启一个openresty(nginx+lua)的服务了。
docker-compose
使用一个 Dockerfile 模板文件,可以让用户很方便的定义一个单独的应用容器。
然而,在日常工作中,经常会碰到需要多个容器相互配合来完成某项任务的情况。
例如要实现一个 Web 项目,除了 Web 服务容器本身,往往还需要再加上后端的数据库服务容器,甚至还包括负载均衡容器等。
Compose 恰好满足了这样的需求。它允许用户通过一个单独的 docker-compose.yml 模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)。
Compose 中有两个重要的概念:
- 服务 (service):一个应用的容器,实际上可以包括若干运行相同镜像的容器实例。
- 项目 (project):由一组关联的应用容器组成的一个完整业务单元,在 docker-compose.yml 文件中定义。
Compose 的默认管理对象是项目,通过子命令对项目中的一组容器进行便捷地生命周期管理。
mac安装的docker里面应该自带docker-compose。
下面我们用 Python 来建立一个能够记录页面访问次数的 web 网站。
新建文件夹,在该目录中编写 app.py 文件
from flask import Flask
from redis import Redis
app = Flask(__name__)
redis = Redis(host='redis', port=6379)
@app.route('/')
def hello():
count = redis.incr('hits')
return 'Hello World! 该页面已被访问 {} 次。\n'.format(count)
if __name__ == "__main__":
app.run(host="0.0.0.0", debug=True)
from flask import Flask
from redis import Redis
app = Flask(__name__)
redis = Redis(host='redis', port=6379)
@app.route('/')
def hello():
count = redis.incr('hits')
return 'Hello World! 该页面已被访问 {} 次。\n'.format(count)
if __name__ == "__main__":
app.run(host="0.0.0.0", debug=True)
编写 Dockerfile 文件,内容为
FROM python:3.6-alpine
ADD . /code
WORKDIR /code
RUN pip install redis flask
CMD ["python", "app.py"]
编写 docker-compose.yml 文件,这个是 Compose 使用的主模板文件。
version: '3'
services:
web:
build: .
ports:
- "5000:5000"
redis:
image: "redis:alpine"
运行docker-compose up
。我们的服务就起来了。
docker-compose是针对某个项目的,只能在docker-compose.yml的工作目录下运行。
docker-compose up -d # 在后台运行
docker-compose ps
#输出如下
Name Command State Ports
--------------------------------------------------------------------------------
docker_redis_1 docker-entrypoint.sh redis ... Up 6379/tcp
docker_web_1 python app.py Up 0.0.0.0:5000->5000/tcp
我们可以通过docker-compose stop
停止这两个服务。通过docker-compose start
开启这两个服务。
也可以知道单个服务: docker-compose start web
。
我们可以通过docker-compose来管理我们的docker服务
docker-machine
我们有多台机器,如果都要安装docker,不能分别ssh到机器上,然后install docker,太麻烦了,就需要有个安装工具。
docker-machine可以在一台机器上通过命令控制几台机器安装docker环境。但是不能指定docker版本。
我们需要驱动来选择我们控制的主机。
Docker Machine 支持多种后端驱动,包括虚拟机、本地主机和云平台等。
这里使用xhyve。
xhyve 是 macOS 上轻量化的虚拟引擎,使用其创建的 Docker Machine 较 VirtualBox 驱动创建的运行效率要高。
首先安装一下xhyve
brew install docker-machine-driver-xhyve
使用create命令创建主机实例。
docker-machine create \
-d xhyve \
# --xhyve-boot2docker-url ~/.docker/machine/cache/boot2docker.iso \
--engine-opt dns=114.114.114.114 \
--engine-registry-mirror https://registry.docker-cn.com \
--xhyve-memory-size 2048 \
--xhyve-rawdisk \
--xhyve-cpu-count 2 \
test
最简单的create命令docker-machine create -d xhyve --xhyve-boot2docker-url ~/.docker/machine/cache/boot2docker.iso test
。
boot2docker是一个最小的linux系统,加--xhyve-boot2docker-url ~/.docker/machine/cache/boot2docker.iso参数避免每次都从外网下载boot2docker.iso。
如果首次下载比较慢,可以手动下载boot2docker.iso放到~/.docker/machine/cache/boot2docker.iso,然后指定如上参数就可以
如果创建成功,通过docker-machine ls
可以看到,我们已经有一个主机处于runing状态了。
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
test - xhyve Running tcp://193.111.32.3:2376 v17.06.0-ce
我们可以通过docker-machine ip test
指定test主机来查看他的ip,docker-machine env test
查看环境变量。
export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://193.111.32.3:2376"
export DOCKER_CERT_PATH="/Users/chenby/.docker/machine/machines/test"
export DOCKER_MACHINE_NAME="test"
# Run this command to configure your shell:
# eval $(docker-machine env test)
通过docker-machine start test
,docker-machine stop test
来开启和关闭这个主机。
主机通信
在执行docker-machine env test
命令的时候可以看到后
# Run this command to configure your shell:
# eval $(docker-machine env test)
你可以执行eval $(docker-machine env test)
。然后docker ps -a
。你就会发现,打印出来的是空。
因为此时你通过设置env变量,已经连接了test主机的docker。test主机只有一个docker其他都没有。
除了这种方式,docker-machine还提供了ssh命令进入主机。docker-machine ssh test
。发现进入了一个新的交互界面,这就是test主机的环境。
我们在test环境中执行docker run -d -p 80:80 --name webserver nginx
来创建一个nginx server。
docker-machine ip test
,使用这个ip访问80端口。可以发现已经找到nginx欢迎页面。
docker-swarm docker集群
docker 1.12 之后,docker swarm成为docker的一个子命令。它可以把多个docker主机组成的系统转换为单一的虚拟docker主机,使得容器可以组成跨主机的子网网络。
docker swarm就是docker的集群工具。
首先,我们创建3个xhyve虚拟主机。
docker-machine create -d xhyve --xhyve-boot2docker-url ~/.docker/machine/cache/boot2docker.iso test
docker-machine create -d xhyve --xhyve-boot2docker-url ~/.docker/machine/cache/boot2docker.iso worker1
docker-machine create -d xhyve --xhyve-boot2docker-url ~/.docker/machine/cache/boot2docker.iso worker2
可以看一下现在有3个虚拟主机。
docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
test - xhyve Running tcp://193.111.32.5:2376 v17.06.0-ce
worker1 - xhyve Running tcp://193.111.32.6:2376 v17.06.0-ce
worker2 - xhyve Running tcp://193.111.32.7:2376 v17.06.0-ce
首先,我们进入test,把它当作manager主机,只有manager主机才能控制其他主机。docker-machine ssh test
。
然后执行swarm init 操作,创建一个集群。docker swarm init --advertise-addr 193.111.32.5
init完之后会打印出
Swarm initialized: current node (kju71ai4gwjd7omujk3wxu52e) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-3p6t8whnzh4q1cv743qe0sov5jhylm3559ktbbvx0zkprjv0je-6zeju5ownlhll8j7m3g99e3ia 193.111.32.5:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
说明test主机已经成为了manager,成功创建了一个集群。
docker node ls #查看一下当前的集群,只有一个test。
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
kju71ai4gwjd7omujk3wxu52e * test Ready Active Leader
我们再进入worker1主机,ssh worker1。
复制一下刚刚init之后打印出来的命令,
docker swarm join --token SWMTKN-1-3p6t8whnzh4q1cv743qe0sov5jhylm3559ktbbvx0zkprjv0je-6zeju5ownlhll8j7m3g99e3ia 193.111.32.5:2377
# This node joined a swarm as a worker.
然后进入worker2,同样的操作。
最后回到ssh test,因为只能在test里面操作子主机。
docker node ls
##
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
7z7ceaabs2t195vhdgr8vyw25 worker1 Ready Active
kju71ai4gwjd7omujk3wxu52e * test Ready Active Leader
tqq0zaw3viz12xs4dd4azflo9 worker2 Ready Active
可以看到我们的test节点是一个Leader的状态。docker info
来查看现在的集群信息。
现在我们的集群创建完了,怎么在集群上跑我们的服务呢?
这里还是以nginx为例。
我们在manager主机上运行:
docker service create -p 80:80 --name webserver nginx
我们看一下服务情况。
docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
ilero5rmrm2b webserver replicated 1/1 nginx:latest *:80->80/tcp
REPLICAS 表示有多少服务的实例在跑。
docker service ps webserver
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
nrxl4obz83e2 webserver.1 nginx:latest test Running Running 7 seconds ago
可以看到现在一个实例跑在test上。
我们也可以
docker service scale webserver=5
拓展到5个worker,
执行
docker service ps webserver
可以看到,我们有5个实例,分别跑在不同的节点上。
我们访问193.111.32.5,193.111.32.6,193.111.32.7,都能看到nginx的服务,但是其实不一定打到的是当前的节点。
这就是swarm做的调度。
比较有意思的是,当你kill掉一个docker 实例。swarm会自动帮你生成一个新的实例来保证5个server。
比如:
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
xqr3dfk34k4w webserver.1 nginx:latest worker1 Running Running 3 minutes ago
k471q80g91wj \_ webserver.1 nginx:latest test Shutdown Shutdown 3 minutes ago
test上面的webserver.1服务挂掉了,自动在worker1上面起了一个webserver.1 的服务。当然在哪起服务要看swarm的算法了。
这里有篇比较好的swarm api的文章:https://www.digitalocean.com/community/tutorials/how-to-create-a-cluster-of-docker-containers-with-docker-swarm-and-digitalocean-on-ubuntu-16-04