概念
- image 镜像:
层叠的只读文件系统,比如有Linux针对于docker而创建的最小的镜像,和其他共享的镜像。简言之就是系统镜像文件。利用面向对象的思想,可以认为image
就是一个类,而容器就该类的一个实例。
Docker
镜像的层次如下:
- 最底端是一个引导文件系统,即bootfs,这很像典型的Linux/Unix的引导文 件系统。Dockei?用户几乎永远不会和引导文件系统有什么交互。实际上,当一个容器启动 后,它将会被移到内存中,而引导文件系统则会被卸载(unmount),以留出更多的内存供 initrd磁盘镜像使用。
- 第二层是root文件系统rootfs,它位于引导文件系统之上。rootfs可以是一种或多种操作系统(如Debian或者Ubuntu文件系统)。在传统的Linux引导过程中,root文件系统会最先以只读的方式加载,当引导结束并完成了完整性检查之后,它才会被切换为读写模式。但是在Docker里,root文件系统永远只能是只读状态
- 在顶层则是联合加载的多个只读应用文件系统,虽然看似加载一个,但是已经加载了多个文件系统,所以称之为联合加载。
Docker将这样的文件系统称为镜像。一个镜像可以放到另一个镜像的顶部。位于下面的镜像称为父镜像(parent image),可以依次类推,直到镜像栈的最底部,最底部的镜像称为基础镜像(base image)。最后,当从一个镜像启动容器时,Docker会在该镜像的最顶层加载一个读写文件系统。
在Ubuntu镜像的存放地址为
/var/lib/docker
,可以通过docker info
查看详细信息
列出镜像
使用docker images [image_name]
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 0458a4468cbc 3 weeks ago 112MB
hello-world latest f2a91732366c 2 months ago 1.85kB
镜像从仓库下载下来。镜像保存在仓库中,而仓库存在于Registry中。默认的Registry 是由Docker公司运营的公共Registry服务,即Docker Hub,如图下:
搜索镜像
使用docker search image_name
可以看到:
仓库名
镜像描述
用户评价(Stars) 反应出一个镜像的受欢迎程度;
是否官方(Official) 由上游开发者管理的镜像(如fedora镜像由Fedora团队管理);
自动构建(Automated) 表示这个镜像是由Docker Hub的自动构建(AutomatedBuild)流程创建的。
拉取镜像
使用docker pull image_name[:tag]
$ sudo docker pull ubuntu
Pulling repository ubuntu
c4ff7513909d: Pulling dependent layers
3db9c44f4520: Pulling dependent layers
75204fdb260b: Pulling dependent layers
可以看到拉取一系列的镜像分层,每一层都是独立的。拉取了ubuntu镜像,实际上得到的是很多版本的Ubuntu操作系统,包括10.04、12.04、13.04和14.04。
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 0458a4468cbc 3 weeks ago 112MB
hello-world latest f2a91732366c 2 months ago 1.85kB
虽然称其为Ubuntu操作系统,但实际上它并不是一个完整的操作系统。它只是一个 裁剪版,只包含最低限度的支持系统运行的组件。
为了区分同一个仓库中的不同镜像,Docker提供了一种称为标签(tag)的功能。每个 镜像在列出来时都带有一个标签,如12.10、12.04、quantal等。每个标签对组成特定 镜像的一些镜像层进行标记(比如,标签12.04就是对所有Ubuntu 12.04镜像的层的标记这种机制使得在同一个仓库中可以存储多个镜像。
我们可以通过在仓库名后面加上一个冒号和标签名来指定该仓库中的某一镜像,如
$ sudo docker run -t -i --name new_container ubuntu:12.04 /bin/bash
[email protected]:/#
这个例子会从镜像ubuntu:12.04启动一个容器,而这个镜像的操作系统则是Ubuntu 12.04。我们还能看到很多镜像具有相同的镜像ID (见镜像ID74fe38dll401),它们被打上很多个标签。比如,74fe38dll401的镜像被打上了 12.04和precise两个标签,分 别代表该Ubuntu发布版的版本号和代号(code name)。
在构建容器时指定仓库的标签也是一个很好的习惯。这样便可以准确地指定容器来源于 哪里不同标签的镜像会有不同,比如Ubutnu 12.04和14.04就不一样,指定镜像的标签会让我们确切知道自己使用的是ubuntu:12.04,这样我们就能准确知道我们在干什么。Docker Hub中有两种类型的仓库:用户仓库(user repository)和顶层仓库(top-level repository)。用户仓库的镜像都是由Docker用户创建的,而顶层仓库则是由Docker内部的 人来管理的。
用户仓库的命名由用户名和仓库名两部分组成,如jamturl/puppet。
- 用户名:jamturl
- 仓库名:puppet
与之相对,顶层仓库只包含仓库名部分,如ubuntu仓库。顶层仓库由Docker公司和 由选定的能提供优质基础镜像的厂商(如Fedora团队提供了 fedora镜像)管理,用户可 以基于这些基础镜像构建自己的镜像。同时顶层仓库也代表了各厂商和Docker公司的一种承诺,即顶层仓库中的镜像是架构良好、安全且最新的。
用docker run命令从镜像启动一个容器时,如果该镜像不在本地,Docker会先从 Docker Hub下载该镜像。如果没有指定具体的镜像标签,那么Docker会自动下载latest标签。
构建Docker镜像有以下两种方法。
- 使用 docker commit 命令。
- 使用 docker build 命令和 Dockerfile 文件。
Docker commit
创建Docker Hub账号
构建镜像中很重要的一环就是如何共享和发布镜像。可以将镜像推送到Docker Hub或 者用户自己的私有Registry中。为了完成这项工作,需要在Docker Hub上创建一个账号, 可以从 https://hub.docker.com/account/signup/加入 Docker Hub
下面就可以测试刚才注册的账号是否能正常工作了。要登录到Docker Hub,可以使用 docker login命令
$ sudo docker login
Username: one2inf
Password:
Email: [email protected] Login
Succeeded
这条命令将会完成登录到Docker Hub的工作,并将认证信息保存起来以供后面使用。你的个人认证信息将会保存到$HOME/.dockercfg文件中。
用Docker的commit命令创建镜像
创建Docker镜像的第一种方法是使用docker commit命令。可以将此想象为是在版本控制系统里提交变更。先创建一个容器,并在容器里做出修改,就像修改代码 一样,最后再将修改提交为一个新镜像。
先从创建一个新容器开始,这个容器基于前面己经见过的ubuntu镜像
$ docker run --name=2commit -it ubuntu /bin/bash
[email protected]:/# apt-get update
......
$ apt-get install -y apache2 [--fix-missing]
启动了一个容器,并在里面安装了 Apache。将这个容器作为一个Web服务器来运行,所以把它的当前状态保存下来。这样就不必每次都创建一个新容器并再次在里面安装Apache 了。为了完成此项工作,需要先使用exit命令从容器里退出,之后再运行docker commit命令
$ sudo docker commit b54765ab2a5b one2inf/apache2
[sudo] password for whoami:
sha256:4c0be6203dec5655c2de0ed
需要注意的是,docker commit提交 的只是创建容器的镜像与容器的当前状态之间有差异的部分,这使得该更新非常轻量。来看看新创建的镜像
$ docker images one2inf/apache2
REPOSITORY TAG IMAGE ID CREATED SIZE
one2inf/apache2 latest 4c0be6203dec About a minute ago 250MB
像git commit一样可以提交一些额外的信息如
-m="A new custom image" 提交一个描述信息
--author="whoami" 提交作者
one2inf/apache2:[tag] 添加一个标签
如
$ sudo docker commit b54765ab2a5b one2inf/apache2:webserver -m="A new custom image" --author="whoami"
这些信息可以使用docker inspect
进行查看
并不推荐使用docker commit的方法来构建镜像。相反,推荐使用被称为 Dockerfile的定义文件和docker build命令来构建镜像。Dockerfile使用基本的基于DSL语法的指令来构建一个Docker镜像,之后使用docker build命令基于该 Dockerfile中的指令构建一个新的镜像。
第一个Dockerfile
- 启动Docker服务
service docker start
先创建一个Dockerfile,内容如下
FROM alpine:latest
MAINTAINER whoami
CMD echo "hello docker" --指定需要执行的命令
该Dockerfile由一系列指令和参数组成。每条指令如FROM,都必须为大写字母,且后面要跟随一个参数Dockerfile中的指令会按顺序从上到下执行,所以应该根据需要合理安排指令的顺序。
参数说明
- alpine是linux针对Docker的一个极小的容器
- latest是标签
- MAINTAINER 指定维护者的名字
- CMD command
每条指令都会创建一个新的镜像层并对镜像进行提交。Docker大体上按照如下流程执行Dockerfile中的指令。
- Docker从基础镜像运行一个容器。
- 执行一条指令,对容器做出修改。
- 执行类似docker ccmmit的操作,提交一个新的镜像层。
- Docker再基于刚提交的镜像运行一个新容器。
- 执行Dockerfile中的下一条指令,直到所有指令都执行完毕。
从上面也可以看出,如果你的Dockerfile由于某些原因(如某条指令失败了)没有正常结束,那么你将得到了一个可以使用的镜像。这对调试非常有帮助,可以基于该镜像运行一个具备交互功能的容器,使用最后创建的镜像对为什么你的指令会失畋进行调试。
- 接着执行
docker build -t hello_docker .
使用docker命令构建一个新的容器,参数
-t
指定名字.
代表当前路径意思是寻找当前路径的Dockerfile,所以Dockerfile
的名字最好与约定的一样即可
最后运行容器
[~] sudo docker run hello_docker
hello docker
第二个Dockerfile
- 先创建工作目录HelloDocker
shell [~] cd HelloDocker [HelloDocker]
- 创建Dockerfile,其内容如下:
dockerfile FROM ubuntu MAINTAINER whoami RUN sed -i ‘s/archive.ubuntu.com/mirrors.ustc.edu.cn/g‘ /etc/apt/sources.list RUN apt-get update RUN apt-get install -y nginx COPY index.html /var/www/html ENTRYPOINT ["/usr/sbin/nginx", "-g", "daemon off;"] EXPOSE 80
FROM ubuntu 设置基础镜像为 ubuntu 每个Dockerf ile的第一条指令都应该是FROM,后续指令都将基于该镜像进行,这个镜像被称为基础镜像(base iamge)
MAINTAINER 指定维护者的名字 whoami 这条指令会告诉Docker该镜像的作者是谁,以及作者 的电子邮件地址。这有助于标识镜像的所有者和联系方式。
RUN sed -i ‘s/archive.ubuntu.com/mirrors.ustc.edu.cn/g‘ /etc/apt/sources.list
RUN 是执行,这一句的意思是替换源为中国科学技术大学的源。
RUN apt-get update 安装完成后,更新apt源
RUN apt-get install -y nginx 安装 nginx
RUN指令会在shell里使用命令包装器/bin/sh -c来执行。如果是在一个不支持shell的平台上运行或者不希望在shell中运行(比如避免shell字符串篡改),也可以使用exec格式的RUN指令
RUN [ "apt-get", " install", "-y", "nginx"]
在这种方式中,使用一个数组来指定要运行的命令和传递给该命令的每个参数。
COPY index.html /var/www/html 将当前路径下的index.html拷贝至nginx的项目目录
ENTRYPOINT ["/usr/sbin/nginx", "-g", "daemon off;"] 入口点,数组中的数据组成一条命令,作用是将nginx放置到前台,而还是作为守护进程运行
EXPOSE 80 将容器的80端口暴露以便访问服务器!
这条指令告诉Docker该容器内的应用程序将会使用容器的指定端口。这并不意味着可以自动访问任意容器运行中服务的端口(这里是80)。出于安全的原因,Docker并不会自动打开该端口,而是需要你在使用docker run运行容器时来指 定需要打开哪些端口。
创建index.html
shell [HelloDocker] echo "<b>hello the cruel woirld</b>" > index.html [HelloDocker] cat index.html <b>hello the cruel woirld</b
- 构建
sudo docker build -t whoami/hello-nginx .
命令中最后的.告诉Docker到本地目录中去找Dockerfile文件。也可以在命令后面加上一个Git仓库的源地址来指定Dockerfile的位置
运行容器
docker run -d -p 80:80 whoami/hello-nginx
使用curl测试
curl http://localhost
Dockerfile与image分层
Dockerfile中的每一行都产生一个新层,即第一层都有一个独立的ID
FROM alpine:latest
-------4e38e38c8ce0
MAINTAINER whoami
--------fb1aabf4427b
CMD echo ‘hello docker‘
---53df065bfdff6
image层本身是read only,当image运行为一个容器时,会产生一个容器层(container layer),可读可写。分层好处:多个image时,相同的层可以共享,减轻压力。
指令失败时会怎样
前面介绍了一个指令失败时将会怎样。下面来看一个例子:假设将软件包的名字弄错了,比如写成了ngin。
再来运行一遍构建过程并看看当指令失败时会怎样
$ cd static一web
$ sudo docker build -t="jamtur01/static_web".
Sending build context to Docker daemon 2.56 kB
Sending build context to Docker daemon
Step 1 : FROM ubuntu:14.04 ---> 8dbd9e392a96
Step 2 : MAINTAINER James Turnbull "[email protected]"
> Running in d97e0clcf6ea
——> 85130977028d
Step 3 : RUN apt-get update
> Running in 85130977028d
——> 997485f46ec4
Step 4 : RUN apt-get install -y ngin > Running in ffcal6d58fd8
Reading package lists...
Building dependency tree...
Reading state information...
E: Unable to locate package ngin
2014/06/04 18:41:11 The command [/bin/sh -c apt-get install -y ngin] returned a non-zero code: 100
这时候需要调试一下这次失败。我可以用docker run命令来基于这次构建到目前 为止己经成功的最后一步创建一个容器,这里它的ID是997485f46ec4
$ sudo docker run -t -i 997485f46ec4 /bin/bash
dcgel2e59fe8:/#
这时在这个容器中可以再次运行apt-get install -y nginx,并指定正确的包名或者通过进一步调试来找出到底是哪里出错了。一旦解决了这个问题就可以退出容器,使用正确的包名修改Dockerfile文件,之后再尝试进行构建。
Dockerfile和构建缓存
由于每一步的构建过程都会将结果提交为镜像,它会将之前的镜像层看做缓存。
然而有些时候需要确保构建过程不会使用缓存。比如apt-get update,那么Docker将不会再次刷新APT包的缓存。这时你可能需要取得每个包的最新版本。要想略过缓存功能,可以使用docker build的--no-cache标志
$ sudo docker build --no-cache -t=image_name
基于缓存的Dockerfile
构建缓存带来的一个好处就是,可以实现简单的Dockerfile模板(比如在 Dockerfile文件顶部增加包仓库或者更新包,从而尽可能确保缓存命中)。在自己的Dockerfile文件顶部使用相同的指令集模板,比如对Ubuntu
FROM ubuntu:14.04
MAINTAINER James Turnbull "[email protected]"
ENV REFRESHED一AT 2014-07-01 RUN apt-get -yq update
首先通过FROM指令为新镜像设置了一个基础镜像ubuntu: 14.04。接着使用MAINTAINER指令添加了自己的详细信息之后又使用了一条新出现的指令ENV来在镜像中设置环境变量。通过ENV指令来设置了一个名为REFRESHED_AT的环境变量,这个环境变量用来表明该镜像模板最后的更新时间。最后,我使用了RUN指令来运行apt-get -yq update 命令。该指令运行时将会刷新APT包的缓存,用来确保能将要安装的每个软件包都更新到最新版本。
有了这个模板,如果想刷新一个构建,只需修改ENV指令中的日期。这使Docker在命 中ENV指令时开始重置这个缓存,并运行后续指令而无须依赖该缓存。也就是说,RUN apt-get update这条指令将会被再次执行,包缓存也将会被刷新为最新内容。可以扩展 此模板,比如适配到不同的平台或者添加额外的需求。
将镜像推送到Docker Hub
镜像构建完毕之后,我们也可以将它上传到Docker Hub上面去,这样其他人就能使用这个镜像了。比如,我们可以在组织内共享这个镜像,或者完全公开这个镜像。Docker Hub也提供了对私有仓库的支持,这是一个需要付费的功能,你可以将镜像存储到私有仓库中,这样只有你或者任何与你共享这个私有仓库的人才能访问该镜像。这样你 就可以将机密信息或者代码放到私有镜像中,不必担心被公开访问了。
可以通过docker push命令将镜像推送到Docker Hub
$ sudo docker push static_web
2013/07/01 18:34:47 Impossible to push a "root" repository.
Please rename your repository in <user>/<repo> (ex: jamturOl/ static_web)
出什么问题了?我们尝试将镜像推送到远程仓库Static_web,但是Docker认为这是 一个root仓库。root仓库是由Docker公司的团队管理的,因此会拒绝我们的推送请求。让我们再换一种方式试一下,
$ sudo docker push jamtur01/static_web
The push refers to a repository [jamturOl/static_web] (len: 1)
Processing checksums Sending image list
Pushing repository jamtur01/static_web to registry-1.docker.io (1 tags)
自动构建
除了从命令行构建和推送镜像,Docker Hub还允许我们定义自动构建(Automated Builds)。为了使用自动构建,我们只需要将GitHub或BitBucket中含有Dockerfile文 件的仓库连接到Docker Hub即可。向这个代码仓库推送代码时,将会触发一次镜像构建活 动并创建一个新镜像。在之前该工作机制也被称为可信构建
在Docker Hub中添加自动构建任务的第一步是将GitHub或者BitBucket账号连接到 Docker Hub。具体操作是,打幵Docker Hub,登录后单击个人信息连接,之后单击Add Repository -〉Automated Build 按钮
有两个选项可以选择:Public and Private (recommended)和 Limited。选择 Public and Private (recommended)并单击 Allow Access 完成授权操作。有可能会被要求输入GitHub的密码来确认访问请求。
完成之后在Github创建一个空项目JupyterEnv
选择该仓库进行自动构建
接下来的操作不言而喻。
删除镜像
使用docker rmi命令来删除一个镜像
$ sudo docker rmi image_name
Untagged: 06c6clf81534
Deleted: 06c6clf81534
Deleted: 9f551a68e60f
Deleted: 997485f4 6ec4
Deleted: al01d806d694
Deleted: 85130977028d
删除了镜像。在这里也可以看到Docker的分层文件系统:每一个Deleted:行都代表一个镜像层被删除。
该操作与git类似只会将本地的镜像删除。如果之前已经将该镜像推送到Docker Hub上,那么它在 Docker Hub上将依然存在。如果想删除一个Docker Hub上的镜像仓库,需要在登录Docker Hub后使用Delete repository链接来删除
当然也可以一次删除多个镜像。
私有Docker Registry
显然,拥有Docker镜像的一个公共的Registry非常有用。但是有时候我们可能希望构建和存储包含不想被公开的信息或数据的镜像。这时候我们有以下两种选择。
利用Docker Hub上的私有仓库。
在防火墙后面运行你自己的Registry。
值得感谢的是,Docker公司的团队开源@了他们用于运行Docker Registry的代码,这样我们就可以基于此代码在内部运行自己的Registry。
从容器运行Registry
从Docker容器安装一个Registry非常简单。
$ sudo docker run -p 5000:5000 registry
该命令将会启动一个运行Registry应用的容器,并绑定到本地宿主机的5000端口。
测试新 Registry
- 通过docker images命令来找到镜像ID
接着,我们找到镜像ID,即22d47c8cb6e5,并使用新的Registry给该镜像打上标签。为了指定新的Registry目的地址,需要在镜像名前加上主机名和端口前缀。在这个例子里我们的Registry主机名为docker.example.com,
$ sudo docker tag 22d47c8cb6e5 docker.example.com:5000/jamtur01/static_web
为镜像打完标签之后,就能通过docker push命令将它推送到新的Registry中去了
$ sudo docker push docker.example.com:5000/jamtur01/static_web The push refers to a repository [docker.example.com:5000/jamturOl /static_web] (len: 1)
Processing checksums Sending image list
Pushing repository docker.example.com:5000/jamturOl/static_web (1 tags) Pushing 22d47c8cb6e556420e5d58ca5cc376ef18e2de93b5cc90e868albbc8318clc Buffering to disk 58375952/? (n/a)
Pushing 58.38 MB/58.38 MB (100%)
这个镜像就被提交到了本地的Registry中,并且可以将其用于使用docker run命令构建新容器
$ sudo docker run -t -i docker.example.com:5000/jamturOl/static_web /bin/bash