容器化的概念很早就有了。2013 年 Docker 引擎的出现使应用程序容器化变得更加容易。
根据 Stack Overflow 开发者调查-2020,Docker是开发者 #1 最想要的平台、#2 最喜欢的平台,以及#3 最流行的平台。
尽管 Docker 功能强大,但上手确并不容易。因此,本书将介绍从基础知识到更高层次容器化的的所有内容。读完整本书之后,你应该能够:
容器化(几乎)任何应用程序
将自定义 Docker 镜像上传到在线仓库
使用 Docker Compose 处理多个容器
熟悉 Linux 终端操作
熟悉 JavaScript(稍后的的演示项目用到了 JavaScript)
容器化和 Docker 简介
怎样安装 Docker
怎样在 macOS 里安装 Docker
怎样在 Windows 上安装 Docker
怎样在 Linux 上安装 Docker
初识 Docker - Docker 基本知识介绍
什么是容器?
什么是 Docker 镜像?
什么是仓库?
Docker 架构概述
全景图
Docker 容器操作基础知识
怎样运行容器
怎样公开端口
如何使用分离模式
怎样列表展示容器
怎样命名或者重命名一个容器
怎样停止或者杀死运行中的容器
怎样重新启动容器
怎样创建而不运行容器
怎样移除挂起的容器
怎样以交互式模式运行容器
怎样在容器里执行命令
如何处理可执行镜像
Docker 镜像操作基础知识
如何创建 Docker 镜像
如何标记 Docker 镜像
如何删除、列表展示镜像
理解 Docker 镜像的分层
怎样从源码构建 NGINX
怎样优化 Docker 镜像
拥抱 Alpine Linux
怎样创建可执行 Docker 镜像
怎样在线共享 Docker 镜像
怎样容器化 JavaScript 应用
如何编写开发 Dockerfile
如何在 Docker 中使用绑定挂载
如何在 Docker 中使用匿名卷
如何在 Docker 中执行多阶段构建
如何忽略不必要的文件
Docker 中的网络操作基础知识
Docker 网络基础
如何在 Docker 中创建用户定义的桥接网络
如何在 Docker 中将容器连接到网络
如何在 Docker 中从网络分离容器
如何删除 Docker 中的网络
如何容器化多容器 JavaScript 应用程序
如何运行数据库服务
如何在 Docker 中使用命名卷
如何从 Docker 中的容器访问日志
如何在 Docker 中创建网络并连接数据库服务
如何编写 Dockerfile
如何在正在运行的容器中执行命令
如何在 Docker 中编写管理脚本
如何使用 Docker-Compose 组合项目
Docker Compose 基础
如何在 Docker Compose 中启动服务
如何在 Docker Compose 中列表展示服务
如何在 Docker Compose 正在运行的服务中执行命令
如何访问 Docker Compose 中正在运行的服务日志
如何在 Docker Compose 中停止服务
如何在 Docker Compose 中编写全栈应用程序
结论
可以在这个仓库中找到示例项目的代码,欢迎 ⭐。
完整代码在 containerized
分支。
这本书是完全开源的,欢迎高质量的贡献。可以在这个仓库中找到完整的内容。
我通常先在本书的 GitBook 版本上进行更改和更新,然后在将其发布在 freeCodeCamp 专栏。你可以在这个链接中找到本书的最新编辑中版本。别忘了评分支持。
如果你正在寻找本书的完整稳定版本,那么 freeCodeCamp 是最好的选择。如果你有所收获,请分享给你的朋友。
不管阅读本书的哪个版本,都不要忘记留下你的意见。欢迎提出建设性的批评。
摘自 IBM,
容器化意味着封装或打包软件代码及其所有依赖项,以便它可以在任何基础架构上统一且一致地运行。
换句话说,容器化可以将软件及其所有依赖项打包在一个自包含的软件包中,这样就可以省略麻烦的配置,直接运行。
举一个现实生活的场景。假设你已经开发了一个很棒的图书管理应用程序,该应用程序可以存储所有图书的信息,还可以为别人提供图书借阅服务。
如果列出依赖项,如下所示:
Node.js
Express.js
SQLite3
理论上应该是这样。但是实际上还要搞定其他一些事情。Node.js 使用了 node-gyp
构建工具来构建原生加载项。根据官方存储库中的安装说明,此构建工具需要 Python 2 或 3 和相应的的 C/C ++ 编译器工具链。
考虑到所有这些因素,最终的依赖关系列表如下:
Node.js
Express.js
SQLite3
Python 2 or 3
C/C++ tool-chain
无论使用什么平台,安装 Python 2 或 3 都非常简单。在 Linux 上,设置 C/C ++ 工具链也非常容易,但是在 Windows 和 Mac 上,这是一项繁重的工作。
在 Windows 上,C++ 构建工具包有数 GB 之大,安装需要花费相当长的时间。在 Mac 上,可以安装庞大的 Xcode 应用程序,也可以安装小巧的 Xcode 命令行工具包。
不管安装了哪一种,它都可能会在 OS 更新时中断。实际上,该问题非常普遍,甚至连官方仓库都专门提供了 macOS Catalina 的安装说明。
这里假设你已经解决了设置依赖项的所有麻烦,并且已经准备好开始。这是否意味着现在开始就一帆风顺了?当然不是。
如果你使用 Linux 而同事使用 Windows 该怎么办?现在,必须考虑如何处理这两个不同的操作系统不一致的路径,或诸如 nginx 之类的流行技术在 Windows 上未得到很好的优化的事实,以及诸如 Redis 之类的某些技术甚至都不是针对 Windows 预先构建的。
即使你完成了整个开发,如果负责管理服务器的人员部署流程搞错了,该怎么办?
所有这些问题都可以通过以下方式解决:
在与最终部署环境匹配的隔离环境(称为容器)中开发和运行应用程序。
将你的应用程序及其所有依赖项和必要的部署配置放入一个文件(称为镜像)中。
并通过具有适当授权的任何人都可以访问的中央服务器(称为仓库)共享该镜像。
然后,你的同事就可以从仓库中下载镜像,可以在没有平台冲突的隔离环境中运行应用,甚至可以直接在服务器上进行部署,因为该镜像也可以进行生产环境配置。
这就是容器化背后的想法:将应用程序放在一个独立的程序包中,使其在各种环境中都可移植且可回溯。
现在的问题是:Docker 在这里扮演什么角色?
正如我之前讲的,容器化是一种将一切统一放入盒子中来解决软件开发过程中的问题的思想。
这个想法有很多实现。Docker 就是这样的实现。这是一个开放源代码的容器化平台,可让你对应用程序进行容器化,使用公共或私有仓库共享它们,也可以编排它们。
目前,Docker 并不是市场上唯一的容器化工具,却是最受欢迎的容器化工具。我喜欢的另一个容器化引擎是 Red Hat 开发的 Podman。其他工具,例如 Google 的 Kaniko,CoreOS 的 rkt 都很棒,但和 Docker 还是有差距。
此外,如果你想了解容器的历史,可以阅读 A Brief History of Containers: From the 1970s Till Now,它描述了该技术的很多重要节点。
Docker 的安装因使用的操作系统而异。但这整个过程都非常简单。
Docker可在 Mac、Windows 和 Linux 这三个主要平台上完美运行。在这三者中,在 Mac 上的安装过程是最简单的,因此我们从这里开始。
在 Mac 上,要做的就是跳转到官方的下载页面,然后单击_Download for Mac(stable)_按钮。
你会看到一个常规的 Apple Disk Image 文件,在该文件的内有 Docker 应用程序。所要做的就是将文件拖放到 Applications 目录中。
只需双击应用程序图标即可启动 Docker。应用程序启动后,将看到 Docker 图标出现在菜单栏上。
现在,打开终端并执行 docker --version
和 docker-compose --version
以验证是否安装成功。
在 Windows 上,步骤几乎相同,当然还需要执行一些额外的操作。安装步骤如下:
跳转到此站点,然后按照说明在 Windows 10 上安装 WSL2。
然后跳转到官方下载页面 并单击 Download for Windows(stable) 按钮。
双击下载的安装程序,然后使用默认设置进行安装。
安装完成后,从开始菜单或桌面启动 Docker Desktop。Docker 图标应显示在任务栏上。
现在,打开 Ubuntu 或从 Microsoft Store 安装的任何发行版。执行 docker --version
和 docker-compose --version
命令以确保安装成功。
也可以从常规命令提示符或 PowerShell 访问 Docker,只是我更喜欢使用 WSL2。
在 Linux 上安装 Docker 的过程有所不同,具体操作取决于你所使用的发行版,它们之间差异可能更大。但老实说,安装与其他两个平台一样容易(如果不能算更容易的话)。
Windows 或 Mac 上的 Docker Desktop 软件包是一系列工具的集合,例如Docker Engine
、Docker Compose
、Docker Dashboard
、Kubernetes
和其他一些好东西。
但是,在 Linux 上,没有得到这样的捆绑包。可以手动安装所需的所有必要工具。不同发行版的安装过程如下:
如果你使用的是 Ubuntu,则可以遵循官方文档中的在 Ubuntu 上安装 Docker 引擎部分。
对于其他发行版,官方文档中提供了 不同发行版的安装指南。
在 Debian上安装 Docker Engine
在 Fedora 上安装 Docker Engine
在 CentOS 上安装 Docker Engine
如果你使用的发行版未在文档中列出,则可以参考从二进制文件安装 Docker 引擎指南。
无论参考什么程序,都必须完成一些非常重要的 Linux 的安装后续步骤。
完成 docker 安装后,必须安装另一个名为 Docker Compose 的工具。可以参考官方文档中的 Install Docker Compose 指南。
安装完成后,打开终端并执行 docker --version
和 docker-compose --version
以确保安装成功。
尽管无论使用哪个平台,Docker 的性能都很好,但与其他平台相比,我更喜欢 Linux。在整本书中,我将使用 Ubuntu 20.10 或者 Fedora 33。
一开始就需要阐明的另一件事是,在整本书中,我不会使用任何 GUI 工具操作 Docker。
我在各个平台用过很多不错的 GUI 工具,但是介绍常见的 docker 命令是本书的主要目标之一。
已经在计算机上启动并运行了 Docker,现在该运行第一个容器了。打开终端并执行以下命令:
docker run hello-world
# Unable to find image 'hello-world:latest' locally
# latest: Pulling from library/hello-world
# 0e03bdcc26d7: Pull complete
# Digest: sha256:4cf9c47f86df71d48364001ede3a4fcd85ae80ce02ebad74156906caff5378bc
# Status: Downloaded newer image for hello-world:latest
#
# Hello from Docker!
# This message shows that your installation appears to be working correctly.
#
# To generate this message, Docker took the following steps:
# 1. The Docker client contacted the Docker daemon.
# 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
# (amd64)
# 3. The Docker daemon created a new container from that image which runs the
# executable that produces the output you are currently reading.
# 4. The Docker daemon streamed that output to the Docker client, which sent it
# to your terminal.
#
# To try something more ambitious, you can run an Ubuntu container with:
# $ docker run -it ubuntu bash
#
# Share images, automate workflows, and more with a free Docker ID:
# https://hub.docker.com/
#
# For more examples and ideas, visit:
# https://docs.docker.com/get-started/
hello-world 镜像是使用 Docker 进行最小化容器化的一个示例。它有一个从 hello.c 文件编译的程序,负责打印出终端看到的消息。
现在,在终端中,可以使用 docker ps -a
命令查看当前正在运行或过去运行的所有容器:
docker ps -a
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 128ec8ceab71 hello-world "/hello" 14 seconds ago Exited (0) 13 seconds ago exciting_chebyshev
在输出中,使用 hello-world
镜像运行了名为 exciting_chebyshev
的容器,其容器标识为 128ec8ceab71
。它已经在 Exited (0) 13 seconds ago
,其中 (0)
退出代码表示在容器运行时未发生任何错误。
现在,为了了解背后发生的事情,必须熟悉 Docker 体系结构和三个非常基本的容器化概念,如下所示:
容器
镜像
仓库
我已经按字母顺序列出了这三个概念,并且将从列表中的第一个开始介绍。
在容器化世界中,没有什么比容器的概念更基础的了。
官方 Docker resources 网站说 -
容器是应用程序层的抽象,可以将代码和依赖项打包在一起。容器不虚拟化整个物理机,仅虚拟化主机操作系统。
可以认为容器是下一代虚拟机。
就像虚拟机一样,容器是与主机系统是彼此之间完全隔离的环境。它也比传统虚拟机轻量得多,因此可以同时运行大量容器,而不会影响主机系统的性能。
容器和虚拟机实际上是虚拟化物理硬件的不同方法。两者之间的主要区别是虚拟化方式。
虚拟机通常由称为虚拟机监控器的程序创建和管理,例如 Oracle VM VirtualBox,VMware Workstation,KVM,Microsoft Hyper-V 等等。该虚拟机监控程序通常位于主机操作系统和虚拟机之间,充当通信介质。
每个虚拟机都有自己的 guest 操作系统,该操作系统与主机操作系统一样消耗资源。
在虚拟机内部运行的应用程序与 guest 操作系统进行通信,该 guest 操作系统在与虚拟机监控器进行通信,后者随后又与主机操作系统进行通信,以将必要的资源从物理基础设施分配给正在运行的应用程序。
虚拟机内部运行的应用程序与物理基础设施之间存在很长的通信链。在虚拟机内部运行的应用程序可能只拥有少量资源,因为 guest 操作系统会占用很大的开销。
与虚拟机不同,容器以更智能的方式完成虚拟化工作。在容器内部没有完整的 guest 操作系统,它只是通过容器运行时使用主机操作系统,同时保持隔离 – 就像传统的虚拟机一样。
容器运行时(即 Docker)位于容器和主机操作系统之间,而不是虚拟机监控器中。容器与容器运行时进行通信,容器运行时再与主机操作系统进行通信,以从物理基础设施中获取必要的资源。
由于消除了整个主机操作系统层,因此与传统的虚拟机相比,容器的更轻量,资源占用更少。
为了说明这一点,请看下面的代码片段:
uname -a
# Linux alpha-centauri 5.8.0-22-generic #23-Ubuntu SMP Fri Oct 9 00:34:40 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
docker run alpine uname -a
# Linux f08dbbe9199b 5.8.0-22-generic #23-Ubuntu SMP Fri Oct 9 00:34:40 UTC 2020 x86_64 Linux
在上面的代码片段中,在主机操作系统上执行了 uname -a
命令以打印出内核详细信息。然后在下一行,我在运行 Alpine Linux 的容器内执行了相同的命令。
从输出中可以看到,该容器确实正在使用主机操作系统中的内核。这证明了容器虚拟化主机操作系统而不是拥有自己的操作系统这一点。
如果你使用的是 Windows 计算机,则会发现所有容器都使用 WSL2 内核。发生这种情况是因为 WSL2 充当了 Windows 上 Docker 的后端。在 macOS 上,默认后端是在 HyperKit 虚拟机管理程序上运行的 VM。
镜像是分层的自包含文件,充当创建容器的模板。它们就像容器的冻结只读副本。镜像可以通过仓库进行共享。
过去,不同的容器引擎具有不同的镜像格式。但是后来,开放式容器计划(OCI)定义了容器镜像的标准规范,该规范被主要的容器化引擎所遵循。这意味着使用 Docker 构建的映像可以与 Podman 等其他运行时一起使用,而不会有兼容性问题。
容器只是处于运行状态的镜像。当从互联网上获取镜像并使用该镜像运行容器时,实际上是在先前的只读层之上创建了另一个临时可写层。
在本书的后续部分中,这一概念将变得更加清晰。但就目前而言,请记住,镜像是分层只读文件,其中保留着应用程序所需的状态。
已经了解了这个难题的两个非常重要的部分,即 Containers 和 Images 。最后一个是 Registry。
镜像仓库是一个集中式的位置,可以在其中上传镜像,也可以下载其他人创建的镜像。Docker Hub 是 Docker 的默认公共仓库。另一个非常流行的镜像仓库是 Red Hat 的 Quay。
在本书中,我将使用 Docker Hub 作为首选仓库。
可以免费在 Docker Hub 上共享任意数量的公共镜像。供世界各地的人们下载免费使用。可在我的个人资料(fhsinchy)页面上找到我上传的镜像。
除了 Docker Hub 或 Quay,还可以创建自己的镜像仓库来托管私有镜像。计算机中还运行着一个本地仓库,该仓库缓存从远程仓库提取的镜像。
既然已经熟悉了有关容器化和 Docker 的大多数基本概念,那么现在是时候了解 Docker 作为软件的架构了。
该引擎包括三个主要组件:
Docker 守护程序: 守护程序(dockerd
)是一个始终在后台运行并等待来自客户端的命令的进程。守护程序能够管理各种 Docker 对象。
Docker 客户端: 客户端(docker
)是一个命令行界面程序,主要负责传输用户发出的命令。
REST API: REST API 充当守护程序和客户端之间的桥梁。使用客户端发出的任何命令都将通过 API 传递,最终到达守护程序。
根据官方文档,
“ Docker 使用客户端-服务器体系结构。Docker client 与 Docker daemon 对话,daemon 繁重地构建、运行和分发 Docker 容器”。
作为用户,通常将使用客户端组件执行命令。然后,客户端使用 REST API 来访问长期运行的守护程序并完成工作。
好吧,说的够多了。现在是时候了解刚刚学习的所有这些知识如何和谐地工作了。在深入解释运行 docker run hello-world
命令时实际发生的情况之前,看一下下面的图片:
该图像是在官方文档中找到的图像的略微修改版本。执行命令时发生的事件如下:
执行 docker run hello-world
命令,其中 hello-world
是镜像的名称。
Docker 客户端访问守护程序,告诉它获取 hello-world
镜像并从中运行一个容器。
Docker 守护程序在本地仓库中查找镜像,并发现它不存在,所以在终端上打印 Unable to find image 'hello-world:latest' locally
。
然后,守护程序访问默认的公共仓库 Docker Hub,拉取 hello-world
镜像的最新副本,并在命令行中展示 Unable to find image 'hello-world:latest' locally
。
Docker 守护程序根据新拉取的镜像创建一个新容器。
最后,Docker 守护程序运行使用 hello-world
镜像创建的容器,该镜像在终端上输出文本。
Docker 守护程序的默认行为是在 hub 中查找本地不存在的镜像。但是,拉取了镜像之后,它将保留在本地缓存中。因此,如果再次执行该命令,则在输出中将看不到以下几行:
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
0e03bdcc26d7: Pull complete
Digest: sha256:d58e752213a51785838f9eed2b7a498ffa1cb3aa7f946dda11af39286c3db9a9
Status: Downloaded newer image for hello-world:latest
如果公共仓库中有可用镜像的更新版本,则守护程序将再次拉取该镜像。那个 :latest
是一个标记。镜像通常包含有意义的标记以指示版本或内部版本。稍后,将更详细地介绍这一点。
在前面的部分中,已经了解了 Docker 的构建模块,还使用 docker run
命令运行了一个容器。
在本节中,将详细介绍容器的操作。容器操作是每天要执行的最常见的任务之一,因此,正确理解各种命令至关重要。
但是请记住,这并不是可以在 Docker 上执行的所有命令的详尽列表。我只会介绍最常见的那些。当想知道某一命令的更多用法时,可以访问 Docker 命令行的官方参考。
之前,已经使用 docker run
来使用 hello-world
镜像创建和启动容器。此命令的通用语法如下:
docker run
尽管这是一个完全有效的命令,但是有一种更好的方式可以将命令分配给 docker
守护程序。
在版本 1.13
之前,Docker 仅具有前面提到的命令语法。后来,命令行经过了重构具有了以下语法:
docker
使用以下语法:
object
表示将要操作的 Docker 对象的类型。这可以是 container
、image
、network
或者 volume
对象。
command
表示守护程序要执行的任务,即 run
命令。
options
可以是任何可以覆盖命令默认行为的有效参数,例如端口映射的 --publish
选项。
现在,遵循此语法,可以将 run
命令编写如下:
docker container run
image name
可以是在线仓库或本地系统中的任何镜像。例如,可以尝试使用fhsinchy / hello-dock 镜像运行容器。该镜像包含一个简单的 Vue.js应用程序,该应用程序在容器内部的端口 80 上运行。
请在终端上执行以下命令以使用此镜像运行容器:
docker container run --publish 8080:80 fhsinchy/hello-dock
# /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
# /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
# /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
# 10-listen-on-ipv6-by-default.sh: Getting the checksum of /etc/nginx/conf.d/default.conf
# 10-listen-on-ipv6-by-default.sh: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
# /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
# /docker-entrypoint.sh: Configuration complete; ready for start up
该命令不言自明。唯一需要说明的部分是 --publish 8080:80
部分,将在下一个小节中进行说明。
容器是隔离的环境。主机系统对容器内部发生的事情一无所知。因此,从外部无法访问在容器内部运行的应用程序。
要允许从容器外部进行访问,必须将容器内的相应端口发布到本地网络上的端口。--publish
或 -p
选项的通用语法如下:
--publish :
在上一小节中编写了 --publish 8080:80
时,这意味着发送到主机系统端口 8080 的任何请求都将转发到容器内的端口 80。
现在要在浏览器上访问该应用程序,只需访问 http://127.0.0.1:8080
。
可以在终端窗口按下 ctrl + c
组合键或关闭终端窗口来停止容器。
run
命令的另一个非常流行的选项是 ---detach
或 -d
选项。在上面的示例中,为了使容器继续运行,必须将终端窗口保持打开状态。关闭终端窗口会停止正在运行的容器。
这是因为,默认情况下,容器在前台运行,并像从终端调用的任何其他普通程序一样将其自身附加到终端。
为了覆盖此行为并保持容器在后台运行,可以在 run
命令中包含 --detach
选项,如下所示:
docker container run --detach --publish 8080:80 fhsinchy/hello-dock
# 9f21cb77705810797c4b847dbd330d9c732ffddba14fb435470567a7a3f46cdc
与前面的示例不同,这次不会看到很多文字,而只获得新创建的容器的 ID。
提供选项的顺序并不重要。如果将 --publish
选项放在 --detach
选项之前,效果相同。
使用 run
命令时必须记住的一件事是镜像名称必须最后出现。如果在镜像名称后放置任何内容,则将其作为参数传递给容器入口点(在在容器内执行命令小节做了解释),可能会导致意外情况。
container ls
命令可用于列出当前正在运行的容器。执行以下命令:
docker container ls
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 9f21cb777058 fhsinchy/hello-dock "/docker-entrypoint.…" 5 seconds ago Up 5 seconds 0.0.0.0:8080->80/tcp gifted_sammet
一个名为 gifted_sammet
的容器正在运行。它是在 5 seconds ago
前创建的,状态为 Up 5 seconds
,这表明自创建以来,该容器一直运行良好。
CONTAINER ID
为 9f21cb777058
,这是完整容器 ID 的前 12 个字符。完整的容器 ID 是 9f21cb77705810797c4b847dbd330d9c732ffddba14fb435470567a7a3f46cdc
,该字符长 64 个字符。在上一节中 docker container run
命令行的输的就是完整的容器 ID 。
列表的 PORTS
列下,本地网络的端口 8080 指向容器内的端口 80。name gifted_sammet
是由 Docker 生成的,可能与你的计算机的不同。
container ls
命令仅列出系统上当前正在运行的容器。为了列出过去运行的所有容器,可以使用 --all
或 -a
选项。
docker container ls --all
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 9f21cb777058 fhsinchy/hello-dock "/docker-entrypoint.…" 2 minutes ago Up 2 minutes 0.0.0.0:8080->80/tcp gifted_sammet
# 6cf52771dde1 fhsinchy/hello-dock "/docker-entrypoint.…" 3 minutes ago Exited (0) 3 minutes ago reverent_torvalds
# 128ec8ceab71 hello-world "/hello" 4 minutes ago Exited (0) 4 minutes ago exciting_chebyshev
如你所见,列表 reverent_torvalds
中的第二个容器是较早创建的,并以状态代码 0 退出,这表明在容器运行期间未产生任何错误。
默认情况下,每个容器都有两个标识符。如下:
CONTAINER ID
- 64 个字符的随机字符串。
NAME
- 两个随机词的组合,下划线连接。
基于这两个随机标识符来引用容器非常不方便。如果可以使用自定义的名称来引用容器,那就太好了。
可以使用 --name
选项来命名容器。要使用名为 hello-dock-container
的 fhsinchy/hello-dock
镜像运行另一个容器,可以执行以下命令:
docker container run --detach --publish 8888:80 --name hello-dock-container fhsinchy/hello-dock
# b1db06e400c4c5e81a93a64d30acc1bf821bed63af36cab5cdb95d25e114f5fb
本地网络上的 8080 端口被 gifted_sammet
容器(在上一小节中创建的容器)占用了。这就是为什么必须使用其他端口号(例如 8888)的原因。要进行验证,执行 container ls
命令:
docker container ls
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# b1db06e400c4 fhsinchy/hello-dock "/docker-entrypoint.…" 28 seconds ago Up 26 seconds 0.0.0.0:8888->80/tcp hello-dock-container
# 9f21cb777058 fhsinchy/hello-dock "/docker-entrypoint.…" 4 minutes ago Up 4 minutes 0.0.0.0:8080->80/tcp gifted_sammet
一个名为 hello-dock-container
的新容器已经启动。
甚至可以使用 container rename
命令来重命名旧容器。该命令的语法如下:
docker container rename
要将 gifted_sammet
容器重命名为 hello-dock-container-2
,可以执行以下命令:
docker container rename gifted_sammet hello-dock-container-2
该命令不会产生任何输出,但是可以使用 container ls
命令来验证是否已进行更改。rename
命令不仅适用于处于运行状态的容器和还适用于处于停止状态的容器。
可以通过简单地关闭终端窗口或单击 ctrl + c
来停止在前台运行的容器。但是,不能以相同方式停止在后台运行的容器。
有两个命令可以完成此任务。第一个是 container stop
命令。该命令的通用语法如下:
docker container stop
其中 container identifier
可以是容器的 ID 或名称。
应该还记得上一节中启动的容器。它仍在后台运行。使用 docker container ls
获取该容器的标识符(在本演示中,我将使用 hello-dock-container
容器)。现在执行以下命令来停止容器:
docker container stop hello-dock-container
# hello-dock-container
如果使用 name 作为标识符,则 name 将作为输出返回。stop
命令通过发送信号SIGTERM
来正常关闭容器。如果容器在一定时间内没有停止运行,则会发出 SIGKILL
信号,该信号会立即关闭容器。
如果要发送 SIGKILL
信号而不是 SIGTERM
信号,则可以改用 container kill
命令。container kill
命令遵循与 stop
命令相同的语法。
docker container kill hello-dock-container-2
# hello-dock-container-2
当我说重启时,我指的如下是两种情况:
重新启动先前已停止或终止的容器。
重新启动正在运行的容器。
正如上一小节中学到的,停止的容器保留在系统中。如果需要,可以重新启动它们。container start
命令可用于启动任何已停止或终止的容器。该命令的语法如下:
docker container start
可以通过执行 container ls --all
命令来获取所有容器的列表,然后寻找状态为 Exited
的容器。
docker container ls --all
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# b1db06e400c4 fhsinchy/hello-dock "/docker-entrypoint.…" 3 minutes ago Exited (0) 47 seconds ago hello-dock-container
# 9f21cb777058 fhsinchy/hello-dock "/docker-entrypoint.…" 7 minutes ago Exited (137) 17 seconds ago hello-dock-container-2
# 6cf52771dde1 fhsinchy/hello-dock "/docker-entrypoint.…" 7 minutes ago Exited (0) 7 minutes ago reverent_torvalds
# 128ec8ceab71 hello-world "/hello" 9 minutes ago Exited (0) 9 minutes ago exciting_chebyshev
现在要重新启动 hello-dock-container
容器,可以执行以下命令:
docker container start hello-dock-container
# hello-dock-container
现在,可以使用 container ls
命令查看正在运行的容器列表,以确保该容器正在运行。
默认情况下,container start
命令以分离模式启动容器,并保留之前进行的端口配置。因此,如果现在访问 http://127.0.0.1:8080
,应该能够像以前一样访问 hello-dock
应用程序。
现在,在想重新启动正在运行的容器,可以使用 container restart
命令。container restart
命令遵循与 container start
命令完全相同的语法。
docker container restart hello-dock-container-2
# hello-dock-container-2
这两个命令之间的主要区别在于,container restart
命令尝试停止目标容器,然后再次启动它,而 start 命令只是启动一个已经停止的容器。
在容器停止的情况下,两个命令完全相同。但是如果容器正在运行,则必须使用container restart
命令。
到目前为止,在本节中,已经使用 container run
命令启动了容器,该命令实际上是两个单独命令的组合。这两个命令如下:
container create
命令从给定的镜像创建一个容器。
container start
命令将启动一个已经创建的容器。
现在,要使用这两个命令执行运行容器部分中显示的演示,可以执行以下操作 :
docker container create --publish 8080:80 fhsinchy/hello-dock
# 2e7ef5098bab92f4536eb9a372d9b99ed852a9a816c341127399f51a6d053856
docker container ls --all
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 2e7ef5098bab fhsinchy/hello-dock "/docker-entrypoint.…" 30 seconds ago Created hello-dock
通过 container ls --all
命令的输出可以明显看出,已经使用 fhsinchy/hello-dock
镜像创建了一个名称为 hello-dock
的容器。容器的 STATUS
目前处于 Created
状态,并且鉴于其未运行,因此不使用 --all
选项就不会列出该容器。
一旦创建了容器,就可以使用 container start
命令来启动它。
docker container start hello-dock
# hello-dock
docker container ls
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 2e7ef5098bab fhsinchy/hello-dock "/docker-entrypoint.…" About a minute ago Up 29 seconds 0.0.0.0:8080->80/tcp hello-dock
容器 STATUS
已从 Created
更改为 Up 29 seconds
,这表明容器现在处于运行状态。端口配置也显示在以前为空的 PORTS
列中。
尽管可以在大多数情况下使用 container run
命令,但本书稍后还会有一些情况要求使用 container create
命令。
如你所见,已被停止或终止的容器仍保留在系统中。这些挂起的容器可能会占用空间或与较新的容器发生冲突。
可以使用 container rm
命令删除停止的容器。通用语法如下:
docker container rm
要找出哪些容器没有运行,使用 container ls --all
命令并查找状态为 Exited
的容器。
docker container ls --all
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# b1db06e400c4 fhsinchy/hello-dock "/docker-entrypoint.…" 6 minutes ago Up About a minute 0.0.0.0:8888->80/tcp hello-dock-container
# 9f21cb777058 fhsinchy/hello-dock "/docker-entrypoint.…" 10 minutes ago Up About a minute 0.0.0.0:8080->80/tcp hello-dock-container-2
# 6cf52771dde1 fhsinchy/hello-dock "/docker-entrypoint.…" 10 minutes ago Exited (0) 10 minutes ago reverent_torvalds
# 128ec8ceab71 hello-world "/hello" 12 minutes ago Exited (0) 12 minutes ago exciting_chebyshev
从输出中可以看到,ID为 6cf52771dde1
和 128ec8ceab71
的容器未运行。要删除 6cf52771dde1
,可以执行以下命令:
docker container rm 6cf52771dde1
# 6cf52771dde1
可以使用 container ls
命令检查容器是否被删除。也可以一次删除多个容器,方法是将其标识符一个接一个地传递,每个标识符之间用空格隔开。
也可以使用 container prune
命令来一次性删除所有挂起的容器。
可以使用 container ls --all
命令检查容器列表,以确保已删除了挂起的容器:
docker container ls --all
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# b1db06e400c4 fhsinchy/hello-dock "/docker-entrypoint.…" 8 minutes ago Up 3 minutes 0.0.0.0:8888->80/tcp hello-dock-container
# 9f21cb777058 fhsinchy/hello-dock "/docker-entrypoint.…" 12 minutes ago Up 3 minutes 0.0.0.0:8080->80/tcp hello-dock-container-2
如果按照本书的顺序进行操作,则应该只在列表中看到 hello-dock-container
和 hello-dock-container-2
。建议停止并删除两个容器,然后再继续进行下一部分。
container run
和 container start
命令还有 --rm
选项,它们表示希望容器在停止后立即被移除。执行以下命令,使用 --rm
选项启动另一个 hello-dock
容器:
docker container run --rm --detach --publish 8888:80 --name hello-dock-volatile fhsinchy/hello-dock
# 0d74e14091dc6262732bee226d95702c21894678efb4043663f7911c53fb79f3
可以使用 container ls
命令来验证容器是否正在运行:
docker container ls
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 0d74e14091dc fhsinchy/hello-dock "/docker-entrypoint.…" About a minute ago Up About a minute 0.0.0.0:8888->80/tcp hello-dock-volatile
现在,如果停止了容器,使用 container ls --all
命令再次检查:
docker container stop hello-dock-volatile
# hello-dock-volatile
docker container ls --all
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
该容器已被自动删除。从现在开始,我将对大多数容器使用 --rm
选项。不需要的地方我会明确提到。
到目前为止,只运行了 hello-world 镜像或 fhsinchy/hello-dock 镜像。这些镜像用于执行非交互式的简单程序。
好吧,镜像并不是那么简单。镜像可以将整个 Linux 发行版封装在其中。
流行的发行版,例如 Ubuntu,Fedora 和 Debian 都在 hub 有官方的 Docker 镜像。编程语言,例如 python、php、[go](https:// hub.docker.com/_/golang) 或类似 node 和 deno 都有其官方镜像。
这些镜像不但仅运行某些预配置的程序。还将它们配置为默认情况下运行的 shell 程序。在镜像是操作系统的情况下,它可以是诸如 sh
或 bash
之类的东西,在竟像是编程语言或运行时的情况下,通常是它们的默认语言的 shell。
正如可能从以前的计算机中学到的一样,shell 是交互式程序。被配置为运行这样的程序的镜像是交互式镜像。这些镜像需要在 container run
命令中传递特殊的 -it
选项。
例如,如果通过执行 docker container run ubuntu
使用 ubuntu
镜像运行一个容器,将不会发生任何事情。但是,如果使用 -it
选项执行相同的命令,会直接进入到 Ubuntu 容器内的 bash 上。
docker container run --rm -it ubuntu
# root@dbb1f56b9563:/# cat /etc/os-release
# NAME="Ubuntu"
# VERSION="20.04.1 LTS (Focal Fossa)"
# ID=ubuntu
# ID_LIKE=debian
# PRETTY_NAME="Ubuntu 20.04.1 LTS"
# VERSION_ID="20.04"
# HOME_URL="https://www.ubuntu.com/"
# SUPPORT_URL="https://help.ubuntu.com/"
# BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
# PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
# VERSION_CODENAME=focal
# UBUNTU_CODENAME=focal
从 cat /etc/os-release
命令的输出中可以看到,我确实正在与在 Ubuntu 容器中运行的 bash 进行交互。
-it
选项提供了与容器内的程序进行交互的场景。此选项实际上是将两个单独的选项混在一起。
选项 -i
或 --interactive
连接到容器的输入流,以便可以将输入发送到 bash。
-t
或 --tty
选项可通过分配伪 tty 来格式化展示并提供类似本机终端的体验。
当想以交互方式运行容器时,可以使用 -it
选项。以交互式方式运行 node
镜像,如下:
docker container run -it node
# Welcome to Node.js v15.0.0.
# Type ".help" for more information.
# > ['farhan', 'hasin', 'chowdhury'].map(name => name.toUpperCase())
# [ 'FARHAN', 'HASIN', 'CHOWDHURY' ]
任何有效的 JavaScript 代码都可以在 node shell 中执行。除了输入 -it
,还可以输入 --interactive --tty
,效果一样,只不过更冗长。
在本书中初识 Docker 部分中,已经了解了在 Alpine Linux 容器内执行命令。 它是这样的:
docker run alpine uname -a
# Linux f08dbbe9199b 5.8.0-22-generic #23-Ubuntu SMP Fri Oct 9 00:34:40 UTC 2020 x86_64 Linux
在此命令中,在 Alpine Linux 容器中执行了 uname -a
命令。像这样的场景(要做的就是在特定的容器内执行特定的命令)非常常见。
假设想使用 base64
程序对字符串进行编码。几乎所有基于 Linux 或 Unix 的操作系统都可以使用此功能(但 Windows 则不可用)。
在这种情况下,可以使用 busybox 之类的镜像快速启动容器,然后执行命令。
使用 base64
编码字符串的通用语法如下:
echo -n my-secret | base64
# bXktc2VjcmV0
将命令传递到未运行的容器的通用语法如下:
docker container run
要使用 busybox 镜像执行 base64 编码,可以执行以下命令:
docker container run --rm busybox echo -n my-secret | base64
# bXktc2VjcmV0
这里发生的是,在 container run
命令中,镜像名称后传递的任何内容都将传递到镜像的默认入口里。
入口点就像是通往镜像的网关。除可执行镜像外的大多数镜像(在使用可执行镜像小节中说明)使用 shell 或 sh
作为默认入口点。因此,任何有效的 shell 命令都可以作为参数传递给它们。
在上一节中,我简要提到了可执行镜像。这些镜像旨在表现得像可执行程序。
以的 rmbyext 项目为例。这是一个简单的 Python 脚本,能够递归删除给定扩展名的文件。 要了解有关该项目的更多信息,可以查看仓库。
如果同时安装了 Git 和 Python,则可以通过执行以下命令来安装此脚本:
pip install git+https://github.com/fhsinchy/rmbyext.git#egg=rmbyext
假设的系统上已经正确设置了 Python,则该脚本应该可以在终端的任何位置使用。使用此脚本的通用语法如下:
rmbyext
要对其进行测试,请在一个空目录下打开终端,并在其中创建具有不同扩展名的一些文件。可以使用 touch
命令来做到这一点。现在,计算机上有一个包含以下文件的目录:
touch a.pdf b.pdf c.txt d.pdf e.txt
ls
# a.pdf b.pdf c.txt d.pdf e.txt
要从该目录删除所有 pdf
文件,可以执行以下命令:
rmbyext pdf
# Removing: PDF
# b.pdf
# a.pdf
# d.pdf
该程序的可执行镜像能够将文件扩展名用作参数,并像 rmbyext
程序一样删除它们。
fhsinchy/rmbyext 镜像的行为类似。该镜像包含 rmbyext
脚本的副本,并配置为在容器内的目录 /zone
上运行该脚本。
现在的问题是容器与本地系统隔离,因此在容器内运行的 rmbyext
程序无法访问本地文件系统。因此,如果可以通过某种方式将包含 pdf 文件的本地目录映射到容器内的 /zone
目录,则容器应该可以访问这些文件。
授予容器直接访问本地文件系统的一种方法是使用绑定挂载。
绑定挂载可以在本地文件系统目录(源)与容器内另一个目录(目标)之间形成双向数据绑定。这样,在目标目录中进行的任何更改都将在源目录上生效,反之亦然。
让我们看一下绑定挂载的实际应用。要使用此镜像而不是程序本身删除文件,可以执行以下命令:
docker container run --rm -v $(pwd):/zone fhsinchy/rmbyext pdf
# Removing: PDF
# b.pdf
# a.pdf
# d.pdf
已经在命令中看到了 -v $(pwd):/zone
部分,你可能已经猜到了 -v
或 --volume
选项用于为容器创建绑定挂载。该选项可以使用三个以冒号(:
)分隔的字段。该选项的通用语法如下:
--volume ::
第三个字段是可选的,但必须传递本地目录的绝对路径和容器内目录的绝对路径。
在这里,源目录是 /home/fhsinchy/the-zone
。假设终端当前在目录中,则 $(pwd)
将替换为包含先前提到的 .pdf
和 .txt
文件的 /home/fhsinchy/the-zone
。
可以在command substitution here 上了解更多信息。
--volume
或 -v
选项对 container run
以及 container create
命令均有效。我们将在接下来的部分中更详细地探讨卷,因此,如果在这里不太了解它们,请不要担心。
常规镜像和可执行镜像之间的区别在于,可执行镜像的入口点设置为自定义程序而不是 sh
,在本例中为 rmbyext
程序。正如在上一小节中所学到的那样,在 container run
命令中在镜像名称之后编写的所有内容都将传递到镜像的入口点。
所以最后,docker container run --rm -v $(pwd):/zone fhsinchy/rmbyext pdf
命令转换为容器内的 rmbyext pdf
。可执行镜像并不常见,但在某些情况下可能非常有用。