Dokcer学习之旅(1)——运行一个简单的容器
阅读原文时间:2023年08月13日阅读:1

基本概念

我们都知道,操作系统分为 内核用户空间。对于 Linux 而言,内核启动后,会挂载 root 文件系统为其提供用户空间支持。而 Docker 镜像(Image),就相当于是一个 root 文件系统。比如官方镜像 ubuntu:18.04 就包含了完整的一套 Ubuntu 18.04 最小系统的 root 文件系统。
Docker 镜像 是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像 不包含 任何动态数据,其内容在构建之后也不会被改变。

因为镜像包含操作系统完整的 root 文件系统,其体积往往是庞大的,因此在 Docker 设计时,就充分利用 Union FS 的技术,将其设计为分层存储的架构。所以严格来说,镜像并非是像一个 ISO 那样的打包文件,镜像只是一个虚拟的概念,其实际体现并非由一个文件组成,而是由一组文件系统组成,或者说,由多层文件系统联合组成。
镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。
这种分层的结构具有几个重要的优势:

  1. 快速构建和重用:如果多个镜像共享相同的层,那么这些层可以被缓存并重复使用,从而加快构建过程。
  2. 更小的镜像大小:由于每个指令只添加或更改一个层,因此镜像可以被精确地构建,只包含所需的文件和更改,从而减小镜像的大小。
  3. 更容易管理:由于每个层都是独立的,可以更容易地管理和更新镜像。当应用程序的某个组件发生变化时,只需重新构建涉及该组件的层,而不需要重新构建整个镜像。

镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的 类 和 实例 一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的 命名空间。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。也因为这种隔离的特性,很多人初学 Docker 时常常会混淆容器和虚拟机。
前面讲过镜像使用的是分层存储,容器也是如此。每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为 容器存储层
容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。
按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用 数据卷(Volume)、或者 绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。
数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此,使用数据卷后,容器删除或者重新运行之后,数据却不会丢失。

镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。
一个 Docker Registry 中可以包含多个 仓库(Repository);每个仓库可以包含多个 标签(Tag);每个标签对应一个镜像。
通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签> 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。
Ubuntu 镜像 为例,ubuntu 是仓库的名字,其内包含有不同的版本标签,如,16.04, 18.04。我们可以通过 ubuntu:16.04,或者 ubuntu:18.04 来具体指定所需哪个版本的镜像。如果忽略了标签,比如 ubuntu,那将视为 ubuntu:latest。

镜像操作

现在,先让我们从0到1,逐步跑起来一个docker容器。
命令行输入docker,可以查看到docker支持的所有命令,docker --help 可以查询更多详情。

root@VM-24-8-debian:~# docker

Usage:  docker [OPTIONS] COMMAND

A self-sufficient runtime for containers

Common Commands:
  run         Create and run a new container from an image
  exec        Execute a command in a running container
  ps          List containers
  build       Build an image from a Dockerfile
  pull        Download an image from a registry
  push        Upload an image to a registry
  images      List images
  login       Log in to a registry
  logout      Log out from a registry
  search      Search Docker Hub for images
  version     Show the Docker version information
  info        Display system-wide information

Management Commands:
  builder     Manage builds
  buildx*     Docker Buildx (Docker Inc., v0.10.5)
  compose*    Docker Compose (Docker Inc., v2.18.1)
  container   Manage containers
  context     Manage contexts
  image       Manage images
  manifest    Manage Docker image manifests and manifest lists
  network     Manage networks
  plugin      Manage plugins
  system      Manage Docker
  trust       Manage trust on Docker images
  volume      Manage volumes

Swarm Commands:
  swarm       Manage Swarm

Commands:
  attach      Attach local standard input, output, and error streams to a running container
  commit      Create a new image from a container's changes
  cp          Copy files/folders between a container and the local filesystem
  create      Create a new container
  diff        Inspect changes to files or directories on a container's filesystem
  events      Get real time events from the server
  export      Export a container's filesystem as a tar archive
  history     Show the history of an image
  import      Import the contents from a tarball to create a filesystem image
  inspect     Return low-level information on Docker objects
  kill        Kill one or more running containers
  load        Load an image from a tar archive or STDIN
  logs        Fetch the logs of a container
  pause       Pause all processes within one or more containers
  port        List port mappings or a specific mapping for the container
  rename      Rename a container
  restart     Restart one or more containers
  rm          Remove one or more containers
  rmi         Remove one or more images
  save        Save one or more images to a tar archive (streamed to STDOUT by default)
  start       Start one or more stopped containers
  stats       Display a live stream of container(s) resource usage statistics
  stop        Stop one or more running containers
  tag         Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE
  top         Display the running processes of a container
  unpause     Unpause all processes within one or more containers
  update      Update configuration of one or more containers
  wait        Block until one or more containers stop, then print their exit codes

Global Options:
      --config string      Location of client config files (default "/root/.docker")
  -c, --context string     Name of the context to use to connect to the daemon (overrides DOCKER_HOST env var and default context set with "docker context use")
  -D, --debug              Enable debug mode
  -H, --host list          Daemon socket to connect to
  -l, --log-level string   Set the logging level ("debug", "info", "warn", "error", "fatal") (default "info")
      --tls                Use TLS; implied by --tlsverify
      --tlscacert string   Trust certs signed only by this CA (default "/root/.docker/ca.pem")
      --tlscert string     Path to TLS certificate file (default "/root/.docker/cert.pem")
      --tlskey string      Path to TLS key file (default "/root/.docker/key.pem")
      --tlsverify          Use TLS and verify the remote
  -v, --version            Print version information and quit

Run 'docker COMMAND --help' for more information on a command.

$ docker pull [选项][Docker Registry 地址[:端口号]/]仓库名[:标签]
例如
$ docker pull ubuntu

要想列出已经下载下来的镜像,可以使用 docker image ls 命令。

root@VM-24-8-debian:~# docker image ls
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
hello-world   latest    9c7a54a9a43c   2 months ago   13.3kB
ubuntu        latest    3b418d7b466a   2 months ago   77.8MB

列表包含了 仓库名、标签、镜像 ID、创建时间 以及 所占用的空间,镜像 ID 则是镜像的唯一标识,一个镜像可以对应多个 标签

启动容器

docker run -it --rm ubuntu bash

  • -it:这是两个参数,一个是 -i:交互式操作,一个是 -t 终端。我们这里打算进入 bash 执行一些命令并查看返回结果,因此我们需要交互式终端。
  • --rm:这个参数是说容器退出后随之将其删除。默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动 docker rm。我们这里只是随便执行个命令,看看结果,不需要排障和保留结果,因此使用 --rm 可以避免浪费空间。
  • ubuntu:18.04:这是指用 ubuntu:18.04 镜像为基础来启动容器。
  • bash:放在镜像名后的是 命令,这里我们希望有个交互式 dockerfile,因此用的是 bash

到这里,我们就很轻松的跑起来了一个最基础的容器,是不是看起来很简单

后台运行

在大部分的场景下,我们希望 docker 的服务是在后台运行的,我们可以过 -d 指定容器的运行模式。
$ docker run -itd <容器ID> /bin/bash

进入容器

在使用 -d 参数时,容器启动后会进入后台。此时想要进入容器,可以通过以下指令进入:

  • **docker attach**
  • **docker exec**:推荐大家使用 docker exec 命令,因为此命令会退出容器终端,但不会导致容器的停止。

docker exec -it <容器ID> /bin/bash

停止运行

$ docker stop <容器 ID>

重启容器

$ docker restart <容器 ID>

$ docker image rm[选项]<镜像1>[<镜像2>...]

// 如果有容器正在使用该镜像,无法正常删除
root@VM-24-8-debian:~# docker image rm 9c7a
Error response from daemon: conflict: unable to delete 9c7a54a9a43c (must be forced) - image is being used by stopped container 0440b31f2027
// 删除对应容器
root@VM-24-8-debian:~# docker ps -a
CONTAINER ID   IMAGE         COMMAND       CREATED          STATUS                      PORTS     NAMES
d28f5ef78fcb   ubuntu        "/bin/bash"   23 minutes ago   Exited (0) 23 minutes ago             distracted_feynman
0440b31f2027   hello-world   "/hello"      5 weeks ago      Exited (0) 5 weeks ago                serene_borg
root@VM-24-8-debian:~# docker rm 044
044
// 现在就可以正常删除镜像
root@VM-24-8-debian:~# docker image rm 9c7a
Untagged: hello-world:latest
Untagged: hello-world@sha256:fc6cf906cbfa013e80938cdf0bb199fbdbb86d6e3e013783e5a766f50f5dbce0
Deleted: sha256:9c7a54a9a43cca047013b82af109fe963fde787f63f9e016fdc3384500c2823d
Deleted: sha256:01bb4fce3eb1b56b05adf99504dafd31907a5aadac736e36b27595c8b92f07f1

Untagged 和 Deleted

如果观察上面这几个命令的运行输出信息的话,你会注意到删除行为分为两类,一类是 Untagged,另一类是 Deleted。我们之前介绍过,镜像的唯一标识是其 ID 和摘要,而一个镜像可以有多个标签。
因此当我们使用上面命令删除镜像的时候,实际上是在要求删除某个标签的镜像。所以首先需要做的是将满足我们要求的所有镜像标签都取消,这就是我们看到的 Untagged 的信息。因为一个镜像可以对应多个标签,因此当我们删除了所指定的标签后,可能还有别的标签指向了这个镜像,如果是这种情况,那么 Delete 行为就不会发生。所以并非所有的 docker image rm 都会产生删除镜像的行为,有可能仅仅是取消了某个标签而已。
当该镜像所有的标签都被取消了,该镜像很可能会失去了存在的意义,因此会触发删除行为。镜像是多层存储结构,因此在删除的时候也是从上层向基础层方向依次进行判断删除。镜像的多层结构让镜像复用变得非常容易,因此很有可能某个其它镜像正依赖于当前镜像的某一层。这种情况,依旧不会触发删除该层的行为。直到没有任何层依赖当前层时,才会真实的删除当前层。这就是为什么,有时候会奇怪,为什么明明没有别的标签指向这个镜像,但是它还是存在的原因,也是为什么有时候会发现所删除的层数和自己 docker pull 看到的层数不一样的原因。
除了镜像依赖以外,还需要注意的是容器对镜像的依赖。如果有用这个镜像启动的容器存在(即使容器没有运行),那么同样不可以删除这个镜像。之前讲过,容器是以镜像为基础,再加一层容器存储层,组成这样的多层存储结构去运行的。因此该镜像如果被这个容器所依赖的,那么删除必然会导致故障。如果这些容器是不需要的,应该先将它们删除,然后再来删除镜像。

tips

$ docker image rm$(docker image ls-q redis)
像其它可以承接多个实体的命令一样,可以使用 docker image ls -q 来配合使用 docker image rm,这样可以成批的删除希望删除的镜像。

导出/导入容器

例如
将容器导出到Ubuntu.tar
$ docker export 1e560fca3906 > ubuntu.tar
使用 docker import 从容器快照文件中再导入为镜像
$ docker import ubuntu.tar ubuntu:v1

运行容器

我们将在docker容器中运行一个 Python Flask 应用来运行一个web应用。-P 可以指定端口

runoob@runoob:~# docker pull training/webapp  # 载入镜像
root@VM-24-8-debian:~# docker run -d -p 6666:5000 training/webapp python app.py
131f2c32413d8ca819661c2891f1e933216d245f0c347cf281bd03f0d6775e6a

// docker开放了5000端口,映射到主机端口6666上
root@VM-24-8-debian:~# docker ps
CONTAINER ID   IMAGE             COMMAND           CREATED         STATUS          PORTS                                       NAMES
131f2c32413d   training/webapp   "python app.py"   2 seconds ago   Up 1 second     0.0.0.0:6666->5000/tcp, :::6666->5000/tcp   infallible_darwin
645ddcba1194   training/webapp   "python app.py"   4 minutes ago   Up 4 minutes    0.0.0.0:5000->5000/tcp, :::5000->5000/tcp   interesting_villani
d28f5ef78fcb   ubuntu            "/bin/bash"       2 hours ago     Up 40 minutes                                               distracted_feynman

也可以使用docker port <容器id>查询 容器端口的映射情况

查询WEB应用日志

docker logs <容器ID>

root@VM-24-8-debian:~# docker logs 13
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

查看应用程序容器的进程

我们还可以使用docker top <容器ID> 来查看容器内部运行的进程

root@VM-24-8-debian:~# docker top 13
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
root                2185308             2185287             0                   18:57               ?                   00:00:00            python app.py

小结

docker各种原理听起来可能很复杂,但其实自己动手用起来,能用到的也不算多,相对难度也没那么大。
PS:下面两个网站总结的很好,文章中抄取了部分内容
利用 commit 理解镜像构成 - Docker — 从入门到实践
Docker 容器使用 | 菜鸟教程