Docker 官方入门教程

84 阅读31分钟

前言

跟着官网的 Get started 溜一遍。


Part 1: Getting started

本页包含如何开始使用Docker的分步说明。在本教程中,你将学习如何:

  • 构建和运行一个作为容器的镜像

  • 使用Docker Hub分享镜像

  • 使用带有数据库的多个容器部署Docker应用程序

  • 使用Docker Compose运行应用程序

此外,你还会了解到构建镜像的最佳实践,包括如何扫描镜像的安全漏洞的说明。

开始学习教程

如果你已经运行了开始学习教程的命令,那么恭喜你!如果没有,请打开命令提示符或bash窗口,运行以下命令:

$ docker run -d -p 80:80 docker/getting-started

你会注意到有几个标志被使用。下面是关于它们的一些更多信息。

  • -d -以分离模式(在后台)运行容器

  • -p 80:80 - 将主机的80端口映射到容器的80端口上

  • docker/getting-started - 要使用的镜像

提示:

你可以结合单字符标志来缩短整个命令的长度。例如,上面的命令可以写成:

$ docker run -dp 80:80 docker/getting-started

什么是容器?

现在你已经运行了一个容器,那么什么是容器呢?简单地说,容器是你机器上的一个沙盒进程,与主机上的所有其他进程隔离。这种隔离利用了内核命名空间和 cgroup,这些功能在Linux中已经存在了很长时间。Docker已经努力使这些功能变得平易近人,易于使用。概括地说,一个容器:

  • 是一个可运行的镜像实例。你可以使用DockerAPI或CLI来创建、启动、停止、移动或删除一个容器。

  • 可以在本地机器、虚拟机上运行或部署到云端。

  • 是可移植的(可以在任何操作系统上运行)

  • 容器是相互隔离的,运行自己的软件、二进制文件和配置。

什么是容器镜像?

当运行一个容器时,它使用一个隔离的文件系统。这个自定义的文件系统是由一个容器镜像提供的。由于镜像包含了容器的文件系统,它必须包含运行一个应用程序所需的一切--所有的依赖性、配置、脚本、二进制文件等。镜像还包含容器的其他配置,如环境变量、运行的默认命令和其他元数据。

如果你熟悉 chroot ,可以把容器想象成 chroot 的扩展版本。文件系统只是来自镜像。但是,容器增加了单纯使用 chroot 时无法获得的额外隔离。

Part 2: Sample application

在本教程的其余部分,我们将使用一个在Node.js中运行的简单todo列表管理器。如果你不熟悉Node.js,不要担心。不需要真正的JavaScript经验。

在这一点上,你的开发团队相当小,你只是在建立一个应用来证明你的MVP(最小可行产品)。你想展示它是如何工作的,以及它能够做什么,而不需要考虑它将如何为一个大的团队、多个开发人员工作,等等。

todo-list-sample.png

获取应用程序

在我们运行应用程序之前,我们需要把应用程序的源代码拿到我们的机器上。对于真正的项目,你通常会克隆 repo。但是,在本教程中,我们已经创建了一个包含应用程序的ZIP文件。

  1. 下载应用程序的内容。你可以拉出整个项目,或者以压缩包的形式下载,然后将应用程序文件夹解压出来,就可以开始使用了。

  2. 解压后,使用你最喜欢的代码编辑器打开该项目。如果你需要一个编辑器,你可以使用Visual Studio Code。你应该看到package.json和两个子目录(src和spec)。

ide-screenshot.png

构建应用程序的容器镜像

为了构建应用程序,我们需要使用一个Dockerfile。Dockerfile只是一个基于文本的指令脚本,用于创建一个容器镜像。如果你以前创建过Dockerfile,你可能会在下面的Dockerfile中看到一些缺陷。但是,不要担心。我们将对它们进行讨论。

  1. 在package.json文件的同一文件夹中创建一个名为Dockerfile的文件
# syntax=docker/dockerfile:1
FROM node:12-alpine
RUN apk add --no-cache python2 g++ make
WORKDIR /app
COPY . .
RUN yarn install --production --registry=https://registry.npmmirror.com
CMD ["node", "src/index.js"]
EXPOSE 3000

国内需要切换系统软件源,需在FROM下一行加上:

RUN set -eux && sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories

请检查Dockerfile文件的扩展名是否为.txt。有些编辑器可能会自动附加这个文件扩展名,这将导致下一步的错误。

  1. 如果你还没有这样做,请打开终端,进入带有Dockerfile的应用程序目录。现在使用docker build命令构建容器镜像。
$ docker build -t getting-started .

这个命令使用Dockerfile来构建一个新的容器镜像。你可能已经注意到,有很多 "层 "被下载。这是因为我们指示构建器,我们想从node:12-alpine镜像开始。但是,由于我们的机器上没有这个,所以需要下载这个镜像。

下载完镜像后,我们复制了我们的应用程序,并使用yarn来安装我们应用程序的依赖项。CMD指令指定了从这个镜像启动容器时要运行的默认命令。

最后,-t标志对我们的镜像进行标记。可以把它看作是最终镜像的一个可读名称。由于我们给镜像命名为getting-started,我们可以在运行容器时引用该镜像。

docker build命令末尾的 '.' 告诉Docker,它应该在当前目录下寻找Docker文件。

启动一个应用程序容器

现在我们有了一个镜像,让我们来运行这个应用程序。要做到这一点,我们将使用docker run命令(还记得前面的那个吗?)

  1. 使用docker run命令启动你的容器,并指定我们刚刚创建的镜像的名称。
$ docker run -dp 3000:3000 getting-started

还记得-d和-p标志吗?我们正在以 "分离 "模式(在后台)运行新的容器,并在主机的3000端口和容器的3000端口之间创建一个映射。如果没有这个端口映射,我们就无法访问这个应用程序。

  1. 几秒钟后,打开你的网络浏览器到 http://localhost:3000。你应该看到我们的应用程序。

todo-list-empty.png 3. 继续添加一两个项目,看看它是否像你期望的那样工作。你可以将项目标记为完成,并删除项目。你的前台正在成功地将项目存储到后台。非常快速和简单,是吧?

在这一点上,你应该有一个运行中的有几个项目的todo列表管理器,都是由你建立的。现在,让我们做一些改变,学习一下管理我们的容器。

回顾一下

在这一小节中,我们学习了关于构建容器镜像的基本知识,并创建了一个Docker文件来实现这一目标。一旦我们建立了一个镜像,我们就启动了容器并看到了正在运行的应用程序。

接下来,我们将对我们的应用程序进行修改,并学习如何用一个新的镜像来更新我们正在运行的应用程序。在这个过程中,我们将学习一些其他有用的命令。

Part 3: Update the application

作为一个小的功能请求,产品团队要求我们改变当我们没有任何todo列表项目时的 "空文本"。他们希望将其改为以下内容:

You have no todo items yet! Add one above!

很简单,对吗?让我们来做这个改变。

更新源代码

  1. 在 src/static/js/app.js 文件中,更新第 56 行以使用新的空文本。
 -                <p className="text-center">No items yet! Add one above!</p>
 +                <p className="text-center">You have no todo items yet! Add one above!</p>
  1. 让我们建立我们的更新版本的镜像,使用我们之前使用的相同命令。
$ docker build -t getting-started .
  1. 让我们使用更新的代码启动一个新的容器。
$ docker run -dp 3000:3000 getting-started

啊哦! 你可能看到了这样的错误(ID将是不同的):

docker: Error response from daemon: driver failed programming external connectivity on endpoint laughing_burnell 
(bb242b2ca4d67eba76e79474fb36bb5125708ebdabd7f45c8eaf16caaabde9dd): Bind for 0.0.0.0:3000 failed: port is already allocated.

那么,发生了什么?我们不能够启动新的容器,因为我们的旧容器还在运行。这是因为该容器正在使用主机的3000端口,而机器上只有一个进程(包括容器)可以监听一个特定的端口。为了解决这个问题,我们需要删除旧的容器。

替换旧的容器

要移除一个容器,首先需要将其停止。一旦它停止了,就可以将其移除。

使用CLI删除一个容器

  1. 通过使用docker ps命令获得容器的ID。
$ docker ps
  1. 使用docker stop命令来停止该容器。
# Swap out <the-container-id> with the ID from docker ps
$ docker stop <the-container-id>
  1. 一旦容器停下来,就可以使用 docker rm 命令删除它。
$ docker rm <the-container-id>

提示:

你可以通过在docker rm命令中添加 "force "标志,在一条命令中停止和删除一个容器。例如:docker rm -f 。

启动更新的应用容器

  1. 现在,启动你更新的应用。
$ docker run -dp 3000:3000 getting-started
  1. http://localhost:3000 刷新浏览器,就能够看到更新后的帮助信息。

todo-list-updated-empty-text.png

回顾总结

虽然我们能够建立一个更新,但有两件事你可能已经注意到了:

  • 我们的待办事项列表中的所有现有项目都消失了! 这不是一个很好的应用程序! 我们很快就会谈及这个问题。

  • 对于这样一个小的变化,涉及很多步骤。在接下来的一节中,我们将讨论如何看到代码的更新,而不需要在每次做出改变时重建和启动一个新的容器。

在谈论持久性之前,我们将很快看到如何与他人分享这些镜像。

Part 4: Share the application

现在,我们已经建立了一个镜像,让我们来分享它吧! 要分享Docker镜像,你必须使用一个Docker registry。默认的 registry 是Docker Hub,我们所使用的所有镜像都来自这里。

Docker ID

Docker ID允许你访问Docker Hub,它是世界上最大的容器镜像库和社区。如果你没有Docker ID,可以免费创建一个Docker ID。

创建一个 repo

要推送一个镜像,我们首先需要在Docker Hub上创建一个仓库。

  1. 注册或登录到Docker Hub。

  2. 点击Create Repository按钮。

  3. 仓库名称使用getting-started。确保可见性是公开的。

私人仓库

Docker提供私有仓库,允许你将内容限制给特定的用户或团队。

  1. 点击创建按钮!

如果你看一下下面的图片,可以看到一个Docker命令的例子。这个命令将推送到这个 repo。

push-command.png

推送镜像

  1. 在命令行中,尝试运行你在Docker Hub上看到的推送命令。注意,你的命令将使用你的命名空间,而不是 "docker"。
$ docker push docker/getting-started
 The push refers to repository [docker.io/docker/getting-started]
 An image does not exist locally with the tag: docker/getting-started

为什么会失败?推送命令正在寻找一个名为docker/getting-started的镜像,但没有找到。如果你运行docker image ls,你也不会看到一个。

为了解决这个问题,我们需要 "标记 "我们建立的现有镜像,给它另一个名字。

  1. 使用docker login -u YOUR-USER-NAME命令登录到Docker Hub。

  2. 使用docker tag命令给已启动的镜像起一个新的名字。确保用你的Docker ID替换掉YOUR-USER-NAME。

$ docker tag getting-started YOUR-USER-NAME/getting-started

现在再试试你的推送命令。如果你是从Docker Hub复制的数值,你可以去掉 tagname 部分,因为我们没有在镜像名称中添加标签。如果你没有指定一个标签,Docker会使用一个叫做 latest 的标签。

$ docker push YOUR-USER-NAME/getting-started

在一个新的实例上运行该镜像

现在我们的镜像已经建立并推送到registry中,让我们尝试在一个从未见过这个容器镜像的全新实例上运行我们的应用吧!要做到这一点,我们将使用Play with Docker。

  1. 打开你的浏览器到Play with Docker

  2. 点击登录,然后从下拉列表中选择docker。

  3. 用你的Docker Hub账户连接。

  4. 一旦你登录了,点击左侧栏的ADD NEW INSTANCE选项。如果你没有看到它,让你的浏览器再宽一点。几秒钟后,在你的浏览器中会打开一个终端窗口。

pwd-add-new-instance.png 5. 在终端,启动你新推送的应用程序。

$ docker run -dp 3000:3000 YOUR-USER-NAME/getting-started

你应该看到镜像被拉下来,并最终启动了!

  1. 当它出现的时候,点击3000标志,你应该看到应用程序和你的修改! 万岁! 如果3000标志没有显示出来,你可以点击 "打开端口 "按钮,然后输入3000。

回顾总结

在这一节中,我们学习了如何通过将镜像推送到registry来分享我们的镜像。然后我们来到一个全新的实例,能够运行新推送的镜像。这在CI pipelines中很常见,pipeline将创建镜像并将其推送到registry,然后生产环境可以使用该镜像的最新版本。

现在我们已经搞清楚了,让我们回到上一节末尾注意到的问题。作为提醒,我们注意到,当我们重启应用程序时,我们失去了所有的todo列表项目。这显然不是一个很好的用户体验,所以让我们学习一下如何在重启时保持数据的持久性吧!

Part 5: Persist the DB

如果你没有注意到,我们每次启动容器时,我们的任务列表都会被擦拭干净。这是为什么呢?让我们深入了解一下容器是如何工作的。

容器的文件系统

当一个容器运行时,它的文件系统使用来自镜像的各个层。每个容器也有自己的 "读写空间 "来创建/更新/删除文件。任何变化都不会在另一个容器中看到,即使它们使用的是同一个镜像。

在实践中看到这一点

为了在实践中看到这一点,我们将启动两个容器并在每个容器中创建一个文件。你将看到的是,在一个容器中创建的文件在另一个容器中是不可用的。

  1. 启动一个ubuntu容器,它将创建一个名为/data.txt的文件,其中有一个1到10000之间的随机数字。
$ docker run -d ubuntu bash -c "shuf -i 1-10000 -n 1 -o /data.txt && tail -f /dev/null"

如果你对这个命令感到好奇,我们正在启动一个bash shell并调用两个命令(为什么我们有&&)。第一部分挑选一个随机数并将其写入/data.txt。第二个命令是简单地观察一个文件,以保持容器的运行。

  1. 验证我们是否可以通过执行容器来看到输出。要做到这一点,你需要获得容器的ID(用 docker ps 来获得),然后用以下命令获得内容。
$ docker exec <container-id> cat /data.txt

你应该看到一个随机的数字!

  1. 现在,让我们启动另一个ubuntu容器(同样的镜像),我们会看到我们没有相同的文件。
$ docker run -it ubuntu ls /

看看吧! 那里没有data.txt文件! 那是因为它被写入了只有第一个容器的从属空间。

  1. 继续前进,使用 docker rm -f <container-id> 命令删除第一个容器。

容器 volumes

通过前面的实验,我们看到,每个容器每次启动时都是从镜像定义开始的。虽然容器可以创建、更新和删除文件,但当容器被删除时,这些变化就会丢失,所有的变化都被隔离在该容器之外。通过卷,我们可以改变这一切。

卷提供了将容器的特定文件系统路径连接回主机的能力。如果容器中的一个目录被挂载,该目录的变化也会在主机上看到。如果我们跨容器重启时挂载同一个目录,我们会看到相同的文件。

有两种主要的卷的类型。我们最终会同时使用这两种,但我们将从命名卷开始。

保存todo数据

默认情况下,todo应用程序将其数据存储在容器的文件系统中的 /etc/todos/todo.db 的SQLite数据库中。如果你对SQLite不熟悉,不用担心!它只是一个关系型数据库,其中所有的数据都存储在一个文件中。虽然这对大规模的应用来说不是最好的,但对小的演示来说是可行的。我们将在后面讨论将其切换到不同的数据库引擎。

由于数据库是一个单一的文件,如果我们能够在主机上持久化该文件,并使其对下一个容器可用,它应该能够在上一个容器停止的地方继续工作。通过创建一个卷,并将其附加(通常称为 "挂载")到数据存储的目录上,我们就可以持久化数据。当我们的容器写到 todo.db 文件时,它将被持久化到卷中的主机。

如前所述,我们将使用一个命名卷。把一个命名的卷看作是一个数据桶。Docker维护磁盘上的物理位置,你只需要记住卷的名字。每次你使用该卷时,Docker会确保提供正确的数据。

  1. 通过使用docker volume create命令创建一个卷。
$ docker volume create todo-db
  1. 再次用 docker rm -f 停止并删除todo应用容器,因为它仍然在运行,没有使用持久化卷。

  2. 启动todo应用程序容器,但添加-v标志以指定卷挂载。我们将使用命名的卷,并将其挂载到/etc/todos,这将捕获在该路径下创建的所有文件。

$ docker run -dp 3000:3000 -v todo-db:/etc/todos getting-started
  1. 容器启动后,打开应用程序,向你的todo列表添加一些项目。

items-added.png 5. 停止并删除todo应用的容器。使用 docker ps 来获取ID,然后 docker rm -f <id> 来删除它。

  1. 使用上面的相同命令启动一个新的容器。

  2. 打开应用程序。你应该看到你的项目仍然在你的列表中!

  3. 当你检查完你的列表后,继续并删除容器。

万岁! 你现在已经学会了如何持久化数据!

注意:

虽然命名卷和绑定挂载(我们将在一分钟内讨论)是默认的Docker引擎安装所支持的两种主要类型的卷,但有许多卷驱动插件可用于支持NFS、SFTP、NetApp等!这在您开始使用Docker引擎时尤为重要。一旦你开始用Swarm、Kubernetes等在集群环境中的多个主机上运行容器,这将特别重要。

深入卷中

很多人经常问:"当我使用一个命名的卷时,Docker究竟把我的数据储存在哪里?" 如果你想知道,你可以使用docker volume inspect命令。

$ docker volume inspect todo-db
[
    {
        "CreatedAt": "2019-09-26T02:18:36Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/todo-db/_data",
        "Name": "todo-db",
        "Options": {},
        "Scope": "local"
    }
]

Mountpoint 是数据存储在磁盘上的实际位置。注意,在大多数机器上,你需要有root权限才能从主机上访问这个目录。但是,这就是它所在的位置!

在Docker桌面上直接访问卷数据

在Docker Desktop中运行时,Docker命令实际上是在你的机器上的一个小虚拟机内运行。如果你想查看 Mountpoint 目录的实际内容,你需要首先进入虚拟机内部。

回顾一下

在这一点上,我们有了一个可以在重启后继续运行的应用程序!我们可以向我们的投资者展示它,并希望他们能看到我们的愿景!

然而,我们在前面看到,为每一个变化重建镜像需要相当多的时间。一定有一个更好的方法来进行修改,对吗?有了绑定挂载(我们之前暗示过的),就有了更好的方法!现在让我们来看看这个方法吧!

Part 6: Use bind mounts

在上一章中,我们谈到并使用了命名卷来持久化我们数据库中的数据。如果我们只是想存储数据,命名卷是很好的,因为我们不必担心数据的存储位置。

通过绑定挂载,我们可以控制主机上的确切挂载点。我们可以用它来持久化数据,但它经常被用来向容器提供额外的数据。当在一个应用程序上工作时,我们可以使用绑定挂载将我们的源代码挂载到容器中,让它看到代码的变化,作出响应,并让我们立即看到变化。

对于基于Node的应用程序,nodemon是一个很好的工具,可以观察文件的变化,然后重新启动应用程序。在大多数其他语言和框架中都有相应的工具。

快速卷类型比较

绑定挂载和命名卷是Docker引擎附带的两种主要卷类型。然而,额外的卷驱动可用于支持其他使用情况(SFTP、Ceph、NetApp、S3等等)。

Named VolumesBind Mounts
Host LocationDocker choosesYou control
Mount Example (using -v)my-volume:/usr/local/data/path/to/data:/usr/local/data
Populates new volume with container contentsYesNo
Supports Volume DriversYesNo

启动一个开发模式的容器

为了运行我们的容器以支持开发工作流程,我们将做以下工作:

  • 将我们的源代码挂载到容器中

  • 安装所有的依赖项,包括 "dev "依赖项

  • 启动nodemon以观察文件系统的变化

那么,让我们开始行动吧!

  1. 确保你没有任何先前的启动容器在运行。

  2. 在app目录下运行以下命令。我们会在之后解释发生了什么。

$ docker run -dp 3000:3000 \
     -w /app -v "$(pwd):/app" \
     node:12-alpine \
     sh -c "yarn install && yarn run dev"

如果你使用的是Windows,那么在PowerShell中使用这个命令。

 PS> docker run -dp 3000:3000 `
     -w /app -v "$(pwd):/app" `
     node:12-alpine `
     sh -c "yarn install && yarn run dev"

如果你使用的是Apple silicon Mac或其他ARM64设备,那么请使用以下命令。

$ docker run -dp 3000:3000 \
     -w /app -v "$(pwd):/app" \
     node:12-alpine \
     sh -c "apk add --no-cache python2 g++ make && yarn install && yarn run dev"
  • -dp 3000:3000 -与之前相同。在分离(后台)模式下运行,并创建一个端口映射

  • -w /app - 设置 "工作目录 "或当前目录,命令将从该目录运行

  • -v "$(pwd):/app" - 绑定挂载主机的当前目录到容器中的 /app 目录中

  • node:12-alpine - 要使用的镜像。注意,这是我们的应用程序的基础镜像,来自Dockerfile

  • sh -c "yarn install && yarn run dev" - 命令。我们用 sh 启动一个shell(alpine没有bash),运行 yarn install 来安装所有的依赖项,然后运行 yarn run dev 。如果我们查看 package.json ,我们会看到 dev 脚本正在启动nodemon

  1. 你可以使用 docker logs 观察日志。当你看到这个时,你就知道你已经准备好了。
$ docker logs -f <container-id>
 nodemon src/index.js
 [nodemon] 1.19.2
 [nodemon] to restart at any time, enter `rs`
 [nodemon] watching dir(s): *.*
 [nodemon] starting `node src/index.js`
 Using sqlite database at /etc/todos/todo.db
 Listening on port 3000

当你看完日志后,按 Ctrl + C 退出。

  1. 现在,让我们对应用程序做一个改变。在 src/static/js/app.js 文件中,让我们把 "添加项目 "按钮改为简单的 "添加"。这一改动将在第109行:
 -                         {submitting ? 'Adding...' : 'Add Item'}
 +                         {submitting ? 'Adding...' : 'Add'}
  1. 只需刷新页面(或打开它),你就应该看到这个变化几乎立即反映在浏览器中。Node服务器可能需要几秒钟的时间来重新启动,所以如果你得到一个错误,只需在几秒钟后尝试刷新。

updated-add-button.png 6. 随意做任何你想做的其他改变。当你完成后,停止容器,用以下方法构建你的新镜像:

$ docker build -t getting-started .

使用绑定挂载对于本地开发设置来说是非常常见的。它的好处是,开发机器不需要安装所有的构建工具和环境。只需一条docker run命令,开发环境就会被拉动并准备就绪。我们将在未来的步骤中谈论Docker Compose,因为这将有助于简化我们的命令(我们已经得到了很多标志)。

回顾总结

在这一点上,我们可以坚持我们的数据库,并对我们的投资者和创始人的需求和要求作出快速反应。万岁! 但是,你猜怎么着?我们收到了好消息!

你的项目已经被选中用于未来的发展!

为了准备生产,我们需要将我们的数据库从在SQLite中工作迁移到可以更好地扩展的东西。为了简单起见,我们将继续使用关系型数据库,并将我们的应用程序转换为使用MySQL。但是,我们应该如何运行MySQL?我们如何让容器之间相互对话?我们接下来会讨论这个问题!

Part 7: Multi-container apps

到目前为止,我们一直在使用单容器应用程序。但是,我们现在想把MySQL添加到应用栈中。下面的问题经常出现 - "MySQL将在哪里运行?把它安装在同一个容器中还是单独运行?" 一般来说,每个容器应该做一件事,而且要做得好。 有几个原因:

  • 有一个很好的机会,你必须以不同于数据库的方式扩展API和前端程序

  • 独立的容器可以让你在隔离的情况下进行版本和更新。

  • 虽然你可以在本地使用数据库的容器,但你可能想在生产中使用数据库的管理服务。你不想把你的数据库引擎和你的应用程序一起运送。

  • 运行多个进程将需要一个进程管理器(容器只启动一个进程),这增加了容器启动/关闭的复杂性

还有更多的原因。因此,我们将更新我们的应用程序,使之像这样工作。

multi-app-architecture.png

容器网络

请记住,默认情况下,容器是孤立运行的,对同一台机器上的其他进程或容器一无所知。那么,我们如何让一个容器与另一个容器对话呢?答案是联网。现在,你不需要成为一个网络工程师(万岁!)。只要记住这个规则...

注意:

如果两个容器在同一个网络上,它们可以互相交谈。如果它们不在一起,它们就不能。

启动MySQL

有两种方法可以把一个容器放在网络上。1)在启动时分配它,或者2)连接一个现有的容器。现在,我们将首先创建网络,并在启动时附加MySQL容器。

  1. 创建网络。
$ docker network create todo-app
  1. 启动一个MySQL容器并将其附加到网络上。我们还将定义一些环境变量,数据库将使用这些变量来初始化数据库(见MySQL Docker Hub列表中的 "环境变量 "部分)。
$ docker run -d \
     --network todo-app --network-alias mysql \
     -v todo-mysql-data:/var/lib/mysql \
     -e MYSQL_ROOT_PASSWORD=secret \
     -e MYSQL_DATABASE=todos \
     mysql:5.7

如果你使用的是基于ARM的芯片,例如Macbook M1 Chips / Apple Silicon,那么使用这个命令。

$ docker run -d \
     --network todo-app --network-alias mysql \
     --platform "linux/amd64" \
     -v todo-mysql-data:/var/lib/mysql \
     -e MYSQL_ROOT_PASSWORD=secret \
     -e MYSQL_DATABASE=todos \
     mysql:5.7

如果你使用的是Windows,那么在PowerShell中使用这个命令。

 PS> docker run -d `
     --network todo-app --network-alias mysql `
     -v todo-mysql-data:/var/lib/mysql `
     -e MYSQL_ROOT_PASSWORD=secret `
     -e MYSQL_DATABASE=todos `
     mysql:5.7

你还会看到我们指定了 --network-alias 标志。我们稍后再来讨论这个问题。

提示!

你会注意到我们在这里使用了一个名为todo-mysql-data的卷,并将其挂载到/var/lib/mysql,这就是MySQL存储数据的地方。然而,我们从未运行过docker卷创建命令。Docker会识别出我们想要使用一个命名的卷,并自动为我们创建一个。

  1. 为了确认我们已经启动并运行了数据库,连接到数据库并验证其连接。
$ docker exec -it <mysql-container-id> mysql -u root -p

当密码提示出现时,键入secret。在MySQL shell中,列出数据库并确认你看到了 todos 数据库。

mysql> SHOW DATABASES;

你应该看到像这样的输出:

 +--------------------+
 | Database           |
 +--------------------+
 | information_schema |
 | mysql              |
 | performance_schema |
 | sys                |
 | todos              |
 +--------------------+
 5 rows in set (0.00 sec)

退出MySQL shell,返回到我们机器上的shell。

mysql> exit

好啊! 我们有了我们的 todos 数据库,它已经准备好供我们使用了!

连接到MySQL

现在我们知道MySQL已经启动并运行了,让我们来使用它吧!但是,问题是......如何使用?如果我们在同一个网络上运行另一个容器,我们如何找到这个容器(记住每个容器有自己的IP地址)?

为了弄清楚这个问题,我们将利用 nicolaka/netshoot 容器,它带有很多工具,对排除故障或调试网络问题很有用。

  1. 使用 nicolaka/netshoot 镜像启动一个新容器。确保将它连接到同一个网络。
$ docker run -it --network todo-app nicolaka/netshoot
  1. 在容器中,我们将使用dig命令,这是一个有用的DNS工具。我们将查找主机名mysql的IP地址。
$ dig mysql

你会得到这样的输出...

 ; <<>> DiG 9.14.1 <<>> mysql
 ;; global options: +cmd
 ;; Got answer:
 ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32162
 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

 ;; QUESTION SECTION:
 ;mysql.                IN    A

 ;; ANSWER SECTION:
 mysql.            600    IN    A    172.23.0.2

 ;; Query time: 0 msec
 ;; SERVER: 127.0.0.11#53(127.0.0.11)
 ;; WHEN: Tue Oct 01 23:47:24 UTC 2019
 ;; MSG SIZE  rcvd: 44

在 "ANSWER SECTION",你会看到 mysqlA 记录,它解析到172.23.0.2(你的IP地址很可能会有不同的值)。虽然 mysql 通常不是一个有效的主机名,但Docker能够将其解析为具有该网络别名的容器的IP地址(还记得我们之前使用的 --network-alias 标志吗)。

这意味着......我们的应用程序只需要连接到一个名为 mysql 的主机,它就可以与数据库对话了 没有比这更简单的了!

用MySQL运行你的应用程序

todo应用程序支持设置一些环境变量来指定MySQL连接设置。它们是:

  • MYSQL_HOST - 运行中的MySQL服务器的主机名

  • MYSQL_USER - 连接时要使用的用户名

  • MYSQL_PASSWORD - 连接时使用的密码

  • MYSQL_DB - 连接后要使用的数据库

通过环境变量设置连接设置

虽然使用env vars来设置连接设置通常在开发中是可以的,但在生产中运行应用程序时,这是非常不可取的。Docker的前安全主管Diogo Monica写了一篇很棒的博文,解释了原因。

一个更安全的机制是使用你的容器协调框架提供的加密支持。在大多数情况下,这些密码是作为文件挂载在运行的容器中。你会看到许多应用(包括MySQL镜像和todo应用)也支持带有 _FILE后 缀的env vars,以指向包含该变量的文件。

作为一个例子,设置 MYSQL_PASSWORD_FILE 变量将导致应用程序使用引用文件的内容作为连接密码。Docker没有做任何事情来支持这些环境变量。你的应用程序将需要知道如何寻找这个变量并获得文件内容。

在解释了所有这些之后,让我们开始我们的开发就绪的容器吧!

  1. 注意:对于8.0及以上版本的MySQL,确保在mysql中包含以下命令。
 mysql> ALTER USER 'root' IDENTIFIED WITH mysql_native_password BY 'secret';
 mysql> flush privileges;
  1. 我们将指定上面的每个环境变量,以及将容器连接到我们的应用网络。
$ docker run -dp 3000:3000 \
   -w /app -v "$(pwd):/app" \
   --network todo-app \
   -e MYSQL_HOST=mysql \
   -e MYSQL_USER=root \
   -e MYSQL_PASSWORD=secret \
   -e MYSQL_DB=todos \
   node:12-alpine \
   sh -c "yarn install && yarn run dev"

如果你使用的是Windows,那么在PowerShell中使用这个命令。

 PS> docker run -dp 3000:3000 `
   -w /app -v "$(pwd):/app" `
   --network todo-app `
   -e MYSQL_HOST=mysql `
   -e MYSQL_USER=root `
   -e MYSQL_PASSWORD=secret `
   -e MYSQL_DB=todos `
   node:12-alpine `
   sh -c "yarn install && yarn run dev"
  1. 如果我们看一下容器的日志( docker logs <container-id> ),我们应该看到一条信息,表明它正在使用mysql数据库。
$ nodemon src/index.js
 [nodemon] 1.19.2
 [nodemon] to restart at any time, enter `rs`
 [nodemon] watching dir(s): *.*
 [nodemon] starting `node src/index.js`
 Connected to mysql db at host mysql
 Listening on port 3000
  1. 在浏览器中打开应用程序,在你的todo列表中添加一些项目。

  2. 连接到mysql数据库,证明这些项目正在被写入数据库。记住,密码是secret

$ docker exec -it <mysql-container-id> mysql -p todos

并在mysql shell中,运行以下程序:

mysql>  select * from todo_items;
 +--------------------------------------+--------------------+-----------+
 | id                                   | name               | completed |
 +--------------------------------------+--------------------+-----------+
 | c906ff08-60e6-44e6-8f49-ed56a0853e85 | Do amazing things! |         0 |
 | 2912a79e-8486-4bc3-a4c5-460793a575ab | Be awesome!        |         0 |
 +--------------------------------------+--------------------+-----------+

很明显,你的表会看起来不同,因为它有你的项目。但是,你应该看到它们存储在那里了。

回顾一下

在这一点上,我们有一个应用程序,它现在将其数据存储在一个外部数据库中,在一个单独的容器中运行。我们学习了一点关于容器网络的知识,看到了如何使用DNS进行服务发现。

但是,你很有可能开始对启动这个应用程序所需要做的一切感到有点不知所措。我们必须创建一个网络,启动容器,指定所有的环境变量,暴露端口,还有更多!这需要记住很多东西,而且这肯定会使事情更难传给别人。

在下一节,我们将谈论Docker Compose。有了Docker Compose,我们可以用更简单的方式分享我们的应用堆栈,让别人用一个(简单的)命令就能把它们启动起来!

Part 8: Use Docker Compose

Docker Compose是一个帮助定义和分享多容器应用程序的工具。使用Compose,我们可以创建一个 YAML 文件来定义服务,只需一个命令,就可以将所有的东西启动或关闭。

使用Compose的最大优势是你可以在一个文件中定义你的应用堆栈,把它放在你的项目 repo的根部(现在是版本控制),并且很容易让其他人对你的项目做出贡献。别人只需要克隆你的 repo 并启动 compose 应用程序。事实上,你可能会看到GitHub/GitLab上有不少项目现在正是这样做的。

那么,我们该如何开始呢?

安装Docker Compose

如果你安装了Windows或Mac的Docker Desktop/Toolbox,你就已经有了Docker Compose! Play-with-Docker实例也已经安装了Docker Compose。如果你是在Linux机器上,你将需要安装Docker Compose。

安装后,你应该能够运行以下内容并看到版本信息。

$ docker-compose version

创建Compose文件

  1. 在应用项目的根部,创建一个名为 docker-compose.yml 的文件。

  2. 在compose文件中,我们将首先定义模式的版本。在大多数情况下,最好使用最新的支持版本。你可以查看Compose文件的参考资料,了解当前的模式版本和兼容性矩阵。

 version: "3.7"
  1. 接下来,我们将定义我们希望作为应用程序的一部分运行的服务(或容器)的列表。
 version: "3.7"

 services:

而现在,我们将开始一次迁移一个服务到compose文件中。

定义应用程序服务

要记住,这是我们用来定义我们的应用容器的命令。

$ docker run -dp 3000:3000 \
  -w /app -v "$(pwd):/app" \
  --network todo-app \
  -e MYSQL_HOST=mysql \
  -e MYSQL_USER=root \
  -e MYSQL_PASSWORD=secret \
  -e MYSQL_DB=todos \
  node:12-alpine \
  sh -c "yarn install && yarn run dev"

如果你使用的是PowerShell,那么就使用这个命令:

PS> docker run -dp 3000:3000 `
  -w /app -v "$(pwd):/app" `
  --network todo-app `
  -e MYSQL_HOST=mysql `
  -e MYSQL_USER=root `
  -e MYSQL_PASSWORD=secret `
  -e MYSQL_DB=todos `
  node:12-alpine `
  sh -c "yarn install && yarn run dev"
  1. 首先,让我们定义服务条目和容器的镜像。我们可以为这个服务选择任何名字。这个名字将自动成为一个网络别名,这在定义我们的MySQL服务时将很有用。
 version: "3.7"

 services:
   app:
     image: node:12-alpine
  1. 通常情况下,你会看到 command 接近于 image 的定义,尽管对排序没有要求。所以,让我们继续前进,把它移到我们的文件中。
 version: "3.7"

 services:
   app:
     image: node:12-alpine
     command: sh -c "yarn install && yarn run dev"
  1. 让我们通过定义服务的 ports 来迁移命令中的 -p 3000:3000 部分。我们将在这里使用简短的语法,但也有一个更粗略的长语法可用。
 version: "3.7"

 services:
   app:
     image: node:12-alpine
     command: sh -c "yarn install && yarn run dev"
     ports:
       - 3000:3000
  1. 接下来,我们将通过使用 working_dirvolumes 的定义来迁移工作目录(-w /app)和卷映射(-v "$(pwd):/app")。卷的语法也有长短之分。

Docker Compose卷定义的一个优点是我们可以使用来自当前目录的相对路径。

 version: "3.7"

 services:
   app:
     image: node:12-alpine
     command: sh -c "yarn install && yarn run dev"
     ports:
       - 3000:3000
     working_dir: /app
     volumes:
       - ./:/app
  1. 最后,我们需要使用 environment 关键词来迁移环境变量的定义。
 version: "3.7"

 services:
   app:
     image: node:12-alpine
     command: sh -c "yarn install && yarn run dev"
     ports:
       - 3000:3000
     working_dir: /app
     volumes:
       - ./:/app
     environment:
       MYSQL_HOST: mysql
       MYSQL_USER: root
       MYSQL_PASSWORD: secret
       MYSQL_DB: todos

定义MySQL服务

现在,是时候定义MySQL服务了。我们为该容器使用的命令如下:

$ docker run -d \
  --network todo-app --network-alias mysql \
  -v todo-mysql-data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=secret \
  -e MYSQL_DATABASE=todos \
  mysql:5.7

如果你使用的是PowerShell,那么使用这个命令:

PS> docker run -d `
  --network todo-app --network-alias mysql `
  -v todo-mysql-data:/var/lib/mysql `
  -e MYSQL_ROOT_PASSWORD=secret `
  -e MYSQL_DATABASE=todos `
  mysql:5.7
  1. 我们将首先定义新的服务并将其命名为 mysql ,这样它就会自动获得网络别名。我们将继续前进并指定要使用的镜像。
 version: "3.7"

 services:
   app:
     # The app service definition
   mysql:
     image: mysql:5.7
  1. 接下来,我们将定义卷的映射。当我们用 docker run 运行容器时,命名的卷会自动创建。然而,在使用Compose运行时,这不会发生。我们需要在顶层的 volumes: 中定义卷:条目,然后在服务配置中指定挂载点。仅仅提供卷的名称,就会使用默认的选项。不过,还有很多可用的选项。
 version: "3.7"

 services:
   app:
     # The app service definition
   mysql:
     image: mysql:5.7
     volumes:
       - todo-mysql-data:/var/lib/mysql

 volumes:
   todo-mysql-data:
  1. 最后,我们只需要指定环境变量。
 version: "3.7"

 services:
   app:
     # The app service definition
   mysql:
     image: mysql:5.7
     volumes:
       - todo-mysql-data:/var/lib/mysql
     environment:
       MYSQL_ROOT_PASSWORD: secret
       MYSQL_DATABASE: todos

 volumes:
   todo-mysql-data:

在这一点上,我们完整的 docker-compose.yml 应该看起来像这样。

version: "3.7"

services:
  app:
    image: node:12-alpine
    command: sh -c "yarn install && yarn run dev"
    ports:
      - 3000:3000
    working_dir: /app
    volumes:
      - ./:/app
    environment:
      MYSQL_HOST: mysql
      MYSQL_USER: root
      MYSQL_PASSWORD: secret
      MYSQL_DB: todos

  mysql:
    image: mysql:5.7
    volumes:
      - todo-mysql-data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: todos

volumes:
  todo-mysql-data:

运行应用程序栈

现在我们有了 docker-compose.yml 文件,我们可以启动它了!

  1. 首先确保没有其他应用程序/数据库的副本正在运行(docker psdocker rm -f <ids>)。

  2. 使用 docker-compose up 命令启动应用程序栈。我们将添加 -d 标志以在后台运行一切。

$ docker-compose up -d

当我们运行它时,我们应该看到像这样的输出:

 Creating network "app_default" with the default driver
 Creating volume "app_todo-mysql-data" with default driver
 Creating app_app_1   ... done
 Creating app_mysql_1 ... done

你会注意到,卷和网络都被创建了。默认情况下,Docker Compose会自动为应用程序栈创建一个网络(这就是为什么我们没有在compose文件中定义一个网络)。

  1. 让我们用 docker-compose logs -f 命令来看看日志。你会看到每个服务的日志都交错成一个流。当你想观察与时间有关的问题时,这非常有用。-f 标志是 "跟随 "日志的,所以会在日志生成时给你实时输出。

如果你已经运行了这个命令,你会看到像这样的输出:

 mysql_1  | 2019-10-03T03:07:16.083639Z 0 [Note] mysqld: ready for connections.
 mysql_1  | Version: '5.7.27'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)
 app_1    | Connected to mysql db at host mysql
 app_1    | Listening on port 3000

服务名称显示在行的开头(通常是彩色的),以帮助区分信息。如果你想查看特定服务的日志,你可以在日志命令的末尾添加服务名称(例如:docker-compose logs -f app)。

提示:在启动应用之前等待DB的到来

当应用程序启动时,它实际上是坐在那里等待MySQL的启动和准备,然后再尝试连接到它。Docker没有任何内置支持来等待另一个容器完全启动、运行并准备好,然后再启动另一个容器。对于基于Node的项目,你可以使用 wait-port依赖。其他语言/框架也有类似的项目。

  1. 在这一点上,你应该能够打开你的应用程序并看到它正在运行。而且,嘿! 我们现在只剩下一个命令了!

把它全部拆掉

当你准备把它全部拆掉时,只需运行 docker-compose down。容器将停止,网络也将被移除。

警告

移除卷

默认情况下,运行 docker-compose down 时,compose文件中命名的卷不会被删除。如果你想移除这些卷,你需要添加 --volumes 标志。

当你删除应用栈时,Docker Dashboard不会删除卷。

一旦拆掉,你可以切换到另一个项目,运行 docker-compose up ,并准备好为该项目做贡献! 这真的没有比这更简单的了!

回顾总结

在本节中,我们了解了Docker Compose,以及它如何帮助我们大幅简化多服务应用程序的定义和共享。我们通过将我们正在使用的命令翻译成适当的compose格式来创建一个Compose文件。

在这一点上,我们开始总结本教程了。然而,有一些关于镜像构建的最佳实践我们想介绍一下,因为我们一直在使用的Docker文件有一个很大的问题。所以,让我们来看看!

Part 9: Image-building best practices

安全扫描

当你建立了一个镜像,使用docker扫描命令扫描它的安全漏洞是一个好的做法。Docker已经与Snyk合作,提供漏洞扫描服务。

注意事项

你必须登录到Docker Hub才能扫描你的镜像。运行命令docker scan --login,然后用docker scan 扫描你的镜像。

例如,要扫描您在本教程中早先创建的getting-started镜像,您可以直接输入

$ docker scan getting-started

扫描使用一个不断更新的漏洞数据库,所以你看到的输出将随着新漏洞的发现而变化,但它可能看起来像这样。

✗ Low severity vulnerability found in freetype/freetype
  Description: CVE-2020-15999
  Info: https://snyk.io/vuln/SNYK-ALPINE310-FREETYPE-1019641
  Introduced through: freetype/freetype@2.10.0-r0, gd/libgd@2.2.5-r2
  From: freetype/freetype@2.10.0-r0
  From: gd/libgd@2.2.5-r2 > freetype/freetype@2.10.0-r0
  Fixed in: 2.10.0-r1

✗ Medium severity vulnerability found in libxml2/libxml2
  Description: Out-of-bounds Read
  Info: https://snyk.io/vuln/SNYK-ALPINE310-LIBXML2-674791
  Introduced through: libxml2/libxml2@2.9.9-r3, libxslt/libxslt@1.1.33-r3, nginx-module-xslt/nginx-module-xslt@1.17.9-r1
  From: libxml2/libxml2@2.9.9-r3
  From: libxslt/libxslt@1.1.33-r3 > libxml2/libxml2@2.9.9-r3
  From: nginx-module-xslt/nginx-module-xslt@1.17.9-r1 > libxml2/libxml2@2.9.9-r3
  Fixed in: 2.9.9-r4

输出中列出了漏洞的类型,一个可以了解更多信息的URL,重要的是哪个版本的相关库修复了这个漏洞。

还有其他几个选项,你可以在docker扫描文档中了解。

除了在命令行上扫描新建立的镜像,你还可以配置Docker Hub自动扫描所有新推送的镜像,然后你可以在Docker Hub和Docker Desktop上看到结果。

hvs.png

镜像分层

你知道吗,你可以看看是什么构成了一个镜像?使用 docker image history 命令,你可以看到用于创建镜像中每一层的命令。

  1. 使用 docker image history 命令查看你在本教程前面创建的 getting-started 镜像中的层。
$ docker image history getting-started

你应该得到类似这样的输出(日期/IDs可能不同)。

 IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
 a78a40cbf866        18 seconds ago      /bin/sh -c #(nop)  CMD ["node" "src/index.j…    0B                  
 f1d1808565d6        19 seconds ago      /bin/sh -c yarn install --production            85.4MB              
 a2c054d14948        36 seconds ago      /bin/sh -c #(nop) COPY dir:5dc710ad87c789593…   198kB               
 9577ae713121        37 seconds ago      /bin/sh -c #(nop) WORKDIR /app                  0B                  
 b95baba1cfdb        13 days ago         /bin/sh -c #(nop)  CMD ["node"]                 0B                  
 <missing>           13 days ago         /bin/sh -c #(nop)  ENTRYPOINT ["docker-entry…   0B                  
 <missing>           13 days ago         /bin/sh -c #(nop) COPY file:238737301d473041…   116B                
 <missing>           13 days ago         /bin/sh -c apk add --no-cache --virtual .bui…   5.35MB              
 <missing>           13 days ago         /bin/sh -c #(nop)  ENV YARN_VERSION=1.21.1      0B                  
 <missing>           13 days ago         /bin/sh -c addgroup -g 1000 node     && addu…   74.3MB              
 <missing>           13 days ago         /bin/sh -c #(nop)  ENV NODE_VERSION=12.14.1     0B                  
 <missing>           13 days ago         /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B                  
 <missing>           13 days ago         /bin/sh -c #(nop) ADD file:e69d441d729412d24…   5.59MB 

每条线代表镜像中的一个层。这里显示的是底部的基础,顶部是最新的层。利用这一点,你也可以迅速看到每个层的大小,有助于诊断大的镜像。

你会注意到有几行是截断的。如果你加上 --no-trunc 标志,你会得到完整的输出(是的......有趣的是你用一个截断的标志来得到不截断的输出,嗯?)

层缓存

现在你已经看到了分层的作用,有一个重要的教训需要学习,以帮助减少你的容器镜像的构建时间。

一旦一个层发生变化,所有下游的层也必须重新创建

让我们再看一下我们使用的Docker文件...

# syntax=docker/dockerfile:1
FROM node:12-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]

回到镜像的历史输出,我们看到 Dockerfile 中的每条命令都成为镜像中的一个新层。你可能还记得,当我们对镜像进行修改时,yarn的依赖关系必须重新安装。有什么办法可以解决这个问题吗?每次构建时都围绕着相同的依赖关系进行运输没有什么意义,对吗?

为了解决这个问题,我们需要重组我们的Docker文件,以帮助支持对依赖关系的缓存。对于基于Node的应用程序来说,这些依赖性是在 package.json 文件中定义的。那么,如果我们先只复制该文件,安装依赖关系,然后再复制其他东西,会怎么样呢?然后,我们只在 package.json 文件有变化的情况下重新创建yarn的依赖关系。有意义吗?

  1. 更新Dockerfile,先复制 package.json ,安装依赖关系,然后再复制其他东西进去。
 # syntax=docker/dockerfile:1
 FROM node:12-alpine
 WORKDIR /app
 COPY package.json yarn.lock ./
 RUN yarn install --production
 COPY . .
 CMD ["node", "src/index.js"]
  1. 在与 Dockerfile 相同的文件夹中创建一个名为 .dockerignore 的文件,内容如下。
 node_modules

.dockerignore 文件是一种简单的方法,可以有选择地只复制与镜像相关的文件。你可以在这里阅读更多关于这个的信息。在这种情况下,node_modules 文件夹应该在第二个COPY 步骤中被省略,因为否则,它可能会覆盖由 RUN 步骤中的命令创建的文件。关于为什么对Node.js应用程序推荐这样做以及其他最佳做法的进一步细节,请看他们的Node.js Web应用程序Docker化指南

  1. 使用 docker build 构建一个新的镜像。
$ docker build -t getting-started .

你应该看到这样的输出...

 Sending build context to Docker daemon  219.1kB
 Step 1/6 : FROM node:12-alpine
 ---> b0dc3a5e5e9e
 Step 2/6 : WORKDIR /app
 ---> Using cache
 ---> 9577ae713121
 Step 3/6 : COPY package.json yarn.lock ./
 ---> bd5306f49fc8
 Step 4/6 : RUN yarn install --production
 ---> Running in d53a06c9e4c2
 yarn install v1.17.3
 [1/4] Resolving packages...
 [2/4] Fetching packages...
 info fsevents@1.2.9: The platform "linux" is incompatible with this module.
 info "fsevents@1.2.9" is an optional dependency and failed compatibility check. Excluding it from installation.
 [3/4] Linking dependencies...
 [4/4] Building fresh packages...
 Done in 10.89s.
 Removing intermediate container d53a06c9e4c2
 ---> 4e68fbc2d704
 Step 5/6 : COPY . .
 ---> a239a11f68d8
 Step 6/6 : CMD ["node", "src/index.js"]
 ---> Running in 49999f68df8f
 Removing intermediate container 49999f68df8f
 ---> e709c03bc597
 Successfully built e709c03bc597
 Successfully tagged getting-started:latest

你会看到所有层都被重建了。非常好,因为我们对 Dockerfile 做了相当大的改动。

  1. 现在,对 src/static/index.html 文件做一个修改(比如把 <title> 改成 "The Awesome Todo App")。

  2. 现在使用 docker build -t getting-started . 再次构建Docker镜像。这一次,你的输出应该有点不同。

 Sending build context to Docker daemon  219.1kB
 Step 1/6 : FROM node:12-alpine
 ---> b0dc3a5e5e9e
 Step 2/6 : WORKDIR /app
 ---> Using cache
 ---> 9577ae713121
 Step 3/6 : COPY package.json yarn.lock ./
 ---> Using cache
 ---> bd5306f49fc8
 Step 4/6 : RUN yarn install --production
 ---> Using cache
 ---> 4e68fbc2d704
 Step 5/6 : COPY . .
 ---> cccde25a3d9a
 Step 6/6 : CMD ["node", "src/index.js"]
 ---> Running in 2be75662c150
 Removing intermediate container 2be75662c150
 ---> 458e5c6f080c
 Successfully built 458e5c6f080c
 Successfully tagged getting-started:latest

首先,你应该注意到,构建的速度快了很多!而且,你会看到,步骤1-4都有 Using cache 。所以,万岁! 我们正在使用构建缓存。推送和拉动这个镜像以及对它的更新也会快很多。Hooray!

多阶段构建

虽然我们不打算在本教程中过多地讨论它,但多阶段构建是一个非常强大的工具,可以帮助使用多个阶段来创建一个镜像。它们有几个优点:

  • 将构建时的依赖性与运行时的依赖性分开

  • 只发送应用程序运行所需的内容,从而减少整个镜像的大小

Maven/Tomcat示例

在构建基于Java的应用程序时,需要一个JDK将源代码编译成Java字节码。然而,在生产中并不需要JDK。另外,你可能会使用Maven或Gradle等工具来帮助构建应用程序。这些在我们的最终镜像中也是不需要的。多阶段构建的帮助。

# syntax=docker/dockerfile:1
FROM maven AS build
WORKDIR /app
COPY . .
RUN mvn package

FROM tomcat
COPY --from=build /app/target/file.war /usr/local/tomcat/webapps 

在本例中,我们使用一个阶段(称为 build ),用Maven进行实际的Java构建。在第二阶段(从 FROM tomcat 开始),我们把 build 阶段的文件复制进来。最后的镜像只是被创建的最后一个阶段(可以用 --target 标志覆盖)。

React例子

在构建React应用程序时,我们需要一个Node环境来编译JS代码(通常是JSX)、SASS样式表等,将其转化为静态HTML、JS和CSS。如果我们不做服务器端渲染,我们甚至不需要一个Node环境来进行生产构建。为什么不把静态资源放在静态nginx容器中?

# syntax=docker/dockerfile:1
FROM node:12 AS build
WORKDIR /app
COPY package* yarn.lock ./
RUN yarn install
COPY public ./public
COPY src ./src
RUN yarn run build

FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html

在这里,我们使用一个node:12镜像来执行构建(最大化层缓存),然后将输出复制到nginx容器中。很酷,是吧?

回顾一下

通过了解一点关于镜像的结构,我们可以更快地构建镜像,并减少修改。扫描镜像让我们相信,我们正在运行和分发的容器是安全的。多阶段构建还可以帮助我们减少整个镜像的大小,并通过分离构建时的依赖和运行时的依赖来提高最终容器的安全性。

Part 10: What next?

虽然我们的研讨会已经结束了,但仍有很多关于容器的知识需要学习!我们不打算在这里进行深入研究,但这里有一些其他领域的内容,接下来可以看看!

容器的编排

在生产中运行容器是很困难的。你不希望登录到一台机器上,简单地运行 docker rundocker-compose up 。为什么不呢?嗯,如果容器死了会怎么样?你如何在几台机器上进行扩展?容器编排解决了这个问题。像Kubernetes、Swarm、Nomad和ECS这样的工具都有助于解决这个问题,它们的方式略有不同。

一般的想法是,你有接收预期状态的 "管理者"。这个状态可能是 "我想运行我的网络应用程序的两个实例,并暴露80端口"。然后,管理人员查看集群中的所有机器,并将工作委托给 "工人 "节点。管理者观察变化(如容器退出),然后努力使实际状态反映预期状态。

云原生计算基金会项目

CNCF是各种开源项目的厂商中立的家园,包括Kubernetes、Prometheus、Envoy、Linkerd、NATS等。你可以在这里查看已毕业和孵化的项目,以及整个CNCF景观。有很多项目可以帮助解决监控、日志、安全、镜像注册、消息传递等方面的问题。

所以,如果你是容器领域和云原生应用开发的新手,欢迎你!请与社区联系,提出问题,并继续学习!我们很高兴有你的加入!


总结

Docker 官方很好的入门文档。

参考链接: