Docker 是一个应用打包、分发、部署的工具。打包就是把软件运行所需的依赖、第三方库、软件打包到一起,变成一个安装包;分发可以把打包好的“安装包”上传到一个镜像仓库,其他人可以非常方便的获取和安装;部署:拿着“安装包”就可以一个命令运行起来你的应用,自动模拟出一摸一样的运行环境,不管是在 Windows/Mac/Linux。
docker设计思想来源于港口码头工人搬运货物
在这里插入图片描述
docker里面有程序和程序运行环境(如openssl、依赖库),这样的话,就可以避免不同程序依赖的版本不同所导致的影响。你也可以把它理解为一个轻量的虚拟机,它只虚拟你软件需要的运行环境,多余的一点都不要,而普通虚拟机则是一个完整而庞大的系统,包含各种不管你要不要的软件。后面会专门对docker和虚拟机做对比。

概述

docker的好处

在这里插入图片描述
上面给了个效果图,具体来说

  • 替代虚拟机:用户只需要关心应用程序,而不是操作系统。
  • 软件原型:快速体验软件,同时避免干扰目前的设置或配备一个虚拟机的麻烦。比如要测试不同版本的 redis,可以使用docker开启多个ubuntu镜像,然后在不同的ubuntu镜像测试不同的redis版本。
  • 打包软件:docker镜像对Linux用户没有依赖,可以将构建的镜像运行在不同的Linux机器上。
  • 让微服务成为可能:docker有助于将一个复杂的系统分解成一系列可组合的部分,有利于用户更好地思考其服务。
  • 网络建模:可以在一台机器上启动数百个隔离的容器,因而对网络进行建模轻而易举。这对于显示世界场景的测试非常有用。
  • 降低调试支出:可以让生产、测试、部署统一环境,而不因不同环境:失效的库、有问题的依赖、更新被错误实施或是执行顺序有误,甚至可能根本没有执行以及无法出现的错误等等。
  • 启用持续交付:更利于构建一个基于流水线的软件交付模型。

容器和虚拟机的区别

在这里插入图片描述
vm(虚拟机)与docker(容器)框架相比,直观上来讲vm多了一层guest OS,同时Hypervisor会对硬件资源进行虚拟化,docker直接使用硬件资源,所以资源利用率相对docker低也是比较容易理解的。
服务器虚拟化解决的核心问题是资源调配,而容器解决的核心问题是应用开发、测试和部署。
虚拟机技术通过Hypervisor层抽象底层基础设施资源,提供相互隔离的虚拟机,通过统一配置、统一管理,计算资源的可运维性,以及资源利用率都能够得到有效的提升。同时,虚拟机提供客户机操作系统,客户机变化不会影响宿主机,能够提供可控的测试环境,更能够屏蔽底层硬件甚至基础软件的差异性,让应用做到的广泛兼容。然而,再厉害的虚拟化技术,都不可避免地出现计算、IO、网络性能损失,毕竟多了一层软件,毕竟要运行一个完整的客户机操作系统。
容器技术严格来说并不是虚拟化,没有客户机操作系统,是共享内核的。容器可以视为软件供应链的集装箱,能够把应用需要的运行环境、缓存环境、数据库环境等等封装起来,以最简洁的方式支持应用运行,轻装上阵,当然是性能更佳。Docker镜像特性则让这种方式简单易行。当然,因为共享内核,容器隔离性也没有虚拟机那么好。
docker并不是虚拟化的运行,是真正运行在物理cpu上;虚拟机是虚拟化,并没有真正运行在cpu上。性能上可以做个测试,虚拟机的cpu效率只能达到50~60%,而docker能达到90%,甚至趋近100%。所以不要在虚拟机上测试cpu的性能。内存方面,两者都差不多。
给个直观的图
在这里插入图片描述

基本概念

Docker 包括三个基本概念:

  • 镜像(Image):Docker 镜像(Image),就相当于是一个 root 文件系统。比如官方镜像ubuntu:16.04 就包含了完整的一套 Ubuntu16.04 最小系统的 root 文件系统。
  • 容器(Container):镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。容器与镜像的关系类似于面向对象编程中的对象与类。Docker 使用客户端-服务器 (C/S) 架构模式,使用远程API来管理和创建Docker容器。Docker 容器通过 Docker 镜像来创建。也可以这样做类比,镜像理解为静态的程序,而容器理解为运行起来后的进程。容器使用写时复制技术,修改后容器的文件是独立的。
  • 仓库(Repository):仓库可看成一个代码控制中心,用来保存镜像。

在这里插入图片描述
在这里插入图片描述

docker安装

这里介绍ubuntu中安装docker ce的方法。

准备工作

安装之前,先卸载旧版本。旧版本的 Docker 称为 docker 或者 docker-engine ,使用以下命令卸载旧版本

sudo apt-get remove docker docker-engine docker.io containerd runc

现在的docker推荐使用 overlay2 文件系统,在现在的版本中,无需手动配置。安装之前,还要安装一些依赖。由于 apt 源使用 HTTPS 以确保软件下载过程中不被篡改。因此,我们首先需要添加使用 HTTPS 传输的软件包以及 CA 证书。

# 先更新一下
sudo apt-get update
 
# 安装相应的依赖
sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common

为了确认所下载软件包的合法性,需要添加软件源的 GPG 密钥。

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

验证密钥

sudo apt-key fingerprint 0EBFCD88

根据系统架构去设置一个稳定的仓库

sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"

注意不同的架构,命令会不一样。
鉴于国内网络问题,建议使用国内源,上两步的命令改一下,主要改地址

# 添加密钥
curl -fsSL https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/gpg | sudo apt-key add -
# 设置仓库
sudo add-apt-repository \
    "deb [arch=amd64] https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu \
    $(lsb_release -cs) \
    stable"

正式安装

再更新一次 apt

sudo apt-get update

正式安装

sudo apt-get install docker-ce docker-ce-cli containerd.io

查看docker版本号

docker -v

后续操作

默认情况下, docker 命令会使用 Unix socket 与 Docker 引擎通讯。而只有 root 用户和 docker 组的用户才可以访问 Docker 引擎的 Unix socket。出于安全考虑,一般 Linux 系统上不会直接使用 root用户。因此,更好地做法是将需要使用 docker 的用户加入 docker 用户组。

# 建立 docker 组
sudo groupadd docker
# 将当前用户加入 docker 组
sudo usermod -aG docker $USER

$USER是一个环境变量,代表当前用户名。登出,并且重新登录,以便用户组会员信息刷新。如果是通过类似xshell之类的连接,再开一个连接即可。
接下来,测试 Docker 是否安装正确

docker run hello-world

如果能输出Hello from Docker!,则表示安装成功。

最后,给一些docker服务命令

systemctl enable docker # 开机自动启动docker
systemctl start docker # 启动docker
systemctl restart docker # 重启dokcer
systemctl stop docker # 关闭dokcer

操作镜像

获取镜像docker pull

从 Docker 镜像仓库获取镜像的命令是 docker pull ,与git相似。其命令格式为

docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]
  • 选项:可以通过 docker pull --help 命令看到,这里不过多介绍。
  • Docker 镜像仓库地址:地址的格式一般是 <域名/IP>[:端口号] 。默认地址是 Docker Hub。
  • 仓库名:这里的仓库名是两段式名称,即 <用户名>/<软件名> 。对于 Docker Hub,如果不给出用户名,则默认为 library ,也就是官方镜像。
    给个例子
docker pull redis:6.0.8 # 标签6.0.8对应redis版本号

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

运行docker run

有了镜像后,我们就能够以这个镜像为基础启动并运行一个容器。由于这里不是主要介绍容器的,为避免概念混乱,不作过多介绍。
以上面的 redis:6.08 为例,如果我们打算启动里面的 bash 并且进行交互式操作的话,可以执行下面的命令。

docker run -it --rm redis:6.0.8 bash

–rm代表运行结束就删除容器
最后可以通过exit退出容器。

查看本地已有的镜像ls

docker image ls

返回本地镜像的大小、id、创建时间等信息。

删除本地镜像docker image rm

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

其中,<镜像> 可以是镜像短 ID 、镜像长 ID 、镜像名或镜像摘要。
我们可以用镜像的完整 ID,也称为长ID ,来删除镜像。使用脚本的时候可能会用长 ID,但是人工输入就太累了,所以更多的时候是用短ID来删除镜像。 docker image ls 默认列出的就已经是短 ID了,一般取前3个字符以上,只要足够区分镜像就可以了。
当然,更精确的是使用镜像摘要删除镜像。通过docker image ls --digests查看镜像的sha256摘要,然后删除。

docker image rm centos
Untagged: centos:latest
Untagged:centos@sha256:b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c
Deleted: sha256:0584b3d2cf6d235ee310cf14b54667d889887b838d3f3d3033acd70fc3c48b8a
Deleted: sha256:97ca462ad9eeae25941546209454496e1d66749d53dfa2ee32bf1faabd239d38

注意如果观察上面这几个命令的运行输出信息的话,你会注意到删除行为分为两类,一类是 Untagged ,另一类是 Deleted 。我们之前介绍过,镜像的唯一标识是其 ID 和摘要,而一个镜像可以有多个标签。
因此当我们使用上面命令删除镜像的时候,实际上是在要求删除某个标签的镜像。所以首先需要做的是将满足我们要求的所有镜像标签都取消,这就是我们看到的 Untagged 的信息。因为一个镜像可以对应多个标签,因此当我们删除了所指定的标签后,可能还有别的标签指向了这个镜像,如果是这种情况,那么 Delete 行为就不会发生。所以并非所有的 docker rmi 都会产生删除镜像的行为,有可能仅仅是取消了某个标签而已。
当该镜像所有的标签都被取消了,该镜像很可能会失去了存在的意义,因此会触发删除行为。镜像是多层存储结构,因此在删除的时候也是从上层向基础层方向依次进行判断删除。镜像的多层结构让镜像复用变动非常容易,因此很有可能某个其它镜像正依赖于当前镜像的某一层。这种情况,依旧不会触发删除该层的行为。直到没有任何层依赖当前层时,才会真实的删除当前层。这就是为什么,有时候会奇怪,为什么明明没有别的标签指向这个镜像,但是它还是存在的原因,也是为什么有时候会发现所删除的层数和自己 docker pull 看到的层数不一样的源。
除了镜像依赖以外,还需要注意的是容器对镜像的依赖。如果有用这个镜像启动的容器存在(即使容器没有运行),那么同样不可以删除这个镜像。之前讲过,容器是以镜像为基础,再加一层容器存储层,组成这样的多层存储结构去运行的。因此该镜像如果被这个容器所依赖的,那么删除必然会导致故障。如果这些容器是不需要的,应该先将它们删除,然后再来删除镜像。
像其它可以承接多个实体的命令一样,可以使用 docker image ls -q 来配合使用 docker image rm ,这样可以成批的删除希望删除的镜像。比如,我们需要删除所有仓库名为 redis 的镜像

docker image rm $(docker image ls -q redis)

提交一个成为新镜像docker commit

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

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

例如,我们可以用下面的命令将容器保存为镜像

docker commit \
    --author "s" \
    --message "修改了默认网页" \
    webserver \
    nginx:v2

返回sha256值则保存成功。
其中 --author 是指定修改的作者,而 --message 则是记录本次修改的内容。这点和 git 版本控制相似,不过这里这些信息可以省略留空。
我们可以用docker image ls看到这个新定制的镜像,还可以用 docker history 具体查看镜像内的历史记录,发现新增了刚刚提交的这一层。
新的镜像定制好后,我们可以来运行这个镜像。

docker run --name web2 -d -p 81:80 nginx:v2

这里我们命名为新的服务为 web2 ,并且映射宿主机的81端口到docker的80端口。我们可以直接访问 。这里解释一下宿主机端口和docker端口。如果宿主机端口相同,会产生冲突,但是对于docker端口,不同容器之间即使端口相同,也不会冲突。这一点类似于物理内存和虚拟内存。http://localhost:81 看到结果。

定制镜像

使用docker commit

我们可以用docker commit定制镜像。

# 用 nginx 镜像启动一个容器,命名为 webserver ,并且映射了 80 端口
docker run --name webserver -d -p 80:80 nginx
# 使用 docker exec 命令进入容器,修改其内容。
docker exec -it webserver bash
# docker终端
root@3729b97e8226:/# echo '<h1>Hello, Docker!</h1>' >
/usr/share/nginx/html/index.html
# 注意退出终端exit

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

使用Dockerfile

从刚才的 docker commit 的介绍中,我们可以了解到,镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。
Dockerfile 是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
还以之前定制 nginx 镜像为例,这次我们使用 Dockerfile 来定制。在一个空白目录中,建立一个文本文件,并命名为 Dockerfile ,其内容

FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
FROM 指定基础镜像

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个 nginx 镜像的容器,再进行修改一样,基础镜像是必须指定的。而 FROM 就是指定基础镜像,因此一个Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。
除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch 。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

FROM scratch

如果以 scratch 为基础镜像的话,意味着不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。
不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,比如swarm、coreos/etcd。对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch 会让镜像体积更加小巧。

RUN执行命令。

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

  • shell 格式: RUN <命令> ,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN指令就是这种格式。
  • exec 格式: RUN ["可执行文件", "参数1", "参数2"] ,这更像是函数调用中的格式。
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 层镜像。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。这是很多初学 Docker 的人常犯的一个错误。
Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。
上面的 Dockerfile 正确的写法应该是这样

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

这样写,有几个好处:
首先,之前所有的命令只有一个目的,就是编译、安装 redis 可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,这里没有使用很多个 RUN 对一一对应不同的命令,而是仅仅使用一个 RUN指令,并使用 && 将各个所需命令串联起来。将之前的 7 层,简化为了 1 层。在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。
并且,这里为了格式化还进行了换行。Dockerfile 支持 Shell 类的行尾添加 \ 的命令换行方式,以及行首 # 进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。
此外,还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt 缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。
很多人初学 Docker 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。

构建镜像

然后在该目录中build镜像

-t 标签名
docker build -t mynginx:v3 .

在这里我们指定了最终镜像的名称 -t nginx:v3 ,构建成功后,我们可以像之前那样来运行这个镜像。

镜像构建上下文(Context)

使用docker build命令进行镜像构建。其格式为

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

上下文是什么意思呢?首先我们要理解 docker build 的工作原理。Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如docker 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 docker 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。
当我们进行镜像构建的时候,并非所有定制都会通过 RUN 指令完成,经常会需要将一些本地文件复制进镜像,比如通过 COPY 指令、 ADD 指令等。而 docker build 命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径, docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
如果在 Dockerfile 中这么写

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 nginx:v3 .中的这个.,实际上是在指定上下文的目录, docker build 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。
理解构建上下文对于镜像构建是很重要的,避免犯一些不应该的错误。比如有些初学者在发现 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 ,以及会将其置于镜像构建上下文目录中。

有关Dockerfile的内容还有很多,由于篇幅问题,不继续介绍。

操作Docker容器

启动容器

启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态( stopped )的容器重新启动。
因为 Docker 的容器实在太轻量级了,很多时候用户都是随时删除和新创建容器。

新建容器并启动

命令为 docker run
先拉取ubuntu:16.04

sudo docker pull ubuntu:16.04

新建一个容器,执行命令,再终止。

sudo docker run ubuntu:16.04 /bin/echo 'Hello world'

此命令输出一个 “Hello World”,之后终止容器。这跟在本地直接执行 /bin/echo 'hello world' 几乎感觉不出任何区别。
当利用 docker run 来创建容器时,Docker 在后台运行的标准操作包括:

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

下面的命令则启动一个 bash 终端,允许用户进行交互

ubuntu@VM-0-13-ubuntu:~$ sudo docker run -t -i ubuntu:16.04 /bin/bash
root@b1d435526f3d:/#

其中, -t 选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上, -i 则让容器的标准输入保持打开。在交互模式下,用户可以通过所创建的终端来输入命令。

启动已终止容器

可以利用 docker container start 命令,直接将一个已经终止的容器启动运行。此外, docker container restart 命令会将一个运行态的容器终止,然后再重新启动它。
容器的核心为所执行的应用程序,所需要的资源都是应用程序运行所必需的。除此之外,并没有其它的资源。可以在伪终端中利用 ps 或 top 来查看进程信息。这个方式用得不多,就不多介绍了。

后台运行

更多的时候,需要让 Docker 在后台运行而不是直接把执行命令的结果输出在当前宿主机下。此时,可以通过添加 -d 参数来实现。
这里给个例子。
不使用-d

sudo docker run ubuntu:16.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
hello world
hello world
hello world

容器会把输出的结果 (STDOUT) 打印到宿主机上面。
如果使用了 -d 参数运行容器

sudo docker run -d ubuntu:16.04 /bin/sh -c "while true;do echo hellworld; sleep 1; done"
fb1dce2083d220be710231b9d40136394d8ce3f4222a8c689d1713810a2fe690

使用 -d 参数启动后会返回一个唯一的 id (CONTAINER ID),也可以通过 docker container ls 命令来查看容器信息。容器会在后台运行,并不会把输出的结果 (STDOUT) 打印到宿主机上面(输出结果可以用 dockerlogs 查看)。

docker container logs [container ID or NAMES]

注意,容器是否会长久运行,是和 docker run 指定的命令有关,和 -d 参数无关。
可以通过docker ps -q命令查看正在运行的容器。

终止容器

可以使用docker container stop来终止一个运行中的容器。此外,当 Docker 容器中指定的应用终结时,容器也自动终止。
例如对于上面那个例子,只启动了一个终端的容器,用户通过 exit 命令或 Ctrl+d 来退出终端时,所创建的容器立刻终止。如果想要在容器不退出的情况下返回宿主机,则使用Ctrl+P+Q。
终止状态的容器可以用 docker container ls -a 命令看到。
先终止容器才能删除容器,先删除容器才能删除镜像,因为容器对镜像有依赖。

进入容器

在使用 -d 参数时,容器启动后会进入后台。但是。某些时候需要进入容器进行操作,可以使用 docker attach 命令或 docker exec 命令,推荐使用 docker exec 命令,原因会在下面说明。

attach

docker attach CONTAINER ID

CONTAINER ID不需要全写,能区分开就行。
如果使用attach命令,使用exit退出容器,会导致容器的停止。

exec

docker exec [参数] CONTAINER ID [可选项]

这里参数只介绍-i、-t更多参数说明请使用 docker exec --help 查看。
只用 -i 参数时,由于没有分配伪终端,界面没有我们熟悉的 Linux 命令提示符,但命令执行结果仍然可以返回。当 -i -t 参数一起使用时,则可以看到我们熟悉的 Linux 命令提示符。
如果使用attach命令,使用exit退出容器,会导致容器的停止。这就是推荐使用docker exec 的原因。

操作仓库

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

官方公共仓库Docker Hub

目前 Docker 官方维护了一个公共仓库 Docker Hub,其中已经包括了数量超过 15000 的镜像。大部分需求都可以通过在 Docker Hub 中直接下载镜像来实现。
公共仓库的使用方法就不介绍了。

私有仓库

有时候使用 Docker Hub 这样的公共仓库可能不方便,用户可以创建一个本地仓库供私人使用。
docker-registry是官方提供的工具,可以用于构建私有的镜像仓库。

安装运行 docker-registry

可以通过获取官方 registry 镜像来运行。

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

这将使用官方的 registry 镜像来启动私有仓库。默认情况下,仓库会被创建在容器的/var/lib/registry 目录下。你可以通过 -v 参数来将镜像文件存放在本地的指定路径。例如下面的例子将上传的镜像放到本地的 /opt/data/registry 目录。

docker run -d \
    -p 5000:5000 \
    -v /opt/data/registry:/var/lib/registry \
    registry

在私有仓库上传、搜索、下载镜像

创建好私有仓库之后,就可以使用 docker tag 来标记一个镜像,然后推送它到仓库。例如私有仓库地址为 127.0.0.1:5000 。
使用 docker tag 将 ubuntu:latest 这个镜像标记为 127.0.0.1:5000/ubuntu:latest
格式为

docker tag IMAGE[:TAG] [REGISTRY_HOST[:REGISTRY_PORT]/]REPOSITORY[:TAG]

使用 docker push 上传标记的镜像。

docker push 127.0.0.1:5000/ubuntu:latest

用 curl 查看仓库中的镜像。

curl 127.0.0.1:5000/v2/_catalog

看到 {"repositories":["ubuntu"]},表明镜像已经被成功上传了。
删除和下载镜像

docker image rm 127.0.0.1:5000/ubuntu:latest
docker pull 127.0.0.1:5000/ubuntu:latest

如果不想使用 127.0.0.1:5000 作为仓库地址,比如想让本网段的其他主机也能把镜像推送到私有仓库。把例如 192.168.199.100:5000 这样的内网地址作为私有仓库地址,这时会发现无法成功推送镜像。
这是因为 Docker 默认不允许非 HTTPS 方式推送镜像。我们可以通过 Docker 的配置选项来取消这个限制,或者通过下面的配置能够通过 HTTPS 访问的私有仓库。
对于使用 systemd 的系统,在 /etc/docker/daemon.json 中写入如下内容(如果文件不存在请新建该文件)

{
    "registry-mirror": [
        "https://registry.docker-cn.com"
    ],
    
    "insecure-registries": [
        "192.168.199.100:5000"
    ]
}

Docker Compose 项目

Compose 项目是 Docker 官方的开源项目,负责实现对 Docker 容器集群的快速编排。从功能上看,跟OpenStack 中的 Heat 十分类似。
Compose 定位是“定义和运行多个 Docker 容器的应用(Defining and running multi-container Docker applications)”,其前身是开源项目 Fig。
通过之前的介绍,我们知道使用一个Dockerfile模板文件,可以让用户很方便的定义一个单独的应用容器。然而,在日常工作中,经常会碰到需要多个容器相互配合来完成某项任务的情况。例如要实现一个 Web 项目,除了 Web 服务容器本身,往往还需要再加上后端的数据库服务容器,甚至还包括负载均衡容器等。
Compose 恰好满足了这样的需求。它允许用户通过一个单独的 docker-compose.yml 模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)。Compose 中有两个重要的概念:

  • 服务 ( service ):一个应用的容器,实际上可以包括若干运行相同镜像的容器实例。
  • 项目 ( project ):由一组关联的应用容器组成的一个完整业务单元,在 docker-compose.yml 文件中定义。

Compose 的默认管理对象是项目,通过子命令对项目中的一组容器进行便捷地生命周期管理。
Compose 项目由 Python 编写,实现上调用了 Docker 服务提供的 API 来对容器进行管理。因此,只要所操作的平台支持 Docker API,就可以在其上利用 Compose 来进行编排管理。

安装

Compose 支持 Linux、macOS、Windows 10 三大平台。
Compose 可以通过 Python 的包管理工具 pip 进行安装,也可以直接下载编译好的二进制文件使用,甚至能够直接在 Docker 容器中运行。前两种方式是传统方式,适合本地环境下安装使用;最后一种方式则不破坏系统环境,更适合云计算场景。
这里我们只介绍二进制安装方式,PIP安装的方式比较麻烦。
Docker for Mac 、 Docker for Windows 自带 docker-compose 二进制文件,安装 Docker 之后可以直接使用。
在 Linux 上的也安装十分简单,从 官方 GitHub Release 处直接下载编译好的二进制文件即可。例如,在 Linux 64 位系统上直接下载对应的二进制包

# 先把docker-compose文件dump到当前目录
wget https://github.com/docker/compose/releases/download/1.27.4/dockercompose-Linux-x86_64
# 然后拷贝到/usr/bin/
sudo cp -arf docker-compose-Linux-x86_64 /usr/bin/docker-compose
sudo chmod +x /usr/bin/docker-compose

安装完成后,查看版本号

docker-compose -version

卸载

如果是二进制包方式安装的,删除二进制文件即可。

sudo rm /usr/bin/docker-compose

Compose 命令

对于 Compose 来说,大部分命令的对象既可以是项目本身,也可以指定为项目中的服务或者容器。如果没有特别的说明,命令对象将是项目,这意味着项目中所有的服务都会受到命令影响。执行 docker-compose [COMMAND] --help 或者 docker-compose help [COMMAND] 可以查看具体某个命令的使用格式。docker-compose 命令的基本的使用格式是

docker-compose [-f=<arg>...] [options] [COMMAND] [ARGS...]

命令选项

  • -f, --file FILE 指定使用的 Compose 模板文件,默认为 docker-compose.yml ,可以多次指定。
  • -p, --project-name NAME 指定项目名称,默认将使用所在目录名称作为项目名。- --x-networking 使用 Docker 的可拔插网络后端特性
  • –x-network-driver DRIVER 指定网络后端的驱动,默认为 bridge- --verbose 输出更多调试信息。
  • -v, --version 打印版本并退出。

单纯介绍命令太枯燥,这里给一个实例结合理解。

使用

准备工作

创建一个测试目录

mkdir composetest
cd composetest

在测试目录中创建一个名为 app.py 的文件,并复制粘贴以下内容:

import time
import redis
from flask import Flask
app = Flask(__name__)
cache = redis.Redis(host='redis', port=6379)
def get_hit_count():
    retries = 5
    while True:
        try:
            return cache.incr('hits')
        except redis.exceptions.ConnectionError as exc:
        if retries == 0:
            raise exc
        retries -= 1
        time.sleep(0.5)
@app.route('/')
def hello():
    count = get_hit_count()
    return 'Hello World! I have been seen {} times.\n'.format(count)

其中,redis 是应用程序网络上的 redis 容器的主机名,该主机使用的端口为 6379。在 composetest 目录中创建另一个名为 requirements.txt 的文件,表示依赖的镜像,内容如下:

flask
redis

创建 Dockerfile 文件

在 composetest 目录中,创建一个名为Dockerfile的文件 ,内容如下

# 从 Python 3.7 映像开始构建镜像
FROM python:3.7-alpine
# 将工作目录设置为 /code
WORKDIR /code
# 设置 flask 命令使用的环境变量。
ENV FLASK_APP app.py
ENV FLASK_RUN_HOST 0.0.0.0
# 安装 gcc,以便诸如 MarkupSafe 和SQLAlchemy 之类的 Python 包可以编译加速。这里先注释掉,下载太费时间了。
# RUN apk add --no-cache gcc musl-dev linux-headers

# 复制 requirements.txt 并安装 Python 依赖项
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt

# 将 . 项目中的当前目录复制到 . 镜像中的工作目录。
COPY . .
# 容器提供默认的执行命令为:flask run。
CMD ["flask", "run"]

创建 docker-compose.yml

在composetest目录中创建一个名为 docker-compose.yml 的文件,其内容如下

# yaml 配置
version: '3'
services:
    web:
        build: .
    ports:
        - "5000:5000"
    redis:
        image: "redis:alpine"

该 Compose 文件定义了两个服务:web 和 redis。

  • web:该 web 服务使用从 Dockerfile 当前目录中构建的镜像。然后,它将容器和主机绑定到暴露的端口 5000。此示例服务使用 Flask Web 服务器的默认端口 5000 。
  • redis:该 redis 服务使用 Docker Hub 的公共 Redis 映像。

使用 Compose 命令构建和运行应用

docker-compose up

如果你想在后台执行该服务可以加上 -d 参数

docker-compose up -d

接下来就可以用浏览器等工具,访问该服务。
还可以查看目前运行的镜像

docker container ls -a
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐