docker
容器生态系统
容器核心技术
容器核心技术是指能够让Contailer在host上运行起来的技术。
容器规范
容器不光是 Docker ,还有其他容器,比如CoreOS 的 rkt. 为了保证容器生态的健康发 展,保证不同容器之间能够兼容,包含 Docker 、Core08、Google 在内的若干公司共同成立了 一 个叫 Open Container lnitiative (OCI) 的组织,其目的是制定开放的容器规范。
目前 OCI 发布了两个规范: runtime spec
和 image format spec
.
有了这两个规范,不同组织和厂商开发的容器能够在不同的 runtime 上运行 .这样就保证了容器的可移植性和互操作性。
容器的runtime
Java 程序就好比是容器, 只叫则好比是 runtime ,JVM 为 Java 程序提供运行环境。同样的道理,容器只有在 runtime 中才能运行。
lxc
、runc
和 rkt
是目前主流的三种容器 runtime。
- lxc 是 Linux上老牌的容器 runtime. Docker 最初也是用 lxc 作为 runtime .
- runc 是 Docker 自己开发的容器 runtime ,符合OC1 规范,也是现在 Docker 的默认runtune .
- rkt 是 Core08 开发的容器 runtime ,符合OCI 规范,因而能够运行 Docker 的容器。
容器管理工具
光有 runtJme 还不够,用户得有工具来管理容器。容器管理工具对内与 runtlme 交互, 对外为用户提供 interface ,比如CLI 。
lxd
是 lxc 对应的管理工具 。- runc 的管理工具是
docker engine
。 docker engine 包含后台 deamon 和 cli 两个部分。我们 通常提到 Docker。 一般就是指的 docker engine。 - rkt 的管理工具是
rkt cli
.
容器定义工具
容器定义工具 容器定义工具允许用户定义容器的内容和属性,这样容器就能够被保存、共享和重建。
docker image
是 Docker 容器的模板. runtime 依据 docker image 创建容器。dockerfile
是包含若干命令的文本文件,可以通过这些命令创建出 docker image .ACI (App Container Image)
与 docker image 类似,只不过它是由 CoreOS 开发的出容器 的 image格式 。
Registry
容器是通过 image 创建的,需要有一个仓库来统 一存放 lmage ,这个仓库就叫做Registry 。
- 企业可以用 Docker Registry 构建私有的 Registry 。
- Docker Hub ( https://hub.docker.com) 是 Docker 为公众提供的托管 Registry,上面有很多现成的 image。为 Docker 用户提供了极大的便利 。
- Quay.io (https://quay.io/) 是另一个公共托管Registry,提供与 DockerHub 类似的服务。
容器OS
由于有容器 runtime ,几乎所有的linux、 MAC OS 和 Windows 都可以运行容器,但这并没有妨碍容器 OS 的问世。
容器 OS 是专门运行容器的操作系统。与常规OS 相比,容器OS 通常体积更小,启动更快。因为是为容器定制的 OS. 通常它们运行容器的效率会更高。
容器平台技术
容器核心技术使得容器能够在单个 host 上运行,==而容器平台技术能够让容器作为集群在分布式环境中运行== 。
容器编排引擎
基于容器的应用一般会采用微服务架构。在这种架构下,应用被划分为不同的组件,并以 服务的形式运行在各自的容器中,通过 API 对外提供服务。为了保证应用的高可用,每个组件 都可能会运行多个相同的容器。这些容器会组成集群,集群中的容器会根据业务需要被动态地 创建、迁移和销毁。
这样一个基于微服务架构的应用系统实际上是 一个动态的可伸缩的系统。 这对我们的部署环境提出了新的要求,我们需要有一种高效的方法来==管理容器集群==。而这,就是容器编排引擎要干的工作。
所谓编排 (orchestration ) 。通常包括容器管理、调度、集群定义和服务发现等。通过容器 编排引擎,容器被有机地组合成微服务应用,实现业务需求。
- docker swarm 是 Docker 开发的容器编排引擎 。
- kubernetes 是 Google 领导开发的开源容器编排引擎,同时支持 Docker 和 CoreOS容器 。
- mesos是一个通用的集群资源调度平台, mesos 与 marathon 一起提供容器编排引擎功能。
容器管理平台
容器管理平台是架构在容器编排引擎之上的 一个更为通用的平台。通常容器管理平台能够 支持多种编排引擎,抽象了编排引擎的底层实现细节,为用户提供更方便的功能 ,比如 application catalog 和一键应用部署等。
Rancher 和 ContainerShip 是容器管理平台的典型代表。
基于容器的PaaS
基于容器的 PaaS 为微服务应用开发人员和公司提供了开发、部署和管理应用的平台 ,使用户不必关心底层基础设施而专注于应用的开发。
容器支持技术
用于支持基于容器的基础设施相干技术。
容器网络
容器的出现使网络拓扑变得更加动态和复杂。用户需要专门的解决方案来管理容器与容器、容器与其他实体之间的连通性和隔离性。
- docker network 是 Docker 原生的网络解决方案。
- 除此之外,我们还可以采用第三方开源解决方案,例如 flanne1、weave 和 calico。不同方案的设计和实现方式不同,各有优势和特点, 应根据实际需要来选型。
服务发现
动态变化是微服务应用的一大特点。当负载增加时,集群会自动创建新的容器:负载减小,多余的容器会被销毁。容器也会根据 host 的资源使用情况在不同 host 中迁移,容器的IP和端口也会随之发生变化。
在这种动态的环镜下,必须要有一种机制让 client 能够知道如何访问容器提供的服务。这就是服务发现技术要完成的工作。
服务发现会保存容器集群中所有微服务最新的信息,比如 IP 和端口,并对外提供 API。提供服务查询功能。
etcd 、consu1 和 zookeeper 是服务发现的典型解决方案。
监控
监控对于基础架构非常重要,而容器的动态特征对监控提出更多挑战。针对容器环境,已经涌现出很多监控工具和方案
docker ps/top/stats是 Docker 原生的命令行监控工具。除了命令行, Docker 也提供了stats API ,用户可以通过HTIP 请求获取容器的状态信息。
sysdig、 cAdvisor/Heapster 和 WeaveScope 是其他开源的容器监控方案。
数据管理
容器经常会在不同的 host 之间迁移,如何保证持久化数据也能够动态迁移,是 Rex-Ray这类数据管理工具提供的能力
日志管理
日志为问题排查和事件管理提供了重要依据。日志工具有两类:
- docker logs 是 Docker 原生的日志工具。
- 而 logspout 对日志提供了路由功能,它可以收集不同容器的日志并转发给其他工具进行后处理 。
安全性
对于年轻的容器,安全性 一直是业界争论的焦点。 OpenSCAP 是一种容器安全工具。OpenSCAP 能够对容器镜像进行扫描,发现潜在的漏洞。
Docker 核心知识
what—什么是容器
Docker 最初是 dotCloud 公司创始人 Solomon Hykes 在法国期间发起的一个公司内部项目,它是基于 dotCloud 公司多年云服务技术的一次革新,并于 2013 年 3 月以 Apache 2.0 授权协议开源,主要项目代码在 GitHub 上进行维护。Docker 项目后来还加入了 Linux 基金会,并成立推动 开放容器联盟(OCI)。
Docker 自开源后受到广泛的关注和讨论,至今其 GitHub 项目已经超过 4 万 6 千个星标和一万多个 fork。甚至由于 Docker 项目的火爆,在 2013 年底,dotCloud 公司决定改名为 Docker。Docker 最初是在 Ubuntu 12.04 上开发实现的;Red Hat 则从 RHEL 6.5 开始对 Docker 进行支持;Google 也在其 PaaS 产品中广泛应用 Docker。
Docker 使用 Google 公司推出的 Go 语言 进行开发实现,基于 Linux 内核的 cgroup,namespace,以及 AUFS 类的 Union FS 等技术,对进程进行封装隔离,属于 操作系统层面的虚拟化技术。由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。最初实现是基于 LXC,从 0.7 版本以后开始去除 LXC,转而使用自行开发的 libcontainer,从 1.11 开始,则进一步演进为使用 runC 和 containerd。
Docker 在容器的基础上,进行了进一步的封装,从文件系统、网络互联到进程隔离等等,极大的简化了容器的创建和维护。使得 Docker 技术比虚拟机技术更为轻便、快捷。
容器与虚拟机
下面的图片比较了 Docker 和传统虚拟化方式的不同之处。传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。
why—为什么需要容器
容器解决的问题
作为一种新兴的虚拟化方式,Docker 跟传统的虚拟化方式相比具有众多的优势
更高效的利用系统资源
由于容器不需要进行硬件虚拟以及运行完整操作系统等额外开销,Docker 对系统资源的利用率更高。无论是应用执行速度、内存损耗或者文件存储速度,都要比传统虚拟机技术更高效。因此,相比虚拟机技术,一个相同配置的主机,往往可以运行更多数量的应用。
更快速的启动时间
传统的虚拟机技术启动应用服务往往需要数分钟,而 Docker 容器应用,由于直接运行于宿主内核,无需启动完整的操作系统,因此可以做到秒级、甚至毫秒级的启动时间。大大的节约了开发、测试、部署的时间。
一致的运行环境
开发过程中一个常见的问题是环境一致性问题。由于开发环境、测试环境、生产环境不一致,导致有些 bug 并未在开发过程中被发现。而 Docker 的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性,从而不会再出现 「这段代码在我机器上没问题啊」 这类问题。
持续交付和部署
对开发和运维(DevOps)人员来说,最希望的就是一次创建或配置,可以在任意地方正常运行。
使用 Docker 可以通过定制应用镜像来实现持续集成、持续交付、部署。开发人员可以通过 Dockerfile
来进行镜像构建,并结合 持续集成(Continuous Integration) 系统进行集成测试,而运维人员则可以直接在生产环境中快速部署该镜像,甚至结合 持续部署(Continuous Delivery/Deployment) 系统进行自动部署。
而且使用 Dockerfile
使镜像构建透明化,不仅仅开发团队可以理解应用运行环境,也方便运维团队理解应用运行所需条件,帮助更好的生产环境中部署该镜像
更轻松的迁移
由于 Docker 确保了执行环境的一致性,使得应用的迁移更加容易。Docker 可以在很多平台上运行,无论是物理机、虚拟机、公有云、私有云,甚至是笔记本,其运行结果是一致的。因此用户可以很轻易的将在一个平台上运行的应用,迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行的情况。
更轻松的维护和扩展
Docker 使用的分层存储以及镜像的技术,使得应用重复部分的复用更为容易,也使得应用的维护更新更加简单,基于基础镜像进一步扩展镜像也变得非常简单。此外,Docker 团队同各个开源项目团队一起维护了一大批高质量的 官方镜像,既可以直接在生产环境使用,又可以作为基础进一步定制,大大的降低了应用服务的镜像制作成本
How —容器时如何工作的
Docker engine
Docker 引擎是一个包含以下主要组件的客户端服务器应用程序。
- 一种服务器,它是一种称为守护进程并且长时间运行的程序。
- REST API用于指定程序可以用来与守护进程通信的接口,并指示它做什么。
- 一个有命令行界面 (CLI) 工具的客户端。
Docker 引擎组件的流程如下图所示:
Docker 系统架构
Docker 使用客户端-服务器 (C/S) 架构模式,使用远程 API 来管理和创建 Docker 容器。
Docker 容器通过 Docker 镜像来创建。
容器与镜像的关系类似于面向对象编程中的对象与类。
Docker | 面向对象 |
---|---|
容器 | 对象 |
镜像 | 类 |
标题 | 说明 |
---|---|
镜像(Images) | Docker 镜像是用于创建 Docker 容器的模板。 |
容器(Container) | 容器是独立运行的一个或一组应用。 |
客户端(Client) | Docker 客户端通过命令行或者其他工具使用 Docker API (https://docs.docker.com/reference/api/docker_remote_api) 与 Docker 的守护进程通信。 |
Docker daemon | 服务器组件,以Linux后台服务的方式运行,Docker daemon运行在Docker host上,负责创建、运行、监控容器,构建、存储镜像。 默认配置下Docker daemon只能响应本地Host的客户端请求。 |
主机(Host) | 一个物理或者虚拟的机器用于执行 Docker 守护进程和容器。 |
仓库(Registry) | Docker 仓库用来保存镜像,可以理解为代码控制中的代码仓库。Docker Hub(https://hub.docker.com) 提供了庞大的镜像集合供使用。 |
Docker Machine | Docker Machine是一个简化Docker安装的命令行工具,通过一个简单的命令行即可在相应的平台上安装Docker,比如VirtualBox、 Digital Ocean、Microsoft Azure。 |
Docker Image
分层存储
操作系统分为内核和用户空间。对于 Linux 而言,内核启动后,会挂载 root
文件系统为其提供用户空间支持。而 Docker 镜像(Image),就相当于是一个 root
文件系统。比如官方镜像 ubuntu:16.04
就包含了完整的一套 Ubuntu 16.04 最小系统的 root
文件系统。
Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变
因为镜像包含操作系统完整的 root
文件系统,其体积往往是庞大的,因此在 Docker 设计时,就充分利用 Union FS 的技术,将其设计为分层存储的架构。所以严格来说,镜像并非是像一个 ISO 那样的打包文件,镜像只是一个虚拟的概念,其实际体现并非由一个文件组成,而是由一组文件系统组成,或者说,由多层文件系统联合组成。
镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。
分层存储的特征还使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。
获取镜像
从 Docker 镜像仓库获取镜像的命令是 docker pull
。其命令格式为:
1 | docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签] |
具体的选项可以通过 docker pull --help
命令看到,这里我们说一下镜像名称的格式。
- Docker 镜像仓库地址:地址的格式一般是
<域名/IP>[:端口号]
。默认地址是 Docker Hub。 - 仓库名:如之前所说,这里的仓库名是两段式名称,即
<用户名>/<软件名>
。对于 Docker Hub,如果不给出用户名,则默认为library
,也就是官方镜像。
比如:
1 | docker pull ubuntu:16.04 |
上面的命令中没有给出 Docker 镜像仓库地址,因此将会从 Docker Hub 获取镜像。而镜像名称是 ubuntu:16.04
,因此将会获取官方镜像 library/ubuntu
仓库中标签为 16.04
的镜像。
从下载过程中可以看到我们之前提及的分层存储的概念,镜像是由多层存储所构成。下载也是一层层的去下载,并非单一文件。下载过程中给出了每一层的 ID 的前 12 位。并且下载结束后,给出该镜像完整的 sha256
的摘要,以确保下载一致性。
在使用上面命令的时候,你可能会发现,你所看到的层 ID 以及 sha256
的摘要和这里的不一样。这是因为官方镜像是一直在维护的,有任何新的 bug,或者版本更新,都会进行修复再以原来的标签发布,这样可以确保任何使用这个标签的用户可以获得更安全、更稳定的镜像。
如果从 Docker Hub 下载镜像非常缓慢,可以参照 镜像加速器
一节配置加速器。
列出镜像
基础命令
1 | docker images |
1 | [root@VM-0-11-centos ~]# docker images |
1 | [root@VM-0-11-centos ~]# docker image ls |
中间层镜像
为了加速镜像构建、重复利用资源,Docker 会利用 中间层镜像。所以在使用一段时间后,可能会看到一些依赖的中间层镜像。默认的 docker image ls
列表中只会显示顶层镜像,如果希望显示包括中间层镜像在内的所有镜像的话,需要加 -a
参数。
1 | docker image ls -a |
镜像体积
docker image ls
列表中的镜像体积总和并非是所有镜像实际硬盘消耗。由于 Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于 Docker 使用 Union FS,相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。以下命令来便捷的查看镜像、容器、数据卷所占用的空间。
1 | docker system df |
1 | [root@VM-0-11-centos ~]# docker system df |
列出部分镜像
根据仓库名列出镜像,可以自动补齐(Tab键)
1 | docker image ls ubuntu |
列出特定的某个镜像,也就是说指定仓库名和标签,可以自动补齐(Tab键)
1 |
|
除此以外,docker image ls
还支持强大的过滤器参数 --filter
,或者简写 -f
以特定格式显示
1 | [root@VM-0-11-centos ~]# docker image ls -q |
==GO语法模板==
只包含镜像ID和仓库名
1 | oot@VM-0-11-centos ~]# docker image ls --format "{{.ID}}: {{.Repository}}" |
1 | [root@VM-0-11-centos ~]# docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}" |
删除镜像
基础命令
1 | docker image rm [选项] <镜像1> [<镜像2> ...] |
其中,<镜像>
可以是 镜像短 ID
、镜像长 ID
、镜像名
或者 镜像摘要
。
可以使用 docker image ls -q
来配合使用 docker image rm
,这样可以成批的删除希望删除的镜像。
比如,需要删除所有仓库名为 redis
的镜像:
1 | docker image rm $(docker image ls -q redis) |
构建镜像
docker commit
步骤:
- 运行容器
- 修改容器
- 将容器报存为新的镜像
Dockerfile
Dockerfile 是一个文本文件,其内包含了一条条的**指令(Instruction)**,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
第一个Demo
- 在空白目录下新建目录,并创建 Dockerfile文件
1 | [root@VM-0-11-centos data]# mkdir mynginx |
- Dockerfile内容为:
1 | FROM nginx |
- 执行docker build命令
1 | [root@VM-0-11-centos mynginx]# docker build -t nginx:v3 . |
.
指定build context为当前目录,Docker 默认会从build context中查找Dockerfile文件,可以通过-f 参数指定Dockerfile的位置。这是在指定上下文路径。
当构建的时候,用户会指定构建镜像上下文的路径,docker build
命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
那么为什么会有人误以为 .
是指定 Dockerfile
所在目录呢?这是因为在默认情况下,如果不额外指定 Dockerfile
的话,会将上下文目录下的名为 Dockerfile
的文件作为 Dockerfile。
这只是默认行为,实际上 Dockerfile
的文件名并不要求必须为 Dockerfile
,而且并不要求必须位于上下文目录中,比如可以用 -f ../Dockerfile.php
参数指定某个文件作为 Dockerfile
。
当然,一般大家习惯性的会使用默认的文件名 Dockerfile
,以及会将其置于镜像构建上下文目录中。
总结:
- 从base镜像运行一个容器
- 执行一条命令,对容器进行修改
- 执行类型docker commit的操作,生成一个新的镜像层
- Docker 再基于刚刚的镜像运行一个新容器
- 重复2-4步,直到Dockerfile中的所有指令执行完毕
查看镜像分层
docker history 会显示镜像的构建历史,也就是Dockerfile的执行过程。
1 | docker histroy |
1 | [root@VM-0-11-centos /]# docker history nginx:v3 |
注:missing表示无法获取IMAGE ID,通常从Docker Hub下载的镜像会有这个问题
其它 docker build
的用法
直接用 Git repo 进行构建
1 | docker build https://github.com/twang2218/gitlab-ce-zh.git |
用给定的 tar 压缩包构建
1 | docker build http://server/context.tar.gz |
Dockerfile常用指令
FROM
指定base镜像
MAINTAINER
设置镜像作者
COPY
将文件从build context 复制到镜像
COPY
支持两种形式:COPY src dest 与 COPY[“src”, “desc”]。
注意:src只能指定build context中的文件或目录
COPY
指令将从构建上下文目录中 <源路径>
的文件/目录复制到新的一层的镜像内的 <目标路径>
位置。比如:
1 | COPY package.json /usr/src/app/ |
<目标路径>
可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR
指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。
ADD
ADD
指令和 COPY
的格式和性质基本一致。但是在 COPY
基础上增加了一些功能。
从build context复制文件到镜像,如果src是归档文件(tar、zip、tgz、xz等),文件会被自动解约到dest。
在某些情况下,这个自动解压缩的功能非常有用,比如官方镜像 ubuntu
中:
1 | FROM scratch |
但在某些情况下,如果我们真的是希望复制个压缩文件进去,而不解压缩,这时就不可以使用 ADD
命令了。
在 Docker 官方的 Dockerfile 最佳实践文档
中要求,尽可能的使用 COPY
,因为 COPY
的语义很明确,就是复制文件而已,而 ADD
则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD
的场合,就是所提及的需要自动解压缩的场合。
因此在 COPY
和 ADD
指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY
指令,仅在需要自动解压缩的场合使用 ADD
ENV
设置环境变量。
格式有两种:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。比如在官方 node
镜像 Dockerfile
中,就有类似这样的代码:
1 | ENV NODE_VERSION 7.2.0 |
在这里先定义了环境变量 NODE_VERSION
,其后的 RUN
这层里,多次使用 $NODE_VERSION
来进行操作定制。可以看到,将来升级镜像构建版本的时候,只需要更新 7.2.0
即可,Dockerfile
构建维护变得更轻松了。
下列指令可以支持环境变量展开: ADD
、COPY
、ENV
、EXPOSE
、LABEL
、USER
、WORKDIR
、VOLUME
、STOPSIGNAL
、ONBUILD
。
可以从这个指令列表里感觉到,环境变量可以使用的地方很多,很强大。通过环境变量,我们可以让一份 Dockerfile
制作更多的镜像,只需使用不同的环境变量即可
EXPOSE
指定容器中的进程会监听某个端口,Docker可以将该端口暴露出去。
格式为 EXPOSE <端口1> [<端口2>...]
。
EXPOSE
指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P
时,会自动随机映射 EXPOSE
的端口。
要将 EXPOSE
和在运行时使用 -p <宿主端口>:<容器端口>
区分开来。-p
,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE
仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。
VOLUME
将文件或者目录声明为volume。
格式为:
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>
WORKDIR
为后面的RUN、CMD、ENTRY、ADD或COPY指令设置镜像中的当前工作目录。
格式为 WORKDIR <工作目录路径>
。
使用 WORKDIR
指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR
会帮你建立目录。
之前提到一些初学者常犯的错误是把 Dockerfile
等同于 Shell 脚本来书写,这种错误的理解还可能会导致出现下面这样的错误:
1 | RUN cd /app |
如果将这个 Dockerfile
进行构建镜像运行后,会发现找不到 /app/world.txt
文件,或者其内容不是 hello
。原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 Dockerfile
中,这两行 RUN
命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dockerfile
构建分层存储的概念不了解所导致的错误。
之前说过每一个 RUN
都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 RUN cd /app
的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。
因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR
指令
RUN
在容器中运行指定的命令
RUN
指令是用来执行命令行命令的。由于命令行的强大能力,RUN
指令在定制镜像时是最常用的指令之一。其格式有两种:
shell 格式:
RUN <命令>
,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的RUN
指令就是这种格式。1
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
exec 格式:
RUN ["可执行文件", "参数1", "参数2"]
,这更像是函数调用中的格式。
CMD
容器启动时运行指定的命令。
CMD
指令的格式和 RUN
相似,也是两种格式:
shell
格式:CMD <命令>
exec
格式:CMD ["可执行文件", "参数1", "参数2"...]
- 参数列表格式:
CMD ["参数1", "参数2"...]
。在指定了ENTRYPOINT
指令后,用CMD
指定具体的参数。
Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD
指令就是用于指定默认的容器主进程的启动命令的。
在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,ubuntu
镜像默认的 CMD
是 /bin/bash
,如果我们直接 docker run -it ubuntu
的话,会直接进入 bash
。我们也可以在运行时指定运行别的命令,如 docker run -it ubuntu cat /etc/os-release
。这就是用 cat /etc/os-release
命令替换了默认的 /bin/bash
命令了,输出了系统版本信息。
在指令格式上,一般推荐使用 exec
格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 "
,而不要使用单引号。
如果使用 shell
格式的话,实际的命令会被包装为 sh -c
的参数的形式进行执行。比如:
1 | CMD echo $HOME |
在实际执行中,会将其变更为:
1 | CMD [ "sh", "-c", "echo $HOME" ] |
ENTRYPOINT
设置容器启动时运行的命令。
Dockerfile中可以有多个ENTRYPOINT指令,但只有最后一个生效。CMD或docker run 之后的参数会被当作参数传递给ENTRYPOINT。
ENTRYPOINT
的格式和 RUN
指令格式一样,分为 exec
格式和 shell
格式。
ENTRYPOINT
的目的和 CMD
一样,都是在指定容器启动程序及参数。ENTRYPOINT
在运行时也可以替代,不过比 CMD
要略显繁琐,需要通过 docker run
的参数 --entrypoint
来指定。
当指定了 ENTRYPOINT
后,CMD
的含义就发生了改变,不再是直接的运行其命令,而是将 CMD
的内容作为参数传给 ENTRYPOINT
指令,换句话说实际执行时,将变为:
1 | <ENTRYPOINT> "<CMD>" |
那么有了 CMD
后,为什么还要有 ENTRYPOINT
呢?这种 <ENTRYPOINT> "<CMD>"
有什么好处么?让我们来看几个场景。
场景一:让镜像变成像命令一样使用
假设我们需要一个得知自己当前公网 IP 的镜像,那么可以先用 CMD
来实现:
1 | FROM ubuntu:16.04 |
假如我们使用 docker build -t myip .
来构建镜像的话,如果我们需要查询当前公网 IP,只需要执行:
1 | $ docker run myip |
嗯,这么看起来好像可以直接把镜像当做命令使用了,不过命令总有参数,如果我们希望加参数呢?比如从上面的 CMD
中可以看到实质的命令是 curl
,那么如果我们希望显示 HTTP 头信息,就需要加上 -i
参数。那么我们可以直接加 -i
参数给 docker run myip
么?
1 | $ docker run myip -i |
我们可以看到可执行文件找不到的报错,executable file not found
。之前我们说过,跟在镜像名后面的是 command
,运行时会替换 CMD
的默认值。因此这里的 -i
替换了原来的 CMD
,而不是添加在原来的 curl -s http://ip.cn
后面。而 -i
根本不是命令,所以自然找不到。
那么如果我们希望加入 -i
这参数,我们就必须重新完整的输入这个命令:
1 | $ docker run myip curl -s http://ip.cn -i |
这显然不是很好的解决方案,而使用 ENTRYPOINT
就可以解决这个问题。现在我们重新用 ENTRYPOINT
来实现这个镜像:
1 | FROM ubuntu:16.04 |
这次我们再来尝试直接使用 docker run myip -i
:
1 | $ docker run myip |
可以看到,这次成功了。这是因为当存在 ENTRYPOINT
后,CMD
的内容将会作为参数传给 ENTRYPOINT
,而这里 -i
就是新的 CMD
,因此会作为参数传给 curl
,从而达到了我们预期的效果。
Docker 容器
启动容器
启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(stopped
)的容器重新启动。
因为 Docker 的容器实在太轻量级了,很多时候用户都是随时删除和新创建容器。
新建并启动
所需要的命令主要为 docker run
。
在讨论Dockerfile时,可用三种方式指定容器启动时执行的命令:
- CMD
- ENTRYPOINT
- docker run命令行中指定
例如,下面的命令输出一个 “Hello World”,之后终止容器。
1 | docker run ubuntu:14.04 /bin/echo 'Hello world' |
下面的命令则启动一个 bash 终端,允许用户进行交互。
1 | docker run -t -i ubuntu:14.04 /bin/bash |
其中,-t
选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上, -i
则让容器的标准输入保持打开。
当利用 docker run
来创建容器时,Docker 在后台运行的标准操作包括:
- 检查本地是否存在指定的镜像,不存在就从公有仓库下载
- 利用镜像创建并启动一个容器
- 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
- 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
- 从地址池配置一个 ip 地址给容器
- 执行用户指定的应用程序
- 执行完毕后容器被终止
启动已终止容器
可以利用 docker container start
命令,直接将一个已经终止的容器启动运行。
1 | docker container start |
守护态运行
更多的时候,需要让 Docker 在后台运行而不是直接把执行命令的结果输出在当前宿主机下。此时,可以通过添加 -d
参数来实现。
下面举两个例子来说明一下。
如果不使用 -d
参数运行容器。
1 | [root@VM-0-11-centos myip]# docker run ubuntu:16.04 /bin/sh -c "while true; do echo hello world; sleep 1; done" |
容器会把输出的结果 (STDOUT) 打印到宿主机上面
如果使用了 -d
参数运行容器。
1 | [root@VM-0-11-centos myip]# docker run -d ubuntu:16.04 /bin/sh -c "while true; do echo hello world; sleep 1; done" |
此时容器会在后台运行并不会把输出的结果 (STDOUT) 打印到宿主机上面(输出结果可以用 docker logs
查看)。
注: 容器是否会长久运行,是和 docker run
指定的命令有关,和 -d
参数无关。
使用 -d
参数启动后会返回一个唯一的 id,也可以通过 docker container ls
命令来查看容器信息。
使用 -d
参数启动后会返回一个唯一的 id,也可以通过 docker container ls
命令来查看容器信息。
1 | [root@VM-0-11-centos myip]# docker container ls |
要获取容器的输出信息,可以通过 docker container logs
命令。
1 | [root@VM-0-11-centos myip]# docker container logs 5173b19b14c4 |
终止容器
可以使用 docker container stop
来终止一个运行中的容器。
此外,当 Docker 容器中指定的应用终结时,容器也自动终止。
例如对于上一章节中只启动了一个终端的容器,用户通过 exit
命令或 Ctrl+d
来退出终端时,所创建的容器立刻终止。
终止状态的容器可以用 docker container ls -a
命令看到
1 | docker container ls -a |
处于终止状态的容器,可以通过 docker container start
命令来重新启动。
此外,docker container restart
命令会将一个运行态的容器终止,然后再重新启动它。
进入容器
在使用 -d
参数时,容器启动后会进入后台。
某些时候需要进入容器进行操作,包括使用
docker attach
docker exec
attach
命令
docker attach
是 Docker 自带的命令。下面示例如何使用该命令。
1 | # 查看运行中的容器 |
如果从这个 stdin 中 exit,会导致容器的停止
exec
命令
docker exec
后边可以跟多个参数,这里主要说明 -i
-t
参数。
只用 -i
参数时,由于没有分配伪终端,界面没有我们熟悉的 Linux 命令提示符,但命令执行结果仍然可以返回。
当 -i
-t
参数一起使用时,则可以看到我们熟悉的 Linux 命令提示符。
1 | [root@VM-0-11-centos myip]# docker exec -it 99a009bbe1f8 bash |
如果从这个 stdin 中 exit,不会导致容器的停止。这就是为什么推荐大家使用 docker exec
的原因。
Docker 仓库
仓库(Repository
)是集中存放镜像的地方。
一个容易混淆的概念是注册服务器(Registry
)。实际上注册服务器是管理仓库的具体服务器,每个服务器上可以有多个仓库,而每个仓库下面有多个镜像。从这方面来说,仓库可以被认为是一个具体的项目或目录。例如对于仓库地址 dl.dockerpool.com/ubuntu
来说,dl.dockerpool.com
是注册服务器地址,ubuntu
是仓库名。
Docker Hub
目前 Docker 官方维护了一个公共仓库 Docker Hub,其中已经包括了数量超过 15,000 的镜像。大部分需求都可以通过在 Docker Hub 中直接下载镜像来实现
注册
你可以在 https://cloud.docker.com 免费注册一个 Docker 账号
登录
可以通过执行 docker login
命令交互式的输入用户名及密码来完成在命令行界面登录 Docker Hub。
你可以通过 docker logout
退出登录
1 |
|
拉取镜像
1 | [root@VM-0-11-centos myip]# docker search centos |
可以看到返回了很多包含关键字的镜像,其中包括镜像名字、描述、收藏数(表示该镜像的受关注程度)、是否官方创建、是否自动创建。
官方的镜像说明是官方项目组创建和维护的,automated 资源允许用户验证镜像的来源和内容。
根据是否是官方提供,可将镜像资源分为两类。
一种是类似 centos
这样的镜像,被称为基础镜像或根镜像。这些基础镜像由 Docker 公司创建、验证、支持、提供。这样的镜像往往使用单个单词作为名字。
还有一种类型,比如 tianon/centos
镜像,它是由 Docker 的用户创建并维护的,往往带有用户名称前缀。可以通过前缀 username/
来指定使用某个用户提供的镜像,比如 tianon 用户。
另外,在查找的时候通过 --filter=stars=N
参数可以指定仅显示收藏数量为 N
以上的镜像。
推送镜像
用户也可以在登录后通过 docker push
命令来将自己的镜像推送到 Docker Hub。
1 | [root@VM-0-11-centos myip]# docker tag ubuntu:16.04 kesenhuang/ubuntu:16.04 |
Docker 私有仓库
有时候使用 Docker Hub 这样的公共仓库可能不方便,用户可以创建一个本地仓库供私人使用。
本节介绍如何使用本地仓库。
docker-registry
是官方提供的工具,可以用于构建私有的镜像仓库。本文内容基于 docker-registry
v2.x 版本。
安装运行 docker-registry
容器运行
你可以通过获取官方 registry
镜像来运行。
1 | docker run -d -p 5000:5000 --restart=always --name registry registry |
这将使用官方的 registry
镜像来启动私有仓库。默认情况下,仓库会被创建在容器的 /var/lib/registry
目录下。你可以通过 -v
参数来将镜像文件存放在本地的指定路径。例如下面的例子将上传的镜像放到本地的 /opt/data/registry
目录。
1 | [root@VM-0-11-centos ~]# docker run -d \ |
在私有仓库上传、搜索、下载镜像
创建好私有仓库之后,就可以使用 docker tag
来标记一个镜像,然后推送它到仓库。例如私有仓库地址为 127.0.0.1:5000
。
先在本机查看已有的镜像。
1 | docker image ls |
使用 docker tag
将 ubuntu:latest
这个镜像标记为 127.0.0.1:5000/ubuntu:latest
。
格式为 docker tag IMAGE[:TAG] [REGISTRY_HOST[:REGISTRY_PORT]/]REPOSITORY[:TAG]
。
1 | [root@VM-0-11-centos ~]# docker tag ubuntu:latest 127.0.0.1:5000/ubuntu:lates |
使用 docker push
上传标记的镜像。
1 | [root@VM-0-11-centos ~]# docker push 127.0.0.1:5000/ubuntu:latest |
用 curl
查看仓库中的镜像。
1 | [root@VM-0-11-centos ~]# curl 127.0.0.1:5000/v2/_catalog |
这里可以看到 {"repositories":["ubuntu"]}
,表明镜像已经被成功上传了。
先删除已有镜像,再尝试从私有仓库中下载这个镜像。
1 | [root@VM-0-11-centos ~]# docker images |
注意事项
如果你不想使用 127.0.0.1:5000
作为仓库地址,比如想让本网段的其他主机也能把镜像推送到私有仓库。你就得把例如 192.168.199.100:5000
这样的内网地址作为私有仓库地址,这时你会发现无法成功推送镜像。
这是因为 Docker 默认不允许非 HTTPS
方式推送镜像。我们可以通过 Docker 的配置选项来取消这个限制,或者查看下一节配置能够通过 HTTPS
访问的私有仓库。
Ubuntu 14.04, Debian 7 Wheezy
对于使用 upstart
的系统而言,编辑 /etc/default/docker
文件,在其中的 DOCKER_OPTS
中增加如下内容:
1 | DOCKER_OPTS="--registry-mirror=https://registry.docker-cn.com --insecure-registries=192.168.199.100:5000" |
重新启动服务。
1 | $ sudo service docker restart |
Ubuntu 16.04+, Debian 8+, centos 7
对于使用 systemd
的系统,请在 /etc/docker/daemon.json
中写入如下内容(如果文件不存在请新建该文件)
1 | { |
注意:该文件必须符合
json
规范,否则 Docker 将不能启动。
其他
对于 Docker for Windows 、 Docker for Mac 在设置中编辑 daemon.json
增加和上边一样的字符串即可。/