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
2
3
docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]

docker pull tomcat
  • 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
2
3
docker image rm [选项] <镜像1> [<镜像2> ...]

其中,<镜像> 可以是镜像短 ID、镜像长 ID、镜像名 或者 镜像摘要。

使用 Dockerfile 定制镜像

镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。

Dockerfile 是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

定制 tomcat 镜像为例,这次我们使用 Dockerfile 来定制。

1
2
3
4
5
mkdir mytomcat
cd mytomcat
touch Dockerfile

vim Dockerfile

Dockerfile文件内容

1
2
FROM tomcat 
RUN echo "hello docker" > index.html

FROM指定基础镜像:
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个 nginx 镜像的容器,再进行修改一样,基础镜像是必须指定的。而 FROM 就是指定基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。

RUN执行命令:
RUN 指令是用来执行命令行命令的。由于命令行的强大能力,RUN 指令在定制镜像时是最常用的指令之一。

构建镜像

Dockerfile 文件所在目录执行:

1
2
3
4
5
6
7
8
9
docker build -t tomcat_zyshep .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM tomcat
---> e43d811ce2f4
Step 2 : RUN echo '<h1>Hello, Docker!</h1>' > index.html
---> Running in 9cdc27646c7b
---> 44aa4490ce2c
Removing intermediate container 9cdc27646c7b
Successfully built 44aa4490ce2c

从命令的输出结果中,我们可以清晰的看到镜像的构建过程。在 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 命令这样的客户端工具,则是通过这组 APIDocker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 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
2
$ docker build -t tomcat .
Sending build context to Docker daemon 2.048 kB

理解构建上下文对于镜像构建是很重要的,避免犯一些不应该的错误。比如有些初学者在发现 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
2
docker run -d ubuntu:17.10 /bin/sh -c "while true; do echo hello world; sleep 1; done"
77b2dc01fe0f3f1265df143181e7b9af5e05279a884f4776ee75350ea9d8017a

此时容器会在后台运行并不会把输出的结果 (STDOUT) 打印到宿主机上面(输出结果可以用 docker logs 查看)。

注: 容器是否会长久运行,是和 docker run 指定的命令有关,和 -d 参数无关。

终止容器

可以使用 docker container stop 来终止一个运行中的容器。

此外,当 Docker 容器中指定的应用终结时,容器也自动终止

终止状态的容器可以用 docker container ls -a 或者 docker ps -a 命令看到

1
2
3
4
root@zysheep:~# docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5a68ca3aa075 mysql:5.7.22 "docker-entrypoint.s…" 2 hours ago Up 8 minutes 0.0.0.0:3306->3306/tcp mysql
9f676a420eee zysheep "catalina.sh run" 2 hours ago Up 8 minutes 0.0.0.0:8080->8080/tcp tomcat_zysheep

处于终止状态的容器,可以通过 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
2
3
4
5
6
7
docker run -p 8081:8080 --name myshop -d -v /usr/local/docker/tomcat/ROOT:/usr/local/tomcat/webapps/ROOT myshop

docker run 启动一个容器
-p(端口映射) 宿主机端口:容器默认端口
--name 容器名字
-d 以守护状态运行在后台运行容器
-v 数据卷路径:挂载的路径

删除数据卷

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
2
3
4
5
docker run -p 8081:8080 --name tomcat2 -v /usr/local/docker/tomcat/ROOT/:/usr/local/tomcat/webapps/ROOT/ tomcat
命令说明:
-p 8080:8080:将容器的8080端口映射到主机的8080端口
-v /usr/local/docker/tomcat/ROOT/:/usr/local/tomcat/webapps/ROOT/:将主机中当前目录下的ROOT挂载到容器的/ROOT
容器没有则自动创建

查看容器启动情况

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
docker run -p 3306:3306 --name mysql \
-v /usr/local/docker/mysql/conf:/etc/mysql \
-v /usr/local/docker/mysql/logs:/var/log/mysql \
-v /usr/local/docker/mysql/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-d mysql:5.7.22


命令参数:
-p 3306:3306:将容器的3306端口映射到主机的3306端口

-v /usr/local/docker/mysql/conf:/etc/mysql:将主机当前目录下的 conf 挂载到容器的 /etc/mysql(直接使用这个命令因为数据卷里面没有东西,所以挂载mysql容器conf目录下也为空)

-v /usr/local/docker/mysql/logs:/var/log/mysql:将主机当前目录下的 logs 目录挂载到容器的 /var/log/mysql

-v /usr/local/docker/mysql/data:/var/lib/mysql:将主机当前目录下的 data 目录挂载到容器的 /var/lib/mysql

-e MYSQL\_ROOT\_PASSWORD=123456:初始化root用户的密码

查看容器启动情况

1
docker ps

使用客户端工具连接 MySQL

导入文件过大的错误

原因:mysql容器中 /etc/mysql/conf.d路径下的文件mysqldump.cnf限制了初始的大小

1
2
3
4
[mysqldump]
quick
quote-names
max_allowed_packet = 16M

解决:在/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
2
3
4
5
docker cp mysql:/ect/mysql  .   
将容器里的文件复制到宿主机

mysql 容器名:文件位置
. 当前目录
1
2
3
4
mv *.* ..
移动所有文件到上一级目录

rm -rf mysql

使用上面的运行容器命令就可以共用配置conf文件了

1
2
3
4
5
6
docker run -p 3306:3306 --name mysql \
-v /usr/local/docker/mysql/conf:/etc/mysql \
-v /usr/local/docker/mysql/logs:/var/log/mysql \
-v /usr/local/docker/mysql/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-d mysql:5.7.22