Docker
Docker 最初是 dotCloud 公司创始人 Solomon Hykes 在法国期间发起的一个公司内部项目,它是基于 dotCloud 公司多年云服务技术的一次革新,并于 2013 年 3 月以 Apache 2.0 授权协议开源,主要项目代码在 GitHub 上进行维护。Docker 项目后来还加入了 Linux 基金会,并成立推动 开放容器联盟(OCI)。
Docker 使用 Google 公司推出的 Go 语言 进行开发实现,基于 Linux 内核的 cgroup,namespace,以及 AUFS 类的 Union FS 等技术,对进程进行封装隔离,属于 操作系统层面的虚拟化技术。由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。最初实现是基于 LXC,从 0.7 版本以后开始去除 LXC,转而使用自行开发的 libcontainer,从 1.11 开始,则进一步演进为使用 runC 和 containerd。
Docker 在容器的基础上,进行了进一步的封装,从文件系统、网络互联到进程隔离等等,极大的简化了容器的创建和维护。使得 Docker 技术比虚拟机技术更为轻便、快捷。
下面的图片比较了 Docker 和传统虚拟化方式的不同之处。传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。
Docker 镜像
获取镜像
从 Docker
镜像仓库获取镜像的命令是 docker pull
1 | docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签] |
- Docker 镜像仓库地址:地址的格式一般是 <域名/IP>[:端口号]。默认地址是 Docker Hub。
- 仓库名:如之前所说,这里的仓库名是两段式名称,即 <用户名>/<软件名>。对于 Docker Hub,如果不给出用户名,则默认为 library,也就是官方镜像。
运行
1 | $ docker run -it --rm tomcat bash |
docker run
就是运行容器的命令,我们这里简要的说明一下上面用到的参数。
-it
:这是两个参数,一个是 -i:交互式操作,一个是 -t 终端。我们这里打算进入 bash 执行一些命令并查看返回结果,因此我们需要交互式终端。--rm
:这个参数是说容器退出后随之将其删除。默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动 docker rm。我们这里只是随便执行个命令,看看结果,不需要排障和保留结果,因此使用 –rm 可以避免浪费空间。tomcat
: 这是指用 tomcat 镜像为基础来启动容器。bash
:放在镜像名后的是命令,这里我们希望有个交互式Shell
,因此用的是 bash。
最后我们通过
exit
(Ctrl+D) 退出了这个容器。
列出镜像
要想列出已经下载下来的镜像,可以使用 docker image ls
命令。
虚悬镜像:上面的镜像列表中,还可以看到一个特殊的镜像,这个镜像既没有仓库名,也没有标签,均为
1 | <none> <none> 00285df0df87 5 days ago 342 MB |
这个镜像原本是有镜像名和标签的,原来为 mongo:3.2
,随着官方镜像维护,发布了新版本后,重新 docker pull mongo:3.2
时,mongo:3.2
这个镜像名被转移到了新下载的镜像身上,而旧的镜像上的这个名称则被取消,从而成为了 <none>
。除了docker pull
可能导致这种情况,docker build
也同样可以导致这种现象。由于新旧镜像同名,旧镜像名称被取消,从而出现仓库名、标签均为 <none>
的镜像。这类无标签镜像也被称为 虚悬镜像(dangling image)
一般来说,虚悬镜像已经失去了存在的价值,是可以随意删除的,可以用下面的命令删除。
1 | docker image prune |
删除本地镜像
如果要删除本地的镜像,可以使用 docker image rm
命令,其格式为:
1 | docker image rm [选项] <镜像1> [<镜像2> ...] |
使用 Dockerfile 定制镜像
镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。
Dockerfile
是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
定制 tomcat 镜像为例,这次我们使用 Dockerfile 来定制。
1 | mkdir mytomcat |
Dockerfile文件内容
1 | FROM tomcat |
FROM指定基础镜像:
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个 nginx 镜像的容器,再进行修改一样,基础镜像是必须指定的。而 FROM 就是指定基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。
RUN执行命令:
RUN 指令是用来执行命令行命令的。由于命令行的强大能力,RUN 指令在定制镜像时是最常用的指令之一。
构建镜像
在 Dockerfile
文件所在目录执行:
1 | docker build -t tomcat_zyshep . |
从命令的输出结果中,我们可以清晰的看到镜像的构建过程。在 Step 2
中,如同我们之前所说的那样,RUN
指令启动了一个容器 9cdc27646c7b
,执行了所要求的命令,并最后提交了这一层 44aa4490ce2c
,随后删除了所用到的这个容器 9cdc27646c7b
。
这里我们使用了 docker build
命令进行镜像构建。其格式为:
1 | docker build [选项] <上下文路径/URL/-> |
在这里我们指定了最终镜像的名称 -t tomcat_zyshep,构建成功后,我们可以像之前运行 tomcat 那样来运行这个镜像,其结果会和 tomcat 一样。
镜像构建上下文(Context)
如果注意,会看到 docker build
命令最后有一个 .
。.
表示当前目录,而 Dockerfile
就在当前目录,因此不少初学者以为这个路径是在指定 Dockerfile
所在路径,这么理解其实是不准确的。如果对应上面的命令格式,你可能会发现,这是在指定上下文路径。那么什么是上下文呢?
首先我们要理解 docker build
的工作原理。Docker
在运行时分为 Docker
引擎(也就是服务端守护进程)和客户端工具。Docker
的引擎提供了一组 REST API
,被称为 ocker Remote API
,而如 docker
命令这样的客户端工具,则是通过这组 API
与 Docker
引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 docker
功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎
)完成。也因为这种 C/S
设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。
当我们进行镜像构建的时候,并非所有定制都会通过 RUN
指令完成,经常会需要将一些本地文件复制进镜像,比如通过 COPY
指令、ADD
指令等。而 docker build
命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker
引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?
这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build
命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker
引擎。这样 Docker
引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
如果在 Dockerfile
中这么写:
1 | COPY ./package.json /app/ |
这并不是要复制执行 docker build
命令所在的目录下的 package.json
,也不是复制 Dockerfile
所在目录下的 package.json
,而是复制 上下文(context) 目录下的 package.json
。
因此,COPY
这类指令中的源文件的路径都是相对路径。这也是初学者经常会问的为什么 COPY ../package.json /app
或者 COPY /opt/xxxx /app
无法工作的原因,因为这些路径已经超出了上下文的范围,Docker 引擎
无法获得这些位置的文件。如果真的需要那些文件,应该将它们复制到上下文目录中去。
现在就可以理解刚才的命令 docker build -t tomcat_zysheep .
中的这个.
,实际上是在指定上下文的目录,docker build
命令会将该目录下的内容打包交给 Docker 引擎
以帮助构建镜像。
如果观察 docker build
输出,我们其实已经看到了这个发送上下文的过程:
1 | $ docker build -t tomcat . |
理解构建上下文对于镜像构建是很重要的,避免犯一些不应该的错误。比如有些初学者在发现 COPY /opt/xxxx /app
不工作后,于是干脆将 Dockerfile
放到了硬盘根目录去构建,结果发现 docker build
执行后,在发送一个几十 GB 的东西,极为缓慢而且很容易构建失败。那是因为这种做法是在让 docker build
打包整个硬盘,这显然是使用错误。
一般来说,应该会将 Dockerfile
置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎
,那么可以用 .gitignore 一样的语法写一个 .dockerignore,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。
那么为什么会有人误以为 . 是指定 Dockerfile
所在目录呢?这是因为在默认情况下,如果不额外指定 Dockerfile
的话,会将上下文目录下的名为 Dockerfile
的文件作为 Dockerfile
。
这只是默认行为,实际上 Dockerfile
的文件名并不要求必须为 Dockerfile
,而且并不要求必须位于上下文目录中,比如可以用 -f ../Dockerfile.php 参数指定某个文件作为 Dockerfile。
当然,一般大家习惯性的会使用默认的文件名 Dockerfile
,以及会将其置于镜像构建上下文目录中。
Docker 容器
容器是独立运行的一个或一组应用,以及它们的运行态环境。对应的,虚拟机可以理解为模拟运行的一整套操作系统(提供了运行态环境和其他系统环境)和跑在上面的应用。
启动容器
启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(stopped)的容器重新启动。
新建并启动
所需要的命令主要为 docker run
。
启动一个 bash 终端,允许用户进行交互。
1 | docker run -t -i tomcat bash |
其中,-t 选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上, -i 则让容器的标准输入保持打开。
守护态运行
更多的时候,需要让 Docker
在后台运行而不是直接把执行命令的结果输出在当前宿主机下。此时,可以通过添加 -d
参数来实现。
1 | docker run -d ubuntu:17.10 /bin/sh -c "while true; do echo hello world; sleep 1; done" |
此时容器会在后台运行并不会把输出的结果 (STDOUT) 打印到宿主机上面(输出结果可以用 docker logs 查看)。
注: 容器是否会长久运行,是和 docker run 指定的命令有关,和 -d 参数无关。
终止容器
可以使用 docker container stop
来终止一个运行中的容器。
此外,当 Docker 容器中指定的应用终结时,容器也自动终止
终止状态的容器可以用 docker container ls -a
或者 docker ps -a
命令看到
1 | root@zysheep:~# docker container ls -a |
处于终止状态的容器,可以通过 docker container start
命令来重新启动。
此外,docker container restart
命令会将一个运行态的容器终止,然后再重新启动它。
进入容器
在使用 -d
参数时,容器启动后会进入后台。使用 docker attach
命令(如果从这个 stdin 中 exit,会导致容器的停止)。或 docker exec
命令,推荐大家使用 docker exec
命令
exec 命令
docker exec
后边可以跟多个参数,这里主要说明 -i -t
参数。
只用 -i
参数时,由于没有分配伪终端,界面没有我们熟悉的 Linux
命令提示符,但命令执行结果仍然可以返回。
当 -i -t
参数一起使用时,则可以看到我们熟悉的 Linux
命令提示符。
Docker 实战
数据卷 (达到负载均衡的效果 )
数据卷
是一个可供一个或多个容器使用的特殊目录,它绕过 UFS,可以提供很多有用的特性:
数据卷
可以在容器之间共享和重用- 对
数据卷
的修改会立马生效 - 对
数据卷
的更新,不会影响镜像 数据卷
默认会一直存在,即使容器被删除
注意:数据卷 的使用,类似于 Linux 下对目录或文件进行 mount,镜像中的被指定为挂载点的目录中的文件会隐藏掉,能显示看的是挂载的 数据卷。
启动一个挂载数据卷的容器
在用 docker run
命令的时候,使用 –mount 标记来将 数据卷
挂载到容器里。在一次 docker run
中可以挂载多个数据卷。
下面创建一个名为 web 的容器,并加载一个 数据卷 到容器的 /weapp 目录。
1 | docker run -p 8081:8080 --name myshop -d -v /usr/local/docker/tomcat/ROOT:/usr/local/tomcat/webapps/ROOT myshop |
删除数据卷
1 | $ docker volume rm my-vol |
数据卷
是被设计用来持久化数据的,它的生命周期独立于容器,Docker
不会在容器被删除后自动删除 数据卷
,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的 数据卷。如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用 docker rm -v
这个命令。
无主的数据卷可能会占据很多空间,要清理请使用以下命令
1 | docker volume prune |
Docker 构建 Tomcat
查找 Docker Hub 上的 Tom
1 | docker search tomcat |
这里我们拉取官方的镜像
1 | docker pull tomcat |
运行容器
1 | docker run -p 8081:8080 --name tomcat2 -v /usr/local/docker/tomcat/ROOT/:/usr/local/tomcat/webapps/ROOT/ tomcat |
查看容器启动情况
1 | docker ps |
通过浏览器访问
1 | 主机地址:端口号 |
docker logs 容器id 查看日志
docker logs -f 容器id 监听日志
Docker 构建 MySQL
查找 Docker Hub 上的 MySQL 镜像
1 | docker search mysql |
这里我们拉取官方的镜像
1 | docker pull mysql:5.7.22 |
运行容器:
1 | docker run -p 3306:3306 --name mysql \ |
查看容器启动情况
1 | docker ps |
使用客户端工具连接 MySQL
导入文件过大的错误
原因:mysql
容器中 /etc/mysql/conf.d
路径下的文件mysqldump.cnf限制了初始的大小
1 | [mysqldump] |
解决:在/etc/mysql/mysql.conf.d
路径文件mysqld.cnf
中添加内容max_allowed_packet = 128M
(这里我该大一点)
1 | each max_allowed_packet= 128M>> mysqld.cnf |
就可以导入成功了,重启mysql容器
1 | docker restart mysql |
复制容器文件到宿主机实现数据卷共用配置文件
1 | docker cp mysql:/ect/mysql . |
1 | mv *.* .. |
使用上面的运行容器命令就可以共用配置conf
文件了
1 | docker run -p 3306:3306 --name mysql \ |