本文主要讲解 Docker 的一些基本操作,包括如何从镜像运行一个容器,如何从容器提交一个镜像,以及 Docker仓库的知识。
准备工作
首先,确保你的电脑连接到了互联网;然后按照之前安装教程里介绍的,运行你的 Docker,然后在终端或者 powershell 里面运行:
sudo docker pull ubuntu:16.04
等待下载完成,你就获得了一个 Ubuntu 16.04 的官方镜像。
完成本文开头的准备工作之后,你可以使用如下命令查看当前你的电脑里有哪些镜像:
sudo docker images
你会得到类似下面的结果(你的结果可能和我有所不同,如果你是全新安装的 Docker,现在应该只有最后一个镜像:Ubuntu:16.04):
可以看到,每个镜像都有5个属性:属于哪个仓库,标签,ID,创建时间和大小。
好了,现在我们已经有一个 Ubuntu 镜像了,下面我们从这个镜像运行一个容器,Docker run 命令可以从一个镜像运行一个包含一个主进程进程的容器:
现在,在你的终端里输入如下命令:
sudo docker run -ti --name first ubuntu:16.04 bash
命令解释:
- Docker run 是从一个镜像运行一个容器的指令。
- -ti 参数的含义是:terminal interactive,这个参数可以让我们进入容器的交互式终端。
- --name 指定容器的名字,后面的 first 就是我们给这个容器起的名字。
- ubuntu:16.04 是致命从哪个镜像运行容器,ubuntu是仓库名,16.04是标签。
- bash 指明我们使用 bash 终端。
运行完以上命令,你会得到下面的结果,你会发现我们已经进入了容器:
现在,你可以在容器里运行一些指令,就好像在一个全新的系统里一样,比如我们运行一个创建文件的指令:
touch test.py
然后你会发现你已经有了这个文件:
还记得吗?在“初遇 Docker”一文中,我们特别强调,镜像是一个静态的概念,这里我们就来看看这是什么意思:
现在我们从相同的镜像再运行一个新的容器:
sudo docker run -ti --name second ubuntu:16.04 bash
执行“ls”命令之后你会发现我们创建的文件“test.py”并不存在:
看到这里,你是否学会了怎么从一个镜像运行一个容器呢?你是否对“镜像是一个静态的概念”这句话有了自己的理解呢?
现在退出我们在上一步运行的容器:直接输入 exit,然后回车就可以退出。这其实是终止了我们的容器:
注意,并不是容器被删除了,而是容器被终止了,我们依旧可以看到我们的容器,运行以下指令可以查看我们当前的容器:
sudo docker ps -a
你会得到下面的输出:
我们刚刚在名字为 first 的容器里创建了一个文件“test.py”,如果这是很重要的文件,你以后还需要它,你希望每次运行一个新容器,这个文件都要存在于新的容器中,怎么办呢?Docker 早就想到了这一点,你可以把容器提交为一个新的镜像,这样你从新镜像运行容器的话,你在容器里创建的文件就都会存在:
使用如下指令可以把刚才名为 first 的容器提交为一个新的镜像:
sudo docker commit first my_image:v1.0
其中,my_image:v1.0 就是“仓库名:版本号”。
现在,使用“sudo docker images”命令查看,你会发现我们多了一个名为“my_image”的镜像:
现在,从这个新的镜像创建容器,你会发现你之前在容器 first 里面创建的 test.py 文件已经存了:
注意
其实,用这种方式创建镜像是很不好的习惯,具体的原因以及更好的办法会在后面的教程说明。现在为了初学者理解方便,我们暂且使用这种方式帮助大家理解,但是希望读者牢记:真正使用 Docker 开发的时候需要避免这种从容器提交镜像的方法!
Docker run 做了什么?
前面,我们一直在用“Docker run”命令来创建容器,那么“Docker run”到底做了什么呢?
具体来说,当你运行“Docker run”的时候:
守护态运行
所谓“守护态运行”其实就是后台运行(background running),有时候,需要让 Docker 在后台运行而不是直接把执行的结果输出到当前的宿主主机下,这个时候需要在运行“docker run”命令的时候加上 “-d”参数(-d means detach)。
下面举个例子说明一下:
如果不使用“-d”参数:
sudo docker run --name withoutD ubuntu:16.04 bash -c "echo hello world"
容器会把输出结果打印到宿主主机上:
如果使用“-d”参数的话,容器启动后会返回容器 ID,但是不会将输出结果打印到宿主主机:
sudo docker run -d --name withD ubuntu:16.04 bash -c "echo hello world"
注意:
这里说的后台运行和容器长久运行不是一回事,后台运行只是说不会在宿主主机的终端打印输出,但是你给定的指令执行完成后,容器就会自动退出,所以,长久运行与否是与你给定的需要容器运行的命令有关,与“-d”参数没有关系。
进入容器和终止容器
进入容器一般有三种方法:
本文只介绍 attach 和 exec 方法,因为这是 Docker 自带的命令,使用起来比较方便;而无论是 ssh 还是 nesenter 的使用都需要一些额外的配置,鉴于本课程是针对完全初学者的,所以就不作太多冗余的介绍了。不过,在后面讲解 Docker 网络的时候,可能还是需要使用一下 ssh。(对于我这种“前后矛盾”的做法希望大家可以理解……)
现在让我们来看一下怎么进入一个正在运行的容器吧,不过在此之前先让我们做一些清理工作,顺便借此学习一下如何删除容器:
sudo docker rm nameOfContainer
以上命令可以删除容器,把 nameOfContainer 替换成你的容器名,按下回车就可以删除对应的容器了。
然后,我们在后台运行一个新的容器:
sudo docker run -d -ti --name background ubuntu:16.04 bash
现在,使用如下命令进入我们的容器:
sudo docker exec -ti background bash
创建一个新文件:
touch test.java
然后在一个新的终端里再次使用我们刚才提到的指令进入容器,发现刚才建立的文件已经存在了:
现在来看一下如何使用 attach 方法吧:
sudo docker attach background
然后按两次回车就可以进入容器了。
疑点解惑:
你可能想问,这两种方法都能进入容器,为什么 Docker 会有两个完全一样功能的指令呢?其实,这两个指令还是有很多不同的,甚至可以说,他们根本就是完全不同的指令!
我这样说你可能更加疑惑了,不要担心,我们马上就来解释:
还记得我们在3.1小节提到的吗?——Docker run 指令会给容器分配一个进程,并且 Docker 的哲学之一就是:一个容器一个进程!所以 attach 实际就是进入容器的主进程,所以无论你同时 attach 多少,其实都是进入了主进程。比如下图,我使用两次 attach 进入同一个容器,然后我在一个 attach 里面运行的指令也会在另一个 attach 里面同步输出,因为它们两个 attach 进入的根本就是一个进程!
在 attach 进入的容器(前提是你退出了 exec)使用“ps -ef”指令可以看出,我们的容器只有一个 bash 进程和 ps 命令本身:
而 exec 就不一样了,exec 的过程其实是给容器新开了一个进程,比如我们使用 exec 进入容器后,使用 ps -ef 命令查看进程:
你会发现,我们除了 ps 命令本身,还有两个 bash 进程,究其原因,就是因为我们 exec 进入容器的时候实际是在容器里面新开了一个进程。这就涉及到了另一个问题,如果你在 exec 里面执行 exit 命令,你只是关掉了 exec 命令新开的进程,而主进程依旧在运行,所以容器并不会停止;而在 attach 里面运行 exit 命令,你实际是终止了主进程,所以容器也就随之被停止了。总结一下,attach 的使用不会在容器开辟新的进程;exec 主要用在需要给容器开辟新进程的情况下。
现在来介绍一下如何终止一个运行的容器。我们的容器在后台运行,现在我们觉得这个容器已经完成了任务,可以把它终止了,怎么办呢?一种办法是 attach 进入容器之后运行“exit”结束容器主进程,这样容器也就随之被终止了。另一种比较推荐的方法是运行:
sudo docker kill nameOfContainer
其它
本小节主要介绍一些有关容器的其它操作,作为前面的补充。
比如你在后台运行一个容器,可是你把“echo”错误输入成了“eceo”:
sudo docker run -d --name logtest ubuntu:16.04 bash -c "eceo hello"
后来,你意识到你的容器没有正常运行,你可以使用“docker logs”指令查看哪里出了问题。
sudo docker logs logtest
你会看到,这条指令会告诉你 eceo 指令未找到。
资源限制主要包含两个方面的内容——内存限制和 CPU 限制。
内存限制
执行“Docker run”命令时可以使用的和内存限制有关的参数如下:
参数 | 简介 |
---|---|
-m, - -memory | 内存限制,格式:数字+单位,单位可以是b, k, m, g,最小4M |
-- -memory-swap | 内存和交换空间总大小限制,注意:必须比-m参数大 |
CPU限制
Docker run 命令执行的时候可以使用的限制 CPU 的参数如下:
参数 | 简介 |
---|---|
-- -cpuset-cpus="" | 允许使用的CPU集 |
-c,- -cpu-shares=0 | CPU共享权值 |
-- -cpu-quota=0 | 限制CPU CFS配额,必须不小于1ms,即>=1000 |
cpu-period=0 | 限制CPU CFS调度周期,范围是100ms~1s,即[1000, 1000000] |
现在详细介绍一下 CPU 限制的这几个参数。
1、可以设置在哪些 CPU 核上运行,比如下面的指令指定容器进程可以在 CPU1 和 CPU3 上运行:
sudo docker run -ti --cpuset-cpus="1,3" --name cpuset ubuntu:16.04 bash
2、CPU 共享权值——CPU 资源相对限制
默认情况下,所有容器都得到同样比例的 CPU 周期,这个比例叫做 CPU 共享权值,通过“-c”或者“- -cpu-shares”设置。Docker 为每个容器设置的默认权值都是1024,不设置或者设置为0都会使用这个默认的共享权值。
比如你有2个同时运行的容器,第一个容器的 CPU 共享权值为3,第2个容器的 CPU 共享权值为1,那么第一个容器将得到75%的 CPU 时间,而第二个容器只能得到25%的 CPU 时间,如果这时你再添加一个 CPU 共享权值为4的容器,那么第三个容器将得到50%的 CPU 时间,原来的第一个和第二个容器分别得到37.5%和12.5的 CPU 时间。
但是需要注意,这个比例只有在 CPU 密集型任务执行的是有才有用,否则容器根本不会占用这么多 CPU 时间。
3、CPU 资源绝对限制
Linux 通过 CFS 来调度各个进程对 CPU 的使用,CFS 的默认调度周期是 100ms。在使用 Docker 的时候我们可以通过“- -cpu-period”参数设置容器进程的调度周期,以及通过“- -cpu-quota”参数设置每个调度周期内容器能使用的 CPU 时间。一般这两个参数是配合使用的。但是,需要注意的是这里的“绝对”指的是一个上限,并不是说容器一定会使用这么多 CPU 时间,如果容器的任务不是很繁重,可能使用的 CPU 时间不会达到这个上限。
注意:
这里我们仅仅是简单介绍了部分资源限制的参数,更多的参数大家可以随着学习的深入自己慢慢探索。关于资源限制的背后原理我们会在后面的教程里进一步探讨。
sudo docker inspect [nameOfContainer]
sudo docker top [nameOfContainer]
sudo docker stop [nameOfContainer]
sudo docker restart [nameOfContainer]
sudo docker pause [nameOfContainer]
sudo docker unpause [nameOfContainer]
sudo docker kill [nameOfContainer]
一些关于容器使用的总结和建议
首先,总结一下容器的生命周期吧:
接下来,根据我们学过的内容,列出一点使用容器的建议,更多的建议会随着阅读的深入进一步提出。
要在容器里面保存重要文件,因为容器应该只是一个进程,数据需要使用数据卷保存,关于数据卷的内容在下一篇文章介绍;
尽量坚持“一个容器,一个进程”的使用理念,当然,在调试阶段,可以使用exec命令为容器开启新进程。
补充一下之前没有涉及到的镜像操作:
sudo docker rmi [nameOfImage]
sudo docker history [nameOfImage]
sudo docker tag my_image:v1.0 my:v0.1
运行了上面的指令我们就得到了一个新的,和原来的镜像一模一样的镜像。
sudo docker inspect [nameOfImage]
所谓“仓库”,简单来说就是集中存放镜像的地方。
Docker 官方维护着一个公共仓库 Docker store,你可以方便的在 Docker store 寻找自己想要的镜像。Docker store 的链接在这里。
当然,你也可以在终端里面登录:
sudo docker login
输入你的用户名和密码就可以登陆了。
然后,可以使用“sudo docker search ubuntu”来搜索 Ubuntu 镜像:
如果你希望建立自己的私有仓库,也是可以的。但是本系列教程不对私有仓库做过多介绍。
本文主要介绍了容器和镜像的基本操作,下一篇教程会介绍一些关于网络和数据卷的内容。