Docker

基本概念

Docker包括三个基本概念:

  • 镜像(Image)
  • 容器(Container)
  • 仓库(Repository)

镜像

作系统分为内核和用户空间。对于 Linux 而言,内核启动后,会挂载root文件系统为其提供用户空间支持。而 Docker 镜像(Image),就相当于是一个root文件系统。
Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。

分层存储

因为镜像包含操作系统完整的root文件系统,其体积往往是庞大的,因此在 Docker 设计时,就充分利用 Union FS 的技术,将其设计为分层存储的架构。所以镜像只是一个虚拟的概念,其实际体现并非由一个文件组成,而是由一组文件系统组成,或者说,由多层文件系统联合组成。
镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。
分层存储的特征还使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。

容器

镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的命名空间。因此容器可以拥有自己的root文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。
每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为容器存储层
容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。
按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用 数据卷(Volume) 、或者绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。
数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此,使用数据卷后,容器删除或者重新运行之后,数据却不会丢失。

仓库

类似于github,用于托管镜像。最常使用的 Registry 公开服务是官方的 Docker Hub ,这也是默认的 Registry,并拥有大量的高质量的官方镜像。
仓库名经常以两段式路径形式出现,比如jwilder/nginx-proxy,前者往往意味着 Docker Registry 多用户环境下的用户名,后者则往往是对应的软件名。

macOS安装

使用Homebrew或者直接上官网下载.dmg文件

brew cask install docker

使用镜像

获取镜像

从Docker镜像仓库获取镜像的命令是docker pull

docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]

具体的选项可以通过docker pull —help命令看到

  • Docker镜像仓库地址:格式一般为<域名/IP>[:端口号],默认地址是Docker Hub
  • 仓库名:两段式名称,即<用户名>/<软件名>,对于Docker Hub,如果不给用户名,默认是library官方镜像

示例:

~ » docker pull ubuntu:18.04
18.04: Pulling from library/ubuntu
7413c47ba209: Pull complete 
0fe7e7cbb2e8: Pull complete 
1d425c982345: Pull complete 
344da5c95cec: Pull complete 
Digest: sha256:c303f19cfe9ee92badbbbd7567bc1ca47789f79303ddcef56f77687d4744cd7a
Status: Downloaded newer image for ubuntu:18.04

上面的命令中没有给出 Docker 镜像仓库地址,因此将会从 Docker Hub 获取镜像。而镜像名称是ubuntu:18.04,因此将会获取官方镜像library/ubuntu仓库中标签为18.04的镜像。

运行

~ » docker run -it --rm ubuntu:18.04 bash
root@52c958411df6:/# 

运行的命令是docker run,参数说明:

  • -it:这是两个参数,一个是-i:交互式操作,一个是-t终端。这里打算进入bash执行一些命令并查看返回结果,因此需要交互式终端。
  • --rm:这个参数是说容器退出后随之将其删除。默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动docker rm。这里只是随便执行个命令,看看结果,不需要排障和保留结果,因此使用—rm可以避免浪费空间。
  • ubuntu:18.04:这是指用ubuntu:18.04镜像为基础来启动容器。
  • bash:放在镜像名后的是命令,这里希望有个交互式 Shell,因此用的是bash

如果要退出该容器,可以使用exit

列出镜像

使用docker image ls命令可以列出已经下载的镜像

~ » docker image ls -a
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              e445ab08b2be        19 hours ago        126MB
ubuntu              18.04               3556258649b2        24 hours ago        64.2MB

类别包含了仓库名标签镜像ID,创建时间以及所占用的空间
镜像 ID则是镜像的唯一标识,一个镜像可以对应多个标签
此处列出的体积是解压后展开后各层所占空间的综合。同时,列表中的镜像体积总和不一定是所有镜像实际消耗的硬盘,由于 Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于 Docker 使用 Union FS,相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。
可以通过docker system df查看镜像,容器,数据卷所占用的空间

虚悬镜像

由于原本的镜像官方升级后,重新pull后镜像名会转移到新的镜像上,原有镜像就会成为虚悬镜像

<none>               <none>              00285df0df87        5 days ago          342 MB

可以通过docker image ls -f dangling=true专门显示这类镜像,这类镜像可以直接使用docker image prune删除掉

中间层镜像

为了加速镜像构建、重复利用资源,Docker 会利用中间层镜像。可以加上-a参数显示
这样会看到很多无标签的镜像,与之前的虚悬镜像不同,这些无标签的镜像很多都是中间层镜像,是其它镜像所依赖的镜像。这些无标签镜像不应该删除,否则会导致上层镜像因为依赖丢失而出错。

列出部分镜像

  1. 根据仓库名:docker image ls ubuntu
  2. 列出特定的某个镜像,也就是说指定仓库名和标签:docker image ls ubuntu:18.04
  3. 使用过滤器参数--filter或者简写-f,例如查看某个位置之前或者之后的镜像:docker image ls -f since/before=mongo:3.2
  4. 以特定格式表示:
  • -q:表示只列出ID

删除镜像

docker image rm [选项] <镜像1> [<镜像2> ...]

镜像可以是长ID短ID镜像名镜像摘要
默认显示的id是短ID,镜像名指的是<仓库名>:<标签>,
镜像摘要最为精确:

~ » docker image ls --digests
REPOSITORY          TAG                 DIGEST                                                                    IMAGE ID            CREATED             SIZE
ubuntu              18.04               sha256:c303f19cfe9ee92badbbbd7567bc1ca47789f79303ddcef56f77687d4744cd7a   3556258649b2        25 hours ago        64.2MB

~ » docker image rm ubuntu@sha256:c303f19cfe9ee92badbbbd7567bc1ca47789f79303ddcef56f77687d4744cd7a
Untagged: ubuntu@sha256:c303f19cfe9ee92badbbbd7567bc1ca47789f79303ddcef56f77687d4744cd7a

Untagged和Deleted

当使用命令删除镜像的时候,实际上是在要求删除某个标签的镜像。所以首先需要做的是将满足要求的所有镜像标签都取消,这就是我们看到的Untagged的信息。因为一个镜像可以对应多个标签,因此当我们删除了所指定的标签后,可能还有别的标签指向了这个镜像,如果是这种情况,那么Delete行为就不会发生。所以并非所有的docker image rm都会产生删除镜像的行为,有可能仅仅是取消了某个标签而已。
当该镜像所有的标签都被取消了,该镜像很可能会失去了存在的意义,因此会触发删除行为。镜像是多层存储结构,因此在删除的时候也是从上层向基础层方向依次进行判断删除。镜像的多层结构让镜像复用变得非常容易,因此很有可能某个其它镜像正依赖于当前镜像的某一层。这种情况,依旧不会触发删除该层的行为。直到没有任何层依赖当前层时,才会真实的删除当前层。
除了镜像依赖以外,还需要注意的是容器对镜像的依赖。如果有用这个镜像启动的容器存在(即使容器没有运行),那么同样不可以删除这个镜像。之前讲过,容器是以镜像为基础,再加一层容器存储层,组成这样的多层存储结构去运行的。因此该镜像如果被这个容器所依赖的,那么删除必然会导致故障。如果这些容器是不需要的,应该先将它们删除,然后再来删除镜像。

用 docker image ls 命令来配合

docker image rm $(docker image ls -q redis)
docker image rm $(docker image ls -q -f before=mongo:3.2)

容器

容器是独立运行的一个或一组应用,以及它们的运行态环境。对应的,虚拟机可以理解为模拟运行的一整套操作系统(提供了运行态环境和其他系统环境)和跑在上面的应用。

镜像是多层存储,每一层是在前一层的基础上进行的修改;而容器同样也是多层存储,是在以镜像为基础层,在其基础上加一层作为容器运行时的存储层。

启动容器

新建并启动

启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(stopped)的容器重新启动。
启动的命令是docker run-t选项表示让Docker分配一个伪终端,并绑定到容器的标准输入上,-i则让容器的标准输入保持打开
当利用docker run来创建容器时,Docker 在后台运行的标准操作包括:

  • 检查本地是否存在指定的镜像,不存在就从公有仓库下载
  • 利用镜像创建并启动一个容器
  • 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
  • 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
  • 从地址池配置一个 ip 地址给容器
  • 执行用户指定的应用程序
  • 执行完毕后容器被终止

启动已终止的容器

使用docker container start命令,直接将一个已经终止的容器启动运行

后台运行

添加-d参数可以是容器以守护态在后台运行执行的命令,可以通过docker container ls命令来查看容器信息,如果要获取容器的输出信息,可以通过docker container logs命令

终止容器

使用docker container stop可以终止一个运行的容器
此外,当 Docker 容器中指定的应用终结时,容器也自动终止。例如在启动了一个终端的容器,当我们通过exit退出终端时,所创建的容器会立即终止。

docker container stop [OPTIONS] CONTAINER [CONTAINER...]

处于终止状态的容器,可以通过docker container start命令重新启动,终止状态的容器可以通过docker container ls -a命令查看。
docker container restart命令会将一个运行态的容器终止,然后在重新启动它

进入容器

要进入在后台运行的容器,可以使用docker attach或者docker exec命令
区别:attach进入后exit,会导致容器停止,而exec不会
参数说明:
-i:保持标准输入,即使没有分配伪终端,仍然可以对命令执行结果返回
-t: 分配伪终端

$ docker run -dit ubuntu
69d137adef7a8a689cbcb059e94da5489d3cddd240ff675c640c8d96e84fe1f6

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
69d137adef7a        ubuntu:latest       "/bin/bash"         18 seconds ago      Up 17 seconds                           zealous_swirles

$ docker exec -i 69d1 bash
ls
bin
boot
dev
...

$ docker exec -it 69d1 bash
root@69d137adef7a:/#

导出和导入容器

如果要导出某个容器快照文件到本地,可以使用docker export命令

docker export CONTAINER ID > xxx.tar

使用docker import从容器快照文件中再导入为镜像

cat xxx.tar | docker import - test/ubuntu:v1.0

还可以通过指定URL或者某个目录来导入

docker import http://example.com/exampleimage.tgz example/imagerepo

注:既可以使用docker load来导入镜像存储文件到本地镜像库,也可以使用docker import来导入一个容器快照到本地镜像库。这两者的区别在于容器快照文件将丢弃所有的历史记录和元数据信息(即仅保存容器当时的快照状态),而镜像存储文件将保存完整记录,体积也要大。此外,从容器快照文件导入时可以重新指定标签等元数据信息。

删除容器

可以使用docker container rm删除一个处于终止状态的容器

docker container rm containerName

-f参数可以删除运行中的容器,Docker会对其发送SIGKILL信号
dcoker container prune可以删除所有处于终止状态的容器

仓库

库(Repository)是集中存放镜像的地方。
一个容易混淆的概念是注册服务器(Registry)。实际上注册服务器是管理仓库的具体服务器,每个服务器上可以有多个仓库,而每个仓库下面有多个镜像。从这方面来说,仓库可以被认为是一个具体的项目或目录。例如对于仓库地址dl.dockerpool.com/ubuntu来说,dl.dockerpool.com是注册服务器地址,ubuntu是仓库名。

Docker 官方维护了一个公共仓库 Docker Hub

登录

可以通过执行doker login命令交互式输入用户名及密码在命令行登录Docker Hub。使用docker logout退出登录

拉取镜像

docker search命令可以用来查找官方仓库中的镜像,并利用docker pull下载到本地

推送镜像

docker push命令可以将自己的镜像推送到Docker Hub

docker push usrname/ubuntu:18.04

自动构建

自动构建(Automated Builds)功能对于需要经常升级镜像内程序来说,十分方便。
有时候,用户构建了镜像,安装了某个软件,当软件发布新版本则需要手动更新镜像。
而自动构建允许用户通过 Docker Hub 指定跟踪一个目标网站(支持 GitHubBitBucket )上的项目,一旦项目发生新的提交 (commit)或者创建了新的标签(tag),Docker Hub 会自动构建镜像并推送到 Docker Hub 中。
要配置自动构建,包括如下的步骤:

  • 登录 Docker Hub;
  • 在 Docker Hub 点击右上角头像,在账号设置(Account Settings)中关联(Linked Accounts)目标网站;
  • 在 Docker Hub 中新建或选择已有的仓库,在Builds选项卡中选择Configure Automated Builds;
  • 选取一个目标网站中的项目(需要含Dockerfile)和分支;
  • 指定Dockerfile的位置,并保存。

之后,可以在 Docker Hub 的仓库页面的Timeline选项卡中查看每次构建的状态。

定制镜像

commit命令理解镜像构成

我们可以根据自己的需要对本地运行的容器进行修改,通过使用docker exec命令进入容器,修改容器文件(也就是容器的存储层

# 表示以交互终端方式进入webserver容器,并进行bash命令,获得可以可操作的shell
docker exec -it webserber bash

修改之后可以通过docker diff命令查看具体的改动
当我们运行一个容器的时候(如果不使用卷的话),我们做的任何文件修改都会被记录于容器存储层里。而 Docker 提供了一个docker commit命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。

docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]

--author表示指定修改的作者,--message记录本次修改的内容,可以为空
可以用docker history具体查看镜像内的历史记录

慎用docker commit

使用docker commit命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。
首先,通过观察docker diff webserver的结果,会发现除了真正想要修改的文件外,由于命令的执行,还有很多文件被改动或添加了。这还仅仅是最简单的操作,如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,如果不小心清理,将会导致镜像极为臃肿。
此外,使用docker commit意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为黑箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具体在操作的。虽然docker diff或许可以告诉得到一些线索,但是远远不到可以确保生成一致镜像的地步。这种黑箱镜像的维护工作是非常痛苦的。
而且,回顾之前提及的镜像所使用的分层存储的概念,除当前层外,之前的每一层都是不会发生改变的,换句话说,任何修改的结果仅仅是在当前层进行标记、添加、修改,而不会改动上一层。如果使用docker commit制作镜像,以及后期修改的话,每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,即使根本无法访问到。这会让镜像更加臃肿。

使用Dockerfile定制镜像

我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。
Dockerfile 是一个文本文件,其内包含了一条条的**指令(Instruction)**,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
例如,在一个空白目录新建一个Dockerfile的文本文件,内容如下:

FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

FROM指定基础镜像

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。FROM就是指定基础镜像,因此一个Dockerfile中FROM是必备的指令,并且必须是第一条指令。
如果不以任何镜像为基础,则可以使用Docker中的特殊镜像:scratch,表示这是一个空白镜像,接下来所写的指令将作为镜像第一层开始存在。

RUN执行命令

RUN指令是用来执行命令行命令的,其形式有以下两种:

  • shell格式:Run <命令>,就像直接在命令行中输入的命令一样。
  • exec格式:RUN ["可执行文件", "参数1", "参数2"],,这更像是函数调用中的格式。

DockerFile中,每一个Run行为,都会新建立一层,所以要尽可能的使用更少的Run,可以使用&&将各个所需命令串联起来。Dockerfile 还支持 Shell 类的行尾添加\的命令换行方式,以及行首#进行注释的格式。此外,在一组命令的最后添加清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了apt缓存文件。这是很重要的一步,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。

FROM debian:stretch

RUN buildDeps='gcc libc6-dev make wget' \
    && apt-get update \
    && apt-get install -y $buildDeps \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.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

构建镜像

在dockerfile所在目录使用docker build命令进行镜像构建,格式如下:

docker builder [选项] <上下文路径/URL/->

其中,url可以是Git repo,tar包。-表示标准输入中读取,可以是:

  • 从标准输入中读取Dockerfile
docker build - < Dockerfile
cat Dockerfile | docker build -
  • 从标准输入中读取上下文压缩包进行构建
docker build - < context.tar.gz

例如:

» docker build -t nginx:v3 . 
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM nginx
 ---> e445ab08b2be
Step 2/2 : RUN echo '<h1>Hello,Docker!</h1>' > /usr/share/nginx/html/index.html
 ---> Running in 07b98b86f9ff
Removing intermediate container 07b98b86f9ff
 ---> f9aad93f8f41
Successfully built f9aad93f8f41
Successfully tagged nginx:v3

-t指定了最终镜像的名称

docker build工作原理:Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API ,而如docker命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种docker功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。

镜像构建上下文(Context)

进行镜像构建的时候,并非所有定制都会通过RUN指令完成,经常会需要将一些本地文件复制进镜像,比如通过COPY指令、ADD指令等。而docker build命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的。当构建的时候,用户会指定构建镜像上下文的路径,docker build命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。

Docker数据管理

在容器中管理数据主要有两种方式:

  • 数据卷
  • 挂载主机目录

数据卷

数据卷是一个可供一个或多个容器使用的特殊目录,它拥有以下特性:

  • 数据卷可以在容器之间共享和重用
  • 对数据卷的修改会立马生效
  • 对数据卷的更新,不会影响镜像
  • 数据卷默认会一致存在,即使容器被删除

创建数据卷

# 创建一个数据卷
docker volume create my-vol
# 查看所有的数据卷
docker volume ls

启动一个挂载数据卷的容器

在用docker run命令的时候,使用—mount标记来将数据卷挂载到容器里。在一次docker run中可以挂载多个数据卷。
下面创建一个名为web的容器,并加载一个数据卷到容器的/webapp目录。

docker run -d -P --name web --mount source=my-val,target=/webapp training/webapp python app.py

查看数据卷的具体信息

通过使用以下命令可以查看某一容器的信息,数据卷信息在”Mounts”下面

docker inspect web

删除数据卷

docker volume rm my-vol

# 删除无主的数据卷
docker volume prune

数据卷是被设计用来持久化数据的,它的生命周期独立于容器,Docker 不会在容器被删除后自动删除数据卷,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的数据卷。如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用docker rm -v这个命令。

挂载主机目录

挂载一个主机目录作为数据卷

使用—mount标记可以指定挂载一个本地主机的目录到容器中去。
例如下面的命令加载主机的/src/webapp目录到容器的/opt/webapp目录。这个功能在进行测试的时候十分方便,比如用户可以放置一些程序到本地目录中,来查看容器是否正常工作。本地目录的路径必须是绝对路径
Docker 挂载主机目录的默认权限是读写,用户也可以通过增加readonly指定为只读

$ docker run -d -P \
    --name web \
    --mount type=bind,source=/src/webapp,target=/opt/webapp,readonly \
    training/webapp \
    python app.py

挂载一个本地主机文件作为数据卷

—mount标记也可以从主机挂载单个文件到容器中,这样就可以记录在容器输入过的命令了

$ docker run --rm -it \
   # -v $HOME/.bash_history:/root/.bash_history \
   --mount type=bind,source=$HOME/.bash_history,target=/root/.bash_history \
   ubuntu:18.04 \
   bash

root@2affd44b4667:/# history
1  ls
2  diskutil list