说起来Docker,大家都或多或少有耳闻,但是我的公司因为业务场景,长尾效应,至今为止Docker只在小部分项目轻度使用。但是这不妨碍我们主动学习,应用Docker,因此,从2018年开始,我就推动Docker在测试部应用。我们的测试平台,GitLab、JIRA、Confluence等都是通过Docker部署的。
团队内部做算法测试的同学搭建开发测试环境需要安装各种第三方库,因为工作环境是内网,无法连接互联网进行安装,痛苦不堪,使用Docker将相关环境在外网构建好传入内网使用是比较方便的方式;还有工具链建设、测试平台建设用到的各种基础设施MySQL、Nginx等快速部署,使用Docker都是最佳方案。为了方便团队同学了解Docker,团队老大让我来进行一次内部Docker入门分享。
没有Docker的从前
为了提高服务器利用效率,我们将一台高性能的服务器通过vSphere虚拟化,创建多台虚拟机,提供相对独立环境隔离的操作系统环境和计算资源,我们可以对每一台虚拟机灵活分配计算资源、存储资源等,还可以对每一台虚拟机创建多个快照,看起来很棒!是的,在传统的软件开发模式下,这种方式挺好,但是这并没有解决软件的部署效率低,交付困难,开发环境与测试环境不一致,测试环境与生产环境不一致等问题,这也是为什么我们有了持续集成,却没有做好持续交付的一大根本问题。
有了Docker的现在
各大互联网厂商都在使用Docker,通过Docker结合微服务等技术他们将DevOps这一理念落地了,真正做到了持续集成、持续部署、持续交付,快速迭代,可以实现一天多次版本上线发布!回头看有了Docker我们在日常工作中能做什么,举个最简单的例子:
以往我们想部署三套环境(一套Dev环境、一套Test环境、一套Release环境),需要三个MySQL数据库,需要怎么做呢?
- 方案1:准备三台服务器,分别安装MySQL,干干净净,互不干扰,但是浪费资源啊;
- 方案2:一台服务器部署三个MySQL实例,分别配置不同的MySQL数据存储路径、配置文件路径、端口,操作复杂,维护困难,想想都头疼;
用Docker以后呢,我们可以这样做:
1 | docker run --name release-mysql -p 3306:3306 -d mysql:5.7 |
2 | docker run --name dev-mysql -p 3307:3306 -d mysql:5.7 |
3 | docker run --name test-mysql -p 3308:3306 -d mysql:5.7 |
这样我们就有了三个独立的MySQL数据库服务!当然这是个示例,没有做数据卷映射等配置,但那些配置相比于传统方案来说,复杂度不值一提。
Docker到底是什么
Docker 是世界领先的软件容器平台。开发人员利用 Docker 可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用 Docker 可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用 Docker 可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为 Linux 和 Windows Server 应用发布新功能。
什么是容器呢?
将软件打包成标准化单元,以用于开发、交付和部署
容器镜像是轻量的、可执行的独立软件包,包含软件运行所需的所有内容:代码、运行时环境、系统工具、系统库和设置。容器化软件适用于基于 Linux 和 Windows 的应用,在任何环境中都能够始终如一地运行。容器赋予了软件独立性,使其免受外在环境差异(例如,开发和预演环境的差异)的影响,从而有助于减少团队间在相同基础设施上运行不同软件时的冲突。
容器的优势特点
轻量
在一台机器上运行的多个 Docker 容器可以共享这台机器的操作系统内核;它们能够迅速启动,只需占用很少的计算和内存资源。镜像是通过文件系统层进行构造的,并共享一些公共文件。这样就能尽量降低磁盘用量,并能更快地下载镜像。
标准
Docker 容器基于开放式标准,能够在所有主流 Linux 版本、Microsoft Windows 以及包括 VM、裸机服务器和云在内的任何基础设施上运行。
安全
Docker 赋予应用的隔离性不仅限于彼此隔离,还独立于底层的基础设施。Docker 默认提供最强的隔离,因此应用出现问题,也只是单个容器的问题,而不会波及到整台机器。
容器 VS 虚拟机
容器和虚拟机具有相似的资源隔离和分配优势,但功能有所不同,因为容器虚拟化的是操作系统,而不是硬件,因此容器更容易移植,效率也更高。
容器
容器是一个应用层抽象,用于将代码和依赖资源打包在一起。多个容器可以在同一台机器上运行,共享操作系统内核,但各自作为独立的进程在用户空间中运行。与虚拟机相比,容器占用的空间较少(容器镜像大小通常只有几十兆),瞬间就能完成启动。
虚拟机
虚拟机 (VM) 是一个物理硬件层抽象,用于将一台服务器变成多台服务器。管理程序允许多个 VM 在一台机器上运行。每个 VM 都包含一整套操作系统、一个或多个应用、必要的二进制文件和库资源,因此占用大量空间。而且 VM 启动也十分缓慢。
对比传统虚拟机总结
特性 | 容器 | 虚拟机 |
---|---|---|
启动 | 秒级 | 分钟级 |
硬盘使用 | 一般为 MB |
一般为 GB |
性能 | 接近原生 | 弱于 |
系统支持量 | 单机支持上千个容器 | 一般几十个 |
容器和虚拟机共用
将容器和虚拟机配合使用,为应用的部署和管理提供极大的灵活性。
如何使用Docker
安装Docker
Docker可以安装在Linux、Mac、Window上面,但是需要注意的是,Unix安装Docker(Linux、Mac)上对操作系统内核版本有要求(RedHat要7UX以上版本),Windows安装Docker推荐使用Win10操作系统(Win7也不是不能装,太折腾,人生苦短)。
具体安装方式Google搜一下就很多,不罗嗦了。
年轻人的第一个Docker命令
我们看下这条docker run hello-world
命令执行后发生了什么:
- 本地没有找到
hello-world:latest
这个镜像
,通过pull
命令从docker的仓库
一个最新的镜像
- 下载完毕后启动了
hello-world
的容器
,输出了一段介绍Docker的信息
基本概念
Docker包括三个基本概念
- 镜像(
Image
) - 容器(
Container
) - 仓库(
Repository
)
理解了这三个概念,就理解了 Docker 的整个生命周期。
Docker镜像
我们都知道,操作系统分为内核和用户空间。对于 Linux 而言,内核启动后,会挂载 root
文件系统为其提供用户空间支持。而 Docker 镜像(Image),就相当于是一个 root
文件系统。比如官方镜像 ubuntu:18.04
就包含了完整的一套 Ubuntu 18.04 最小系统的 root
文件系统。
Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。
Docker 容器
镜像(Image
)和容器(Container
)的关系,就像是面向对象程序设计中的 类
和 实例
一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的 命名空间。因此容器可以拥有自己的 root
文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。也因为这种隔离的特性,很多人初学 Docker 时常常会混淆容器和虚拟机。
前面讲过镜像使用的是分层存储,容器也是如此。每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为容器存储层。
容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。
按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用 数据卷(Volume)、或者绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。
数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此,使用数据卷后,容器删除或者重新运行之后,数据却不会丢失。
Docker仓库
镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。
一个 Docker Registry 中可以包含多个仓库(Repository
);每个仓库可以包含多个标签(Tag
);每个标签对应一个镜像。
通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签>
的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest
作为默认标签。
以 Ubuntu 镜像 为例,ubuntu
是仓库的名字,其内包含有不同的版本标签,如,16.04
, 18.04
。我们可以通过 ubuntu:14.04
,或者 ubuntu:18.04
来具体指定所需哪个版本的镜像。如果忽略了标签,比如 ubuntu
,那将视为 ubuntu:latest
。
仓库名经常以 两段式路径 形式出现,比如 jwilder/nginx-proxy
,前者往往意味着 Docker Registry 多用户环境下的用户名,后者则往往是对应的软件名。但这并非绝对,取决于所使用的具体 Docker Registry 的软件或服务。
Docker Registry 公开服务
Docker Registry 公开服务是开放给用户使用、允许用户管理镜像的 Registry 服务。一般这类公开服务允许用户免费上传、下载公开的镜像,并可能提供收费服务供用户管理私有镜像。
最常使用的 Registry 公开服务是官方的 Docker Hub,这也是默认的 Registry,并拥有大量的高质量的官方镜像。除此以外,还有 CoreOS 的 Quay.io,CoreOS 相关的镜像存储在这里;Google 的 Google Container Registry,Kubernetes 的镜像使用的就是这个服务。
由于某些原因,在国内访问这些服务可能会比较慢。国内的一些云服务商提供了针对 Docker Hub 的镜像服务(Registry Mirror
),这些镜像服务被称为加速器。常见的有 阿里云加速器、DaoCloud 加速器 等。使用加速器会直接从国内的地址下载 Docker Hub 的镜像,比直接从 Docker Hub 下载速度会提高很多。在 安装 Docker 一节中有详细的配置方法。
国内也有一些云服务商提供类似于 Docker Hub 的公开服务。比如 时速云镜像仓库、网易云镜像服务、DaoCloud 镜像市场、阿里云镜像库 等。
私有 Docker Registry
除了使用公开服务外,用户还可以在本地搭建私有 Docker Registry。Docker 官方提供了 Docker Registry镜像,可以直接使用做为私有 Registry 服务。在 私有仓库 一节中,会有进一步的搭建私有 Registry 服务的讲解。
开源的 Docker Registry 镜像只提供了 Docker Registry API 的服务端实现,足以支持 docker
命令,不影响使用。但不包含图形界面,以及镜像维护、用户管理、访问控制等高级功能。在官方的商业化版本 Docker Trusted Registry 中,提供了这些高级功能。
除了官方的 Docker Registry 外,还有第三方软件实现了 Docker Registry API,甚至提供了用户界面以及一些高级功能。比如,VMWare Harbor 和 Sonatype Nexus。
使用 Docker 镜像
在之前的介绍中,我们知道镜像是 Docker 的三大组件之一。
Docker 运行容器前需要本地存在对应的镜像,如果本地不存在该镜像,Docker 会从镜像仓库下载该镜像。
获取镜像
之前提到过,Docker Hub 上有大量的高质量的镜像可以用,这里我们就说一下怎么获取这些镜像。
从 Docker 镜像仓库获取镜像的命令是 docker pull
。其命令格式为:
1 | docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签] |
具体的选项可以通过 docker pull --help
命令看到,这里我们说一下镜像名称的格式。
- Docker 镜像仓库地址:地址的格式一般是
<域名/IP>[:端口号]
。默认地址是 Docker Hub。 - 仓库名:如之前所说,这里的仓库名是两段式名称,即
<用户名>/<软件名>
。对于 Docker Hub,如果不给出用户名,则默认为library
,也就是官方镜像。
比如:
1 | $ docker pull ubuntu:18.04 |
2 | 18.04: Pulling from library/ubuntu |
3 | bf5d46315322: Pull complete |
4 | 9f13e0ac480c: Pull complete |
5 | e8988b5b3097: Pull complete |
6 | 40af181810e7: Pull complete |
7 | e6f7c7e5c03e: Pull complete |
8 | Digest: sha256:147913621d9cdea08853f6ba9116c2e27a3ceffecf3b492983ae97c3d643fbbe |
9 | Status: Downloaded newer image for ubuntu:18.04 |
上面的命令中没有给出 Docker 镜像仓库地址,因此将会从 Docker Hub 获取镜像。而镜像名称是 ubuntu:18.04
,因此将会获取官方镜像 library/ubuntu
仓库中标签为 18.04
的镜像。
从下载过程中可以看到我们之前提及的分层存储的概念,镜像是由多层存储所构成。下载也是一层层的去下载,并非单一文件。下载过程中给出了每一层的 ID 的前 12 位。并且下载结束后,给出该镜像完整的 sha256
的摘要,以确保下载一致性。
在使用上面命令的时候,你可能会发现,你所看到的层 ID 以及 sha256
的摘要和这里的不一样。这是因为官方镜像是一直在维护的,有任何新的 bug,或者版本更新,都会进行修复再以原来的标签发布,这样可以确保任何使用这个标签的用户可以获得更安全、更稳定的镜像。
运行
有了镜像后,我们就能够以这个镜像为基础启动并运行一个容器。以上面的 ubuntu:18.04
为例,如果我们打算启动里面的 bash
并且进行交互式操作的话,可以执行下面的命令。
1 | $ docker run -it --rm \ |
2 | ubuntu:18.04 \ |
3 | bash |
4 | |
5 | root@e7009c6ce357:/# cat /etc/os-release |
6 | NAME="Ubuntu" |
7 | VERSION="18.04.1 LTS (Bionic Beaver)" |
8 | ID=ubuntu |
9 | ID_LIKE=debian |
10 | PRETTY_NAME="Ubuntu 18.04.1 LTS" |
11 | VERSION_ID="18.04" |
12 | HOME_URL="https://www.ubuntu.com/" |
13 | SUPPORT_URL="https://help.ubuntu.com/" |
14 | BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" |
15 | PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" |
16 | VERSION_CODENAME=bionic |
17 | UBUNTU_CODENAME=bionic |
docker run
就是运行容器的命令,具体格式我们会在 容器 一节进行详细讲解,我们这里简要的说明一下上面用到的参数。
-it
:这是两个参数,一个是-i
:交互式操作,一个是-t
终端。我们这里打算进入bash
执行一些命令并查看返回结果,因此我们需要交互式终端。--rm
:这个参数是说容器退出后随之将其删除。默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动docker rm
。我们这里只是随便执行个命令,看看结果,不需要排障和保留结果,因此使用--rm
可以避免浪费空间。ubuntu:18.04
:这是指用ubuntu:18.04
镜像为基础来启动容器。bash
:放在镜像名后的是命令,这里我们希望有个交互式 Shell,因此用的是bash
。
进入容器后,我们可以在 Shell 下操作,执行任何所需的命令。这里,我们执行了 cat /etc/os-release
,这是 Linux 常用的查看当前系统版本的命令,从返回的结果可以看到容器内是 Ubuntu 18.04.1 LTS
系统。
最后我们通过 exit
退出了这个容器。
列出镜像
要想列出已经下载下来的镜像,可以使用 docker images
命令。
1 | root@evi1-book:/home/evi1# docker images |
2 | REPOSITORY TAG IMAGE ID CREATED SIZE |
3 | evi1/pypiserver 1.2.5 be7e6669fce0 37 hours ago 88.5MB |
4 | hello-world latest fce289e99eb9 4 days ago 1.84kB |
5 | <none> <none> 00285df0df87 5 days ago 342 MB |
6 | nginx latest 02256cfb0e4b 9 days ago 109MB |
7 | alpine 3.7 9bea9e12e381 2 weeks ago 4.21MB |
8 | ubuntu 18.04 f753707788c5 4 weeks ago 127 MB |
9 | ubuntu latest f753707788c5 4 weeks ago 127 MB |
10 | mysql 5.7 ae6b78bedf88 7 weeks ago 372MB |
11 | pypiserver/pypiserver v1.2.5 b26b8ee831d7 7 weeks ago 86MB |
12 | codekoala/pypi latest f6ab585f84ed 7 months ago 61.3MB |
列表包含了 仓库名
、标签
、镜像 ID
、创建时间
以及 所占用的空间
。
其中仓库名、标签在之前的基础概念章节已经介绍过了。镜像 ID 则是镜像的唯一标识,一个镜像可以对应多个标签。因此,在上面的例子中,我们可以看到 ubuntu:18.04
和 ubuntu:latest
拥有相同的 ID,因为它们对应的是同一个镜像。
镜像体积
如果仔细观察,会注意到,这里标识的所占用空间和在 Docker Hub 上看到的镜像大小不同。比如,ubuntu:18.04
镜像大小,在这里是 127 MB
,但是在 Docker Hub 显示的却是 50 MB
。这是因为 Docker Hub 中显示的体积是压缩后的体积。在镜像下载和上传过程中镜像是保持着压缩状态的,因此 Docker Hub 所显示的大小是网络传输中更关心的流量大小。而 docker image ls
显示的是镜像下载到本地后,展开的大小,准确说,是展开后的各层所占空间的总和,因为镜像到本地后,查看空间的时候,更关心的是本地磁盘空间占用的大小。
另外一个需要注意的问题是,docker image ls
列表中的镜像体积总和并非是所有镜像实际硬盘消耗。由于 Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于 Docker 使用 Union FS,相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。
你可以通过以下命令来便捷的查看镜像、容器、数据卷所占用的空间。
1 | root@evi1-book:/home/evi1# docker system df |
2 | TYPE TOTAL ACTIVE SIZE RECLAIMABLE |
3 | Images 13 2 1.931GB 1.614GB (83%) |
4 | Containers 2 1 2B 0B (0%) |
5 | Local Volumes 14 0 0B 0B |
6 | Build Cache 0 0 0B 0B |
虚悬镜像
上面的镜像列表中,还可以看到一个特殊的镜像,这个镜像既没有仓库名,也没有标签,均为 <none>
。:
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 ls -f dangling=true |
2 | REPOSITORY TAG IMAGE ID CREATED SIZE |
3 | <none> <none> 00285df0df87 5 days ago 342 MB |
一般来说,虚悬镜像已经失去了存在的价值,是可以随意删除的,可以用下面的命令删除。
1 | $ docker image prune |
删除本地镜像
如果要删除本地的镜像,可以使用 docker image rm
命令,其格式为:
1 | $ docker image rm [选项] <镜像1> [<镜像2> ...] |
用 ID、镜像名、摘要删除镜像
其中,<镜像>
可以是 镜像短 ID
、镜像长 ID
、镜像名
或者 镜像摘要
。
比如我们有这么一些镜像:
1 | $ docker image ls |
2 | REPOSITORY TAG IMAGE ID CREATED SIZE |
3 | centos latest 0584b3d2cf6d 3 weeks ago 196.5 MB |
4 | redis alpine 501ad78535f0 3 weeks ago 21.03 MB |
5 | docker latest cf693ec9b5c7 3 weeks ago 105.1 MB |
6 | nginx latest e43d811ce2f4 5 weeks ago 181.5 MB |
我们可以用镜像的完整 ID,也称为 长 ID
,来删除镜像。使用脚本的时候可能会用长 ID,但是人工输入就太累了,所以更多的时候是用 短 ID
来删除镜像。docker image ls
默认列出的就已经是短 ID 了,一般取前3个字符以上,只要足够区分于别的镜像就可以了。
比如这里,如果我们要删除 redis:alpine
镜像,可以执行:
1 | $ docker image rm 501 |
2 | Untagged: redis:alpine |
3 | Untagged: redis@sha256:f1ed3708f538b537eb9c2a7dd50dc90a706f7debd7e1196c9264edeea521a86d |
4 | Deleted: sha256:501ad78535f015d88872e13fa87a828425117e3d28075d0c117932b05bf189b7 |
5 | Deleted: sha256:96167737e29ca8e9d74982ef2a0dda76ed7b430da55e321c071f0dbff8c2899b |
6 | Deleted: sha256:32770d1dcf835f192cafd6b9263b7b597a1778a403a109e2cc2ee866f74adf23 |
7 | Deleted: sha256:127227698ad74a5846ff5153475e03439d96d4b1c7f2a449c7a826ef74a2d2fa |
8 | Deleted: sha256:1333ecc582459bac54e1437335c0816bc17634e131ea0cc48daa27d32c75eab3 |
9 | Deleted: sha256:4fc455b921edf9c4aea207c51ab39b10b06540c8b4825ba57b3feed1668fa7c7 |
我们也可以用镜像名
,也就是 <仓库名>:<标签>
,来删除镜像。
1 | $ docker image rm centos |
2 | Untagged: centos:latest |
3 | Untagged: centos@sha256:b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c |
4 | Deleted: sha256:0584b3d2cf6d235ee310cf14b54667d889887b838d3f3d3033acd70fc3c48b8a |
5 | Deleted: sha256:97ca462ad9eeae25941546209454496e1d66749d53dfa2ee32bf1faabd239d38 |
当然,更精确的是使用 镜像摘要
删除镜像。
1 | $ docker image ls --digests |
2 | REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE |
3 | node slim sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228 6e0c4c8e3913 3 weeks ago 214 MB |
4 | |
5 | $ docker image rm node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228 |
6 | Untagged: node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228 |
导出和导入镜像
根据我们公司的工作环境,互联网隔绝,必然要从外网下载或构建好镜像传入内网使用
下面我们就以evi1/pypiserver:1.2.5
这个镜像为例演示下
首先我们在外网查看下镜像信息
1 | root@evi1-book:/tmp/docker_images# docker images |
2 | REPOSITORY TAG IMAGE ID CREATED SIZE |
3 | evi1/pypiserver 1.2.5 be7e6669fce0 38 hours ago 88.5MB |
导出镜像
通过docker save
命令来执行导出镜像
1 | root@evi1-book:/tmp/docker_images# docker save --help |
2 | |
3 | Usage: docker save [OPTIONS] IMAGE [IMAGE...] |
4 | |
5 | Save one or more images to a tar archive (streamed to STDOUT by default) |
6 | |
7 | Options: |
8 | -o, --output string Write to a file, instead of STDOUT |
针对我们这里的镜像,命令应如下书写
1 | root@evi1-book:/tmp/docker_images# docker save -o evi1-pypiserver_1.2.5.tar evi1/pypiserver:1.2.5 |
此处注意几点:
save -o 后面的参数是我们要保存的文件, 我们约定使用
.tar
结尾,此时导出的文件是为压缩的文件tar文件我们按照
{用户名}-{镜像名}_{标签}.tar
格式来命名,这样方便管理,统一格式镜像参数我们使用镜像名称+标签的方式,这样有一个好处,导入后的镜像自带名称和标签,无需手动命名,否则导入的镜像会显示为如下样子
1
REPOSITORY TAG IMAGE ID CREATED SIZE
2
<none> <none> be7e6669fce0 38 hours ago 88.5MB
至此我们就可以将镜像传入内网了,但是这个文件导出后大小为88.5MB
,我们还可以压缩下,加快传输速度
1 | root@evi1-book:/tmp/docker_images# tar czvf evi1-pypiserver_1.2.5.tar.gz evi1-pypiserver_1.2.5.tar |
2 | evi1-pypiserver_1.2.5.tar |
3 | root@evi1-book:/tmp/docker_images# du -sh * |
4 | 89M evi1-pypiserver_1.2.5.tar |
5 | 33M evi1-pypiserver_1.2.5.tar.gz |
看到文件压缩后大小仅为原始镜像的1/3,所以大家从外网拉取镜像时要保持压缩的好习惯
导入镜像
镜像压缩包被传入内网后,我们先将压缩包上传至Docker所在的服务器任意目录下(通常我们会建立一个目录专门存放各种镜像压缩包,也便于我们后续建立私有镜像仓库,统一管理)
第一步先解压镜像压缩包
1 | root@evi1-book:/tmp/docker_images# tar zxvf pypiserver_1.2.5.tar.gz |
解压后我们得到一个evi1-pypiserver_1.2.5.tar
文件
接下里开始真正的导入镜像操作docker load
1 | root@evi1-book:/tmp/docker_images# docker load --help |
2 | |
3 | Usage: docker load [OPTIONS] |
4 | |
5 | Load an image from a tar archive or STDIN |
6 | |
7 | Options: |
8 | -i, --input string Read from tar archive file, instead of STDIN |
9 | -q, --quiet Suppress the load output |
针对我们这里的镜像,命令应如下书写
1 | root@evi1-book:/tmp/docker_images# docker load < evi1-pypiserver_1.2.5.tar |
然后我们通过docker image ls
就能看到导入的镜像了
1 | root@xxxx:/home/root# docker image ls evi1/pypiserver |
2 | REPOSITORY TAG IMAGE ID CREATED SIZE |
3 | evi1/pypiserver 1.2.5 be7e6669fce0 38 hours ago 88.5MB |
操作 Docker 容器
容器是 Docker 又一核心概念。
简单的说,容器是独立运行的一个或一组应用,以及它们的运行态环境。对应的,虚拟机可以理解为模拟运行的一整套操作系统(提供了运行态环境和其他系统环境)和跑在上面的应用。
启动容器
启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(stopped
)的容器重新启动。
因为 Docker 的容器实在太轻量级了,很多时候用户都是随时删除和新创建容器。
新建并启动
所需要的命令主要为 docker run
。
例如,下面的命令输出一个 “Hello World”,之后终止容器。
1 | $ docker run ubuntu:18.04 /bin/echo 'Hello world' |
2 | Hello world |
这跟在本地直接执行 /bin/echo 'hello world'
几乎感觉不出任何区别。
下面的命令则启动一个 bash 终端,允许用户进行交互。
1 | $ docker run -t -i ubuntu:18.04 /bin/bash |
2 | root@af8bae53bdd3:/# |
其中,-t
选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上, -i
则让容器的标准输入保持打开。
在交互模式下,用户可以通过所创建的终端来输入命令,例如
1 | root@af8bae53bdd3:/# pwd |
2 | / |
3 | root@af8bae53bdd3:/# ls |
4 | bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var |
当利用 docker run
来创建容器时,Docker 在后台运行的标准操作包括:
- 检查本地是否存在指定的镜像,不存在就从公有仓库下载
- 利用镜像创建并启动一个容器
- 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
- 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
- 从地址池配置一个 ip 地址给容器
- 执行用户指定的应用程序
- 执行完毕后容器被终止
启动已终止容器
可以利用 docker container start
命令,直接将一个已经终止的容器启动运行。
容器的核心为所执行的应用程序,所需要的资源都是应用程序运行所必需的。除此之外,并没有其它的资源。可以在伪终端中利用 ps
或 top
来查看进程信息。
1 | root@ba267838cc1b:/# ps |
2 | PID TTY TIME CMD |
3 | 1 ? 00:00:00 bash |
4 | 11 ? 00:00:00 ps |
可见,容器中仅运行了指定的 bash 应用。这种特点使得 Docker 对资源的利用率极高,是货真价实的轻量级虚拟化。
后台运行
更多的时候,需要让 Docker 在后台运行而不是直接把执行命令的结果输出在当前宿主机下。此时,可以通过添加 -d
参数来实现。
下面举两个例子来说明一下。
如果不使用 -d
参数运行容器。
1 | $ docker run ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done" |
2 | hello world |
3 | hello world |
4 | hello world |
5 | hello world |
容器会把输出的结果 (STDOUT) 打印到宿主机上面
如果使用了 -d
参数运行容器。
1 | $ docker run -d ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done" |
2 | 77b2dc01fe0f3f1265df143181e7b9af5e05279a884f4776ee75350ea9d8017a |
此时容器会在后台运行并不会把输出的结果 (STDOUT) 打印到宿主机上面(输出结果可以用 docker logs
查看)。
注: 容器是否会长久运行,是和 docker run
指定的命令有关,和 -d
参数无关。
使用 -d
参数启动后会返回一个唯一的 id,也可以通过 docker container ls
命令来查看容器信息。
1 | $ docker container ls |
2 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES |
3 | 77b2dc01fe0f ubuntu:18.04 /bin/sh -c 'while tr 2 minutes ago Up 1 minute agitated_wright |
要获取容器的输出信息,可以通过 docker container logs
命令。
1 | $ docker container logs [container ID or NAMES] |
2 | hello world |
3 | hello world |
4 | hello world |
5 | . . . |
终止容器
可以使用 docker container stop
来终止一个运行中的容器。
此外,当 Docker 容器中指定的应用终结时,容器也自动终止。
例如对于上一章节中只启动了一个终端的容器,用户通过 exit
命令或 Ctrl+d
来退出终端时,所创建的容器立刻终止。
终止状态的容器可以用 docker container ls -a
命令看到。例如
1 | docker container ls -a |
2 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES |
3 | ba267838cc1b ubuntu:18.04 "/bin/bash" 30 minutes ago Exited (0) About a minute ago trusting_newton |
4 | 98e5efa7d997 training/webapp:latest "python app.py" About an hour ago Exited (0) 34 minutes ago backstabbing_pike |
处于终止状态的容器,可以通过 docker container start
命令来重新启动。
此外,docker container restart
命令会将一个运行态的容器终止,然后再重新启动它。
进入容器
在使用 -d
参数时,容器启动后会进入后台。
某些时候需要进入容器进行操作,包括使用 docker attach
命令或 docker exec
命令,推荐大家使用 docker exec
命令,原因会在下面说明。
attach
命令
docker attach
是 Docker 自带的命令。下面示例如何使用该命令。
1 | $ docker run -dit ubuntu |
2 | 243c32535da7d142fb0e6df616a3c3ada0b8ab417937c853a9e1c251f499f550 |
3 | |
4 | $ docker container ls |
5 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES |
6 | 243c32535da7 ubuntu:latest "/bin/bash" 18 seconds ago Up 17 seconds nostalgic_hypatia |
7 | |
8 | $ docker attach 243c |
9 | root@243c32535da7:/# |
注意: 如果从这个 stdin 中 exit,会导致容器的停止。
exec
命令
-i -t 参数
docker exec
后边可以跟多个参数,这里主要说明 -i
-t
参数。
只用 -i
参数时,由于没有分配伪终端,界面没有我们熟悉的 Linux 命令提示符,但命令执行结果仍然可以返回。
当 -i
-t
参数一起使用时,则可以看到我们熟悉的 Linux 命令提示符。
1 | $ docker run -dit ubuntu |
2 | 69d137adef7a8a689cbcb059e94da5489d3cddd240ff675c640c8d96e84fe1f6 |
3 | |
4 | $ docker container ls |
5 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES |
6 | 69d137adef7a ubuntu:latest "/bin/bash" 18 seconds ago Up 17 seconds zealous_swirles |
7 | |
8 | $ docker exec -i 69d1 bash |
9 | ls |
10 | bin |
11 | boot |
12 | dev |
13 | ... |
14 | |
15 | $ docker exec -it 69d1 bash |
16 | root@69d137adef7a:/# |
如果从这个 stdin 中 exit,不会导致容器的停止。这就是为什么推荐大家使用 docker exec
的原因。
更多参数说明请使用 docker exec --help
查看。
导出和导入容器
导出容器
如果要导出本地某个容器,可以使用 docker export
命令。
1 | $ docker container ls -a |
2 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES |
3 | 7691a814370e ubuntu:18.04 "/bin/bash" 36 hours ago Exited (0) 21 hours ago test |
4 | $ docker export 7691a814370e > ubuntu.tar |
这样将导出容器快照到本地文件。
导入容器快照
可以使用 docker import
从容器快照文件中再导入为镜像,例如
1 | $ cat ubuntu.tar | docker import - test/ubuntu:v1.0 |
2 | $ docker image ls |
3 | REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE |
4 | test/ubuntu v1.0 9d37a6082e97 About a minute ago 171.3 MB |
此外,也可以通过指定 URL 或者某个目录来导入,例如
1 | $ docker import http://example.com/exampleimage.tgz example/imagerepo |
注:用户既可以使用 docker load 来导入镜像存储文件到本地镜像库,也可以使用 docker import 来导入一个容器快照到本地镜像库。这两者的区别在于容器快照文件将丢弃所有的历史记录和元数据信息(即仅保存容器当时的快照状态),而镜像存储文件将保存完整记录,体积也要大。此外,从容器快照文件导入时可以重新指定标签等元数据信息。
删除容器
可以使用 docker container rm
来删除一个处于终止状态的容器。例如
1 | $ docker container rm trusting_newton |
2 | trusting_newton |
如果要删除一个运行中的容器,可以添加 -f
参数。Docker 会发送 SIGKILL
信号给容器。
清理所有处于终止状态的容器
用 docker container ls -a
命令可以查看所有已经创建的包括终止状态的容器,如果数量太多要一个个删除可能会很麻烦,用下面的命令可以清理掉所有处于终止状态的容器。
1 | $ docker container prune |
访问仓库
仓库(Repository
)是集中存放镜像的地方。
Docker Hub
目前 Docker 官方维护了一个公共仓库 Docker Hub,其中已经包括了数量超过 15,000 的镜像。大部分需求都可以通过在 Docker Hub 中直接下载镜像来实现。
注册
你可以在 https://hub.docker.com 免费注册一个 Docker 账号。
登录
可以通过执行 docker login
命令交互式的输入用户名及密码来完成在命令行界面登录 Docker Hub。
你可以通过 docker logout
退出登录。
拉取镜像
你可以通过 docker search
命令来查找官方仓库中的镜像,并利用 docker pull
命令来将它下载到本地。
例如以 centos
为关键词进行搜索:
1 | $ docker search centos |
2 | NAME DESCRIPTION STARS OFFICIAL AUTOMATED |
3 | centos The official build of CentOS. 465 [OK] |
4 | tianon/centos CentOS 5 and 6, created using rinse instea... 28 |
5 | blalor/centos Bare-bones base CentOS 6.5 image 6 [OK] |
6 | saltstack/centos-6-minimal 6 [OK] |
7 | tutum/centos-6.4 DEPRECATED. Use tutum/centos:6.4 instead. ... 5 [OK] |
可以看到返回了很多包含关键字的镜像,其中包括镜像名字、描述、收藏数(表示该镜像的受关注程度)、是否官方创建、是否自动创建。
官方的镜像说明是官方项目组创建和维护的,automated 资源允许用户验证镜像的来源和内容。
根据是否是官方提供,可将镜像资源分为两类。
一种是类似 centos
这样的镜像,被称为基础镜像或根镜像。这些基础镜像由 Docker 公司创建、验证、支持、提供。这样的镜像往往使用单个单词作为名字。
还有一种类型,比如 tianon/centos
镜像,它是由 Docker 的用户创建并维护的,往往带有用户名称前缀。可以通过前缀 username/
来指定使用某个用户提供的镜像,比如 tianon 用户。
另外,在查找的时候通过 --filter=stars=N
参数可以指定仅显示收藏数量为 N
以上的镜像。
下载官方 centos
镜像到本地。
1 | $ docker pull centos |
2 | Pulling repository centos |
3 | 0b443ba03958: Download complete |
4 | 539c0211cd76: Download complete |
5 | 511136ea3c5a: Download complete |
6 | 7064731afe90: Download complete |
推送镜像
用户也可以在登录后通过 docker push
命令来将自己的镜像推送到 Docker Hub。
以下命令中的 username
请替换为你的 Docker 账号用户名。
1 | $ docker tag ubuntu:18.04 username/ubuntu:18.04 |
2 | |
3 | $ docker image ls |
4 | |
5 | REPOSITORY TAG IMAGE ID CREATED SIZE |
6 | ubuntu 18.04 275d79972a86 6 days ago 94.6MB |
7 | username/ubuntu 18.04 275d79972a86 6 days ago 94.6MB |
8 | |
9 | $ docker push username/ubuntu:18.04 |
10 | |
11 | $ docker search username |
12 | |
13 | NAME DESCRIPTION STARS OFFICIAL AUTOMATED |
14 | username/ubuntu |
Docker 数据管理
在容器中管理数据主要有两种方式:
- 数据卷(Volumes)
- 挂载主机目录 (Bind mounts)
数据卷
数据卷
是一个可供一个或多个容器使用的特殊目录,它绕过 UFS,可以提供很多有用的特性:
数据卷
可以在容器之间共享和重用- 对
数据卷
的修改会立马生效 - 对
数据卷
的更新,不会影响镜像 数据卷
默认会一直存在,即使容器被删除
注意:
数据卷
的使用,类似于 Linux 下对目录或文件进行 mount,镜像中的被指定为挂载点的目录中的文件会隐藏掉,能显示看的是挂载的数据卷
。
创建一个数据卷
1 | $ docker volume create my-vol |
查看所有的 数据卷
1 | $ docker volume ls |
2 | |
3 | local my-vol |
在主机里使用以下命令可以查看指定 数据卷
的信息
1 | $ docker volume inspect my-vol |
2 | [ |
3 | { |
4 | "Driver": "local", |
5 | "Labels": {}, |
6 | "Mountpoint": "/var/lib/docker/volumes/my-vol/_data", |
7 | "Name": "my-vol", |
8 | "Options": {}, |
9 | "Scope": "local" |
10 | } |
11 | ] |
启动一个挂载数据卷的容器
在用 docker run
命令的时候,使用 --mount
标记来将 数据卷
挂载到容器里。在一次 docker run
中可以挂载多个 数据卷
。
下面创建一个名为 web
的容器,并加载一个 数据卷
到容器的 /webapp
目录。
1 | $ docker run -d -P \ |
2 | --name web \ |
3 | # -v my-vol:/wepapp \ |
4 | --mount source=my-vol,target=/webapp \ |
5 | training/webapp \ |
6 | python app.py |
查看数据卷的具体信息
在主机里使用以下命令可以查看 web
容器的信息
1 | $ docker inspect web |
数据卷
信息在 “Mounts” Key 下面
1 | "Mounts": [ |
2 | { |
3 | "Type": "volume", |
4 | "Name": "my-vol", |
5 | "Source": "/var/lib/docker/volumes/my-vol/_data", |
6 | "Destination": "/app", |
7 | "Driver": "local", |
8 | "Mode": "", |
9 | "RW": true, |
10 | "Propagation": "" |
11 | } |
12 | ], |
删除数据卷
1 | $ docker volume rm my-vol |
数据卷
是被设计用来持久化数据的,它的生命周期独立于容器,Docker 不会在容器被删除后自动删除 数据卷
,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的 数据卷
。如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用 docker rm -v
这个命令。
无主的数据卷可能会占据很多空间,要清理请使用以下命令
1 | $ docker volume prune |
挂载主机目录
挂载一个主机目录作为数据卷
使用 --mount
标记可以指定挂载一个本地主机的目录到容器中去。
1 | $ docker run -d -P \ |
2 | --name web \ |
3 | # -v /src/webapp:/opt/webapp \ |
4 | --mount type=bind,source=/src/webapp,target=/opt/webapp \ |
5 | training/webapp \ |
6 | python app.py |
上面的命令加载主机的 /src/webapp
目录到容器的 /opt/webapp
目录。这个功能在进行测试的时候十分方便,比如用户可以放置一些程序到本地目录中,来查看容器是否正常工作。本地目录的路径必须是绝对路径,以前使用 -v
参数时如果本地目录不存在 Docker 会自动为你创建一个文件夹,现在使用 --mount
参数时如果本地目录不存在,Docker 会报错。
Docker 挂载主机目录的默认权限是 读写
,用户也可以通过增加 readonly
指定为 只读
。
1 | $ docker run -d -P \ |
2 | --name web \ |
3 | # -v /src/webapp:/opt/webapp:ro \ |
4 | --mount type=bind,source=/src/webapp,target=/opt/webapp,readonly \ |
5 | training/webapp \ |
6 | python app.py |
加了 readonly
之后,就挂载为 只读
了。如果你在容器内 /opt/webapp
目录新建文件,会显示如下错误
1 | /opt/webapp # touch new.txt |
2 | touch: new.txt: Read-only file system |
查看数据卷的具体信息
在主机里使用以下命令可以查看 web
容器的信息
1 | $ docker inspect web |
挂载主机目录
的配置信息在 “Mounts” Key 下面
1 | "Mounts": [ |
2 | { |
3 | "Type": "bind", |
4 | "Source": "/src/webapp", |
5 | "Destination": "/opt/webapp", |
6 | "Mode": "", |
7 | "RW": true, |
8 | "Propagation": "rprivate" |
9 | } |
10 | ], |
挂载一个本地主机文件作为数据卷
--mount
标记也可以从主机挂载单个文件到容器中
1 | $ docker run --rm -it \ |
2 | # -v $HOME/.bash_history:/root/.bash_history \ |
3 | --mount type=bind,source=$HOME/.bash_history,target=/root/.bash_history \ |
4 | ubuntu:18.04 \ |
5 | bash |
6 | |
7 | root@2affd44b4667:/# history |
8 | 1 ls |
9 | 2 diskutil list |
这样就可以记录在容器输入过的命令了。
Reference