# 六.运行和管理容器

前言

容器是基于容器技术所建立和运行的轻量级应用运行环境,它是 Docker 封装和管理应用程序或微服务的“集装箱”

# 1.容器的创建和启动

在了解容器的各项操作之前,我们再来回顾一下之前我们所提及的容器状态流转。

Docker 容器的生命周期里分为五种状态,其分别代表着:

  • Created:容器已经被创建,容器所需的相关资源已经准备就绪,但容器中的程序还未处于运行状态。
  • Running:容器正在运行,也就是容器中的应用正在运行。
  • Paused:容器已暂停,表示容器中的所有程序都处于暂停 ( 不是停止 ) 状态。
  • Stopped:容器处于停止状态,占用的资源和沙盒环境都依然存在,只是容器中的应用程序均已停止。
  • Deleted:容器已删除,相关占用的资源及存储在 Docker 中的管理信息也都已释放和移除。

# 1.1 创建容器

当我们选择好镜像以后,就可以通过 docker create 这个命令来创建容器了。

docker create nginx:1.12
34f277e22be252b51d204acbb32ce21181df86520de0c337a835de6932ca06c3
1
2

执行 docker create 后,Docker 会根据我们所给出的镜像创建容器,在控制台中会打印出 Docker 为容器所分配的容器 ID,此时容器是处于 Created 状态的。

之后我们对容器的操作可以通过这个容器 ID 或者它的缩略形式进行,但用容器 ID 操作容器就和用镜像 ID 操作镜像一样烦闷,所以我们更习惯于使用容器名来操作容器。

要使用容器名操作容器,就先得给容器命名,在创建容器时,我们可以通过 --name 这个选项来配置容器名。

docker create --name nginx nginx:1.12
1

# 1.2 启动容器

通过 docker create 创建的容器,是处于 Created 状态的,其内部的应用程序还没有启动,所以我们需要通过 docker start 命令来启动它。

docker start nginx
1

由于我们为容器指定了名称,这样的操作会更加自然,所以我们非常推荐为每个被创建的容器都进行命名。

当容器启动后,其中的应用就会运行起来,容器的几个生命周期也会绑定到了这个应用上,这个之前我们已经提及,这里就不在赘述。只要应用程序还在运行,那么容器的状态就会是 Running,除非进行一些修改容器的操作。

在 Docker 里,还允许我们通过 docker run 这个命令将 docker createdocker start 这两步操作合成为一步,进一步提高工作效率。

docker run --name nginx -d nginx:1.12
89f2b769498a50f5c35a314ab82300ce9945cbb69da9cda4b022646125db8ca7
1
2

通过 docker run 创建的容器,在创建完成之后会直接启动起来,不需要我们再使用 docker start 去启动了。

这里需要注意的一点是,通常来说我们启动容器会期望它运行在“后台”,而 docker run 在启动容器时,会采用“前台”运行这种方式,这时候我们的控制台就会衔接到容器上,不能再进行其他操作了。我们可以通过 -d--detach 这个选项告诉 Docker 在启动后将程序与控制台分离,使其进入“后台”运行。

# 2.管理容器

容器创建和启动后,除了关注应用程序是否功能正常外,我们也会关注容器的状态等内容。

通过 docker ps 这个命令,我们可以罗列出 Docker 中的容器。

docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
89f2b769498a        nginx:1.12          "nginx -g 'daemon of…"   About an hour ago   Up About an hour    80/tcp              nginx
1
2
3

默认情况下,docker ps 列出的容器是处于运行中的容器,如果要列出所有状态的容器,需要增加 -a--all 选项。

docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
425a0d3cd18b        redis:3.2           "docker-entrypoint.s…"   2 minutes ago       Created                                 redis
89f2b769498a        nginx:1.12          "nginx -g 'daemon of…"   About an hour ago   Up About an hour    80/tcp              nginx
1
2
3
4

docker ps 的结果中,我们可以看到几项关于容器的信息。其中 CONTAINER IDIMAGECREATEDNAMES 大家都比较容易理解,分别表示容器 ID,容器所基于的镜像,容器的创建时间和容器的名称。

结果中的 COMMAND 表示的是容器中主程序 ( 也就是与容器生命周期所绑定进程所关联的程序 ) 的启动命令,这条命令是在镜像内定义的,而容器的启动其实质就是启动这条命令。关于 COMMAND 的更多知识,我们在之后的 Docker 镜像制作中会更详细的解读。

结果中的 STATUS 表示容器所处的状态,其值和我们之前所谈到的状态有所区别,主要是因为这里还记录了其他的一些信息。在这里,常见的状态表示有三种:

  • Created 此时容器已创建,但还没有被启动过。
  • Up [ Time ] 这时候容器处于正在运行状态,而这里的 Time 表示容器从开始运行到查看时的时间。
  • Exited ([ Code ]) [ Time ] 容器已经结束运行,这里的 Code 表示容器结束运行时,主程序返回的程序退出码,而 Time 则表示容器结束到查看时的时间。

有些读者有疑问,既然是列出容器,应该为命令取一些带有 ls 字眼的名字,为啥会用类似 Linux 中查看进程的 ps 呢?这其实有一部分历史原因,由于容器并非真的包裹住了进程,而只是隔离了进程,进程还是允许在宿主机操作系统之上的,所以列出镜像的过程到更新是查看正在运行的进程,故而有了这样的名字。

当然,在 Docker 逐渐成熟后,命令的命名也没有原来那么随意了,已经逐渐转换为使用大家广泛认可的形式。只是 docker ps 这条命令,还保留着复古的风格。

# 2.1 停止和删除容器

要将正在运行的容器停止,我们可以使用 docker stop 命令。

docker stop nginx
1

容器停止后,其维持的文件系统沙盒环境还是存在的,内部被修改的内容也都会保留,我们可以通过 docker start 命令将这个容器再次启动。

当我们需要完全删除容器时,可以通过 docker rm 命令将容器进行删除。

docker rm nginx
1

正在运行中的容器默认情况下是不能被删除的,我们可以通过增加 -f--force 选项来让 docker rm 强制停止并删除容器,不过这种做法并不妥当。

# 2.2 随手删除容器

与其他虚拟机不同,Docker 的轻量级容器设计,讲究随用随开,随关随删。也就是说,当我们短时间内不需要使用容器时,最佳的做法是删除它而不是仅仅停止它。

有的读者会问,容器一旦删除,其内部的文件系统变动也就消失了,这样做岂不是非常麻烦。要解决这个疑惑,其根本是解决为什么我们会对容器中的文件系统做更改。我这里总结了两个对虚拟环境做更改的原因,以及在 Docker 中如何优雅的解决它们。

  • 在使用虚拟机或其他虚拟化所搭建的虚拟环境时,我们倾向于使用一个干净的系统镜像并搭建程序的运行环境,由于将这类虚拟环境制作成镜像的成本较高,耗时也非常久,所以我们对于一些细小的改动倾向于修改后保持虚拟环境不被清除即可。而在 Docker 中,打包镜像的成本是非常低的,其速度也快得惊人,所以如果我们要为程序准备一些环境或者配置,完全可以直接将它们打包至新的镜像中,下次直接使用这个新的镜像创建容器即可。
  • 容器中应用程序所产生的一些文件数据,是非常重要的,如果这些数据随着容器的删除而丢失,其损失是非常巨大的。对于这类由应用程序所产生的数据,并且需要保证它们不会随着容器的删除而消失的,我们可以使用 Docker 中的数据卷来单独存放。由于数据卷是独立于容器存在的,所以其能保证数据不会随着容器的删除而丢失。关于数据卷的具体使用,在之后的小节会专门讲解。

解决了这两个问题,大家心中的疑虑是不是就小了很多。而事实上,容器的随用随删既能保证在我们不需要它们的时候它们不会枉占很多资源,也保证了每次我们建立和启动容器时,它们都是“热乎”的崭新版本。大家都知道,系统卡就重装,而借助 Docker 秒级的容器启停特性,我们就是可以这么任性的“重装”。

# 3.进入容器

很多时间,我们需要的操作并不仅仅是按镜像所给出的命令启动容器而已,我们还会希望进一步了解容器或操作容器,这时候最佳的方式就是让我们进入到容器了。

我们知道,容器是一个隔离运行环境的东西,它里面除了镜像所规定的主进程外,其他的进程也是能够运行的,Docker 为我们提供了一个命令 docker exec 来让容器运行我们所给出的命令。

这里我们试试用容器中的 more 命令查看容器的主机名定义。

docker exec nginx more /etc/hostname
::::::::::::::
/etc/hostname
::::::::::::::
83821ea220ed
1
2
3
4
5

docker exec 命令能帮助我们在正在运行的容器中运行指定命令,这对于服务控制,运维监控等有着不错的应用场景。但是在开发过程中,我们更常使用它来作为我们进入容器的桥梁。

熟悉 Linux 的朋友们知道,我们操作 Linux 这个过程,并不是 Linux 内部的某些机能,而是通过控制台软件来完成的。控制台软件分析我们的命令,将其转化为对 Linux 的系统调用,实现了我们对 Linux 的操作。若不是这样,生涩的系统调用方法对普通开发者来说简直就是黑洞一般的存在,更别提用它们控制系统了。

在 Linux 中,大家熟悉的控制台软件应该是 Shell 和 Bash 了,它们分别由 sh 和 bash 这两个程序启动。

说到这里,有读者一定想到了,既然有这两个控制台程序,我们只要在容器里执行它们,然后通过它们去控制容器内的环境,岂不就可以“自由的飞翔”了吗。没错,这里说的进入容器,就是通过 docker exec 命令来启动 sh 或 bash,并通过它们实现对容器内的虚拟环境的控制。

由于 bash 的功能要比 sh 丰富,所以在能够使用 bash 的容器里,我们优先选择它作为控制台程序。

docker exec -it nginx bash
root@83821ea220ed:/#
1
2

在借助 docker exec 进入容器的时候,我们需要特别注意命令中的两个选项不可或缺,即 -i-t ( 它们俩可以利用简写机制合并成 -it )。

其中 -i ( --interactive ) 表示保持我们的输入流,只有使用它才能保证控制台程序能够正确识别我们的命令。而 -t ( --tty ) 表示启用一个伪终端,形成我们与 bash 的交互,如果没有它,我们无法看到 bash 内部的执行结果。

熟悉通过在容器中执行控制台程序进而进入容器这种方法,在开发过程中你能更轻松的观察容器中发生了什么,也更容易排查程序或者环境引起的问题。

# 3.1 衔接到容器

Docker 为我们提供了一个 docker attach 命令,用于将当前的输入输出流连接到指定的容器上。

docker attach nginx
1

这个命令最直观的效果可以理解为我们将容器中的主程序转为了“前台”运行 ( 与 docker run 中的 -d 选项有相反的意思 )。

由于我们的输入输出流衔接到了容器的主程序上,我们的输入输出操作也就直接针对了这个程序,而我们发送的 Linux 信号也会转移到这个程序上。例如我们可以通过 Ctrl + C 来向程序发送停止信号,让程序停止 ( 从而容器也会随之停止 )。

在实际开发中,由于 docker attach 限制较多,功能也不够强大,所以并没有太多用武之地,这里我们就一笔带过,不做详细的解读了。