前言

  1. 本教程是基于Docker官方指南文档翻译修改的。如需更深入的理解,请参阅官方原文。若在翻译过程中有任何错误,请联系我进行指正。
  2. 在本文中所涉及到的命令行代码部分,我使用了$指示符。若要直接复制代码,请自行删除$符号。

  3. 本教程是为Docker初学者准备的,适合那些对Docker还不太熟悉的朋友。若您已经对Docker有一定的了解和经验,本教程可能会过于基础。

概述

Docker简介

Docker 是一个用于开发、交付和运行应用程序的开放平台。 Docker 使您能够将应用程序与基础架构分开,以便 您可以快速交付软件。使用 Docker,您可以管理您的基础架构,以与管理应用程序相同的方式。通过利用 Docker 的 快速传送、测试和部署代码的方法,您可以显著减少编写代码和在生产环境中运行代码之间的延迟。

学习流程

本博客就基于Docker官方教程,手把手地教你使用Docker,你将在本指南中学习和执行的一些操作包括:

  1. 构建和运行一个镜像
  2. 使用Docker Hub分享镜像
  3. 使用带一个数据库的多个容器部署Docker 应用、
  4. 使用Docker Compose运行应用

在你着手学习这个指南前,你应该现了解什么是容器(container)和镜像(image)。

容器(container)

简单说,容器是在一个在你的计算机上的一个沙盒进程,它可以你隔离你主机计算机上其他的进程。这种隔离利用了Linux中已经存在很长时间的内核名称空间和cgroups功能。Docker致力于让这些功能更容易去接近和使用。总结一下什么是容器:

  1. 是一个可运行的镜像实例。你可以使用DockerAPI或者CLI来创建,开始,停止,移动或者删除一个容器。
  2. 可以在本地计算机,虚拟机上运行或者直接云部署
  3. 可移植(可以被运行在任何操作系统)
  4. 与其他容器隔离运行自己的软件,二进制文件和配置

容器镜像(container image)

当运行一个容器,它使用一个独立的文件系统。这个自定义文件系统是一个容器镜像提供的。因为镜像包含容器的文件系统,它必须包含一个应用运行的所有依赖,配置,二进制文件等等。这个镜像同样包含容器的其他配置,例如文件变量,一个运行的默认指令和其他元数据。

容器化应用程序

在本章学习结束,你将得到一个运行在Node.js上的简单的待办列表管理器(为了方便辨识,在下文中统称为:”todo应用“)。如果你对Node.js不熟悉,也不要担心。这个指南不要你对JavaScript有额外的知识储备。

为了完成本章指南,你需要下面一些前置条件:

获取应用

在你运行应用之前,你需要获取应用的源代码放到你的计算机上。

  1. 使用下面的指令来克隆getting-started仓库

    1
    $git clone https://github.com/docker/getting-started.git
  2. 查看已经克隆的仓库内容。在 getting-started/app目录下你可以看到 package.json 和两个子目录 (srcspec)。

image-20230525142654034.png

构建应用的容器镜像

为了构建容器镜像,你将需要使用一个Dockerfile。一个Dockerfile一个简单的基于文本的文件,没有文件扩展名,包含一个包含一系列指令的脚本。Docker使用这个脚本来构建容器镜像。

  1. app目录,在package.json的相同的位置,创建一个名为Dockerfile的文件。你可以使用下面的指令来创建一个基于你正在运行的系统的Dockerfile。

    在终端中,运行下面列出的指令

    改变目录到app目录,替换/path/to/app变成你getting-strated/app目录

    1
    $cd /path/to/app

    创建一个名为 Dockerfile的空文件

    1
    $touch Dockerfile

    在Windows 命令提示符中,运行下面列出的指令

    改变目录到app目录,替换/path/to/app变成你getting-strated/app目录

    1
    $cd \path\to\app

    创建一个名为 Dockerfile的空文件

    1
    $type nul > Dockerfile
  2. 使用一个文本编辑器或者代码编辑器,添加下面的内容到Dockerfile:

    1
    2
    3
    4
    5
    6
    7
    8
    # syntax=docker/dockerfile:1

    FROM node:18-alpine
    WORKDIR /app
    COPY . .
    RUN yarn install --production
    CMD ["node", "src/index.js"]
    EXPOSE 3000
  3. 使用下面的指令构建容器镜像:

    在终端中,改变目录路径到getting-started/app目录。用你的getting-started/app目录路径替换/path/to/app

    1
    $cd /path/to/app

    构建容器镜像。(指令不要漏了后面的.)

    1
    $docker build -t getting-strated .

    docker build指令会根据Dockerfile去创建一个容器镜像。你可能已经发现Docker下载了一一系列的“layers”,这是因为你指导了构造器:你想从node:18-alpine镜像开始构建。但是因为你没有这些文件在你的计算机上,所以Docker需要去下载这些镜像。

    在Docker下载完这些镜像之后,Dockerfile里面的指令会被复制到你的应用里面,然后使用yarn去安装你的应用依赖。CMD指令指定了容器从这个镜像运行时的默认命令

    最后,t标签标记了你的镜像。简单地想一个人类可读的名字给最终镜像。因为你给这个容器命名为getting-started,当你运行容器时,你可以引用这个的镜像。

    docker build命令末尾的.告诉Docker:它应该在当前目录寻找Dockerfile

启动应用容器

你现在拥有一个镜像,你应该在容器中启动该应用。为了这样做, 你将要用到docker run命令

  1. docker run命令启动你的容器并指定你创建的镜像名字

    1
    $docker run -dp 3000:3000 getting-started

    你使用-d标签去以”分离“模式(在后台中)运行新的容器。你同样可以用-p标签来创建一个主机端口3000和容器端口3000之间的映射。没有端口映射,你将无法访问该应用

    1. 几秒后,打开你的web浏览器进入http://localhost:3000,你就可以看到你的app了。

      image-20230531093507582

  2. 继续添加一两个todo任务,看看它是否如你所期望的那样工作。你可以将todo任务标记为完成并将其删除。你的前端已成功将todo任务存储在后端中。

image-20230531095012710

在这一步教程中,你应该已经运行了一个带有你新增的一些的todo任务的to应用

如果你快速阅览一次你的所有容器,你应该看到至少一个使用 getting-started 镜像和端口3000的容器正在运行。为了查看你的所有容器,你应该使用CLI或者Docker Desktop的图像界面。

在终端输入docker ps列出你的所有容器

1
$docker ps

终端上应该出现与下面类似的内容

1
2
CONTAINER ID   IMAGE             COMMAND                   CREATED        STATUS        PORTS                    NAMES
4b5519f0c7da getting-started "docker-entrypoint.s…" 17 hours ago Up 17 hours 0.0.0.0:3000->3000/tcp compassionate_yonath

在Docker Desktop中, 选择Containers菜单栏去查看你的容器列表

image-20230531110818166

更新应用程序

上一章节中,你成功容器化了一个应用程序。在这一章节,你将会更新这个应用和容器镜像。你将学会如何去停止和移除一个容器

更新源代码

在下面的步骤中,你将实现这样一个功能:to应用的todo列表中没有任何todo任务时,把提示的”No items yet! Add one above!“改成”You have no todo items yet! Add one above!“

  1. src/static/js/app.js 文件,更新下面的代码(修改原来56行代码的文本内容)

    1
    2
    3
    4
    ...
    - <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>
    ...
  2. 使用上一章节使用过的同一个构建命令docker build来的构建你已经更新版本的镜像

    1
    $docker build -t getting-started .
  3. 启动一个新的容器来使用更新过的代码

    1
    $docker run -dp 3000:3000 getting-started

    你可能会看到一个像这样的错误(ID会有所区别)

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

​ 发生此错误是因为旧容器仍在运行时,无法启动新容器。原因是旧容器已经在使用主机的端口3000,计算机(包括容器)上的只能有一个进程监听一个指定端口(端口占用)。要解决此问题,你需要移除旧容器。

移除旧容器

为了移除旧容器,你首先需要停止它。一旦它已经停止,你就可以移除它。你可以通过CLI或者Docker Desktop的图形界面来执行移除这一操作,选择你喜欢的方式来就好。

  1. 通过docker ps命令获取所有的容器ID

    1
    $docker ps
  2. 使用docker stop命令去停止容器。用docker ps查到的容器ID替换<the-container-id>

    1
    $docker stop <the-container-id>
  3. 一旦容器已经停止,你就可以通过docker rm命令移除它了。

    1
    $docker rm <the-container-id>

你可以通过在docker rm命令后面添加force的方式来实现只用一条指令停止并移除一个容器,例如:docker rm -f <the-container-id>

  1. 打开Docker Desktop的Containers视图
  2. 选择你要删除的当前正在运行的旧容器行Actions列下的垃圾桶图标
  3. 在确认对话框,选择Delete forever

启动新容器

  1. 现在,使用docker run启动你更新后的app

    1
    $docker run -dp 3000:3000 getting-started
  2. 刷新你浏览器上的http://localhost:3000,你就可以看到你更新后的帮助文本了

    image-20230602104818628

分享应用程序

现在你已经构建了一个镜像,你可以分享它。为了分享Docker镜像,你必须使用一个Docker注册表。默认注册表是Docker hub,它是你目前使用的所有镜像的来源。

Docker ID:一个Docker ID允许你去访问Docker Hub,一个世界上最大的容器镜像图书馆和社区。如果你还没拥有一个 Docker ID ,立刻免费创建一个吧

创建一个仓库

为了推送一个镜像, 你首先在Docker Hub需要创建一个仓库

  1. 注册并登陆Docker Hub
  2. 选择Create Repository按钮
  3. 使用getting-started作为仓库名,确保可见性是公开

私人仓库:你知道吗?Docker提供了私人仓库,允许你对指定的用户或者团队限制内容。在Docker pricing可以了解更多些细节

  1. 选择Create按钮

如果你看到下面的图片,可以看到有一个样例Docker命令。这个命令会将内容推送这个仓库。

image-20230602141929803

推送镜像

  1. 在命令行界面,尝试运行你在Docker Hub看到的推送命令。记住你的命令应该使用你的命名空间,而不是下面的“gallifreycar”

    1
    2
    3
    $docker push gallifreycar/getting-started
    The push refers to repository [docker.io/gallifreycar/getting-started]
    An image does not exist locally with the tag: gallifreycar/getting-started

​ 为什么它失败了呢?这个推送命令在寻找一个名为 gallifreycar/getting-started的镜像,但是找不到。如果你运行docker image ls命令,你也会找不到这个镜像。

​ 为了修复这个问题,你需要“tag”(标记) 你一个已经存在的镜像:一个你已经构建并给它起了别名的镜像。

  1. 使用docker login -u YOU-USER-NAME命令登陆到Docker Hub

  2. 使用docker tag命令去给getting-started镜像一个新名字。请确保将YOUR-USER-NAME替换为您的 Docker ID。

    1
    $docker tag getting-started YOUR-USER-NAME/getting-started
  3. 现在再次尝试你的推送命令。如果你正在从Docker Hub中拷贝值,你可以去掉tagname部分,因为你没有给镜像名添加标签。如果你没有指定标签,Docker 将使用一个名为latest的标签。

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

在新的实例上运行镜像

现在你的镜像已经被构建好并推送到一个仓库了,尝试运行你的app在一个全新的从未运行过该容器镜像的的实例上。为了做到这一点,你将使用Play with Docker

Play with Docker:Play with Docker使用 amd64平台。如果您使用的是基于 ARM 的 Apple Silicon Mac,则需要重新构建映像使其兼容 Play with Docker,并将新映像推送到你的仓库中。

为了在amd64平台构建这个镜像,我们要使用--platform标签

1
$docker build --platform linux/amd64 -t YOUR-USER-NAME/getting-started .

Docker buildx同样支持构建多平台镜像。为了了解更多,请查看多平台镜像

  1. 打开你的浏览器到Play with Docker
  2. 选择 Login ,然后从下拉菜单中选择docker
  3. 连接你的Docker Hub账号
  4. 一旦你登陆成功,选择ADD NEW INSTANCE选项在左边的侧边栏中。如果你没有看到它,让你的浏览器变宽一点。在几秒钟之后,一个window终端会你的浏览器打开。

image-20230602171613463

  1. 在这个终端,启动你刚刚推送的app

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

你应该会看到镜像被下载下来,最终启动

  1. 3000徽章出现时选择它,你会看到带着修改痕迹的应用程序出现。如果3000徽章没有出现,你可以选择Open Port按钮输入3000

image-20230606170406739

持久化数据库

如果您没有注意到,每次启动容器时,您的todo事项列表都是空的。为什么会这样?在本部分中,您将深入了解容器是如何工作的。

容器的文件系统

当一个容器运行时,它使用来自镜像的各个层(layers)来构建其文件系统。每个容器同样拥有它自己的”临时空间“去创建/更新/移除文件。任何的更改不会被另一个容器看到,及时他们在使用同一个镜像。

在实践中体验

为了理解这个过程,你可以启动两个容器和为它们各自创建一个文件。你将会看到你在一个容器中的创建的文件并不会影响另一个容器。

  1. 启动一个ubuntu容器并用该容器将创建一个名为/data.txt且内容带有1到10000随机数字的文件
    1
    $docker run -d ubuntu bash -c "shuf -i 1-10000 -n 1 -o /data.txt && tail -f /dev/null"

如果你对这个命令好奇,这个命令会启动一个bash shell和调用两个命令(这就是为什么你会有&&的原因)。第一部分命令会选取一个随机的数字然后写入/data.txt。第二个命令会对文件进行监视以保持容器持续运行。

  1. 验证一下你是否能访问容器中的终端并看到控制台输出。为了做到这一点,你可以使用CLI或者Docker Desktop的图形界面

    在命令行中,使用docker exec命令去访问容器。你需要获取容器ID(使用docker ps来获取)。在你的Mac或者Linux终端,或者window的命令行提示符或者PowerShell,使用下面的命令获取内容:

    1
    $docker exec <container-id> cat /data.txt

    你将看到一个随机的数字

    在Docker Desktop中,前往Containers,将鼠标悬停在运行ubuntu映像的容器上运行ubuntu镜像,然后选择Show container actions菜单。从下拉菜单中,选择Open in terminal

    你将看到一个在Ubuntu容器上运行着一个shell的终端。运行下面的命令去查看/data.txt文件的内容。之后再次关闭此终端

    1
    $cat /data.txt

    你应该会看到应该随机的数字

  2. 现在,启动另一个ubantu容器(用相同的镜像)然后你会看到你不会拥有相同的文件。在你的Mac或者Linux终端,或者Window的命令行提示符或者PowerShell,使用以下命令获取内容:

    1
    $docker run -it ubuntu ls /

在这种情况下,这个命令会列出容器根目录下的文件。看到,这里没有data.txt文件!这是它只被写入第一个容器的私人空间。

  1. 继续,使用docker rm -f <container-id>命令来移除第一个容器

容器卷(volume)

在之前的试验中,你可以看到每个容器启动都是从镜像的定义开始的。当容器创建,更新和删除文件,这些改变会在容器移除时丢失,Docker会隔离所有对容器的改变。利用卷,你可以改变这一种情况。

提供了将容器的特定文件系统路径连接回主机的能力。如果你在容器中挂载一个目录,在目录中的更改会同样被主机看到。如果你在容器重启时挂载相同一个卷,你就可以看到相同的文件了。(简单说就是容器里面的文件系统的一个目录和你主机里面的一个目录相互映射了)

这里有两种主要类型的卷。你最终两种都会用到,但是你将会从卷挂载(volume mount)开始。

持久化todo数据

默认情况下,todo应用(你在上文中获取的待办列表管理器)将会存储它的数据在一个SQLite数据库,在容器的文件系统中的/etc/todos/todo.db下。如果你对SQLite不熟悉,不用担心!它是一个非常简单的关系型数据库:将所有的数据存储在一个单独的文件中。虽然它对于大规模应用来说并不是最好的选择,但是对小demo来说是非常好的选择。接下来你将会学习如何切换不填的数据库引擎。

由于数据库是一个单独的文件,如果您能够将该文件保存在主机上并使其可用于下一个容器,那么它应该能够从上一个容器中断的地方恢复。通过创建一个卷并将其附加(通常称为“挂载”)到存储数据的目录,可以持久保存数据。当容器写入todo.db文件时,它会将数据持久化保存到卷中的主机中。

就跟之前提及的一样,你接下来将要去使用卷挂载(volume mount)。卷挂载可以看成一个不透明的数据桶。Docker完全管理卷,包括在磁盘中的存储位置。你只需要记住卷的名字。

创建一个卷并启动容器

你可以使用CLI或者Docker Desktop的图形界面来创建卷并启动容器

  1. 使用docker volume create命令创建一个卷

    1
    $docker volume create todo-db
  2. 使用docker rm -f <id>再次停止并移除todo应用,因为它仍然在没有使用持久化的卷的情况下运行。

  3. 重启todo应用,但是这次添加上--mount选择去指定一个卷挂载。给这个卷一个名字,然后把它挂载到容器中的/etc/todos:它将捕获这个路径下创建的所有文件。在你的Mac或者Linux终端,或者Windows的命令行指示符或者PowerShell,运行下面的命令:

    1
    $docker run -dp 3000:3000 --mount type=volume,src=todo-db,target=/etc/todos getting-started
  1. 创建卷

    a. 在Docker Desktop选择Volume

    b. 在Volume中选择Create

    c. 指定todo-db作为卷名,如何选择Create

  2. 停止并移除旧容器

    a. 在Docker Desktop选择Containers

    b.在Actions列中选择Delete旧容器

  3. 带着挂载的卷重新启动容器

    a.在Docker Desktop中选择搜索框

​ b. 在搜索窗口中,选择Images

​ c. 在搜索框中,指定名称getting-started.

​ d. 选择你的镜像并点击Run

​ e. 选择Optional settings

​ f. 在Host path指定卷的名称todo-db

​ g. 在Container path,指定etc/todo

​ h .点击Run

注意:你可能遇到这样的报错:Unable to find image 'getting-started:latest' locally,这可能是因为你在分享应用程序中给你应用重新命名为了YOUR-USER-NAME/getting-started,如我的镜像已经重新命名为gallifreycar/getting-started。你可以选择重新构建一个名为getting-started,或者把上面的镜像修改给你自己构建的镜像来消除这个报错。

验证数据持久化

  1. 当容器启动,打开todo应用然后添加一些待办项到你的todo列表中

    image-20230609163531947

  1. 停止和移除todo应用的容器。使用Docker Desktop或者docker psdocker rm -f <id>来移除它
  2. 上面相同的步骤启动一个新容器
  3. 打开app,你应该看到你的待办项仍然在你的待办列表中
  4. 继续,在你检查完你的清单后删除容器

现在你已经学会了如何持久化数据了。

深入了解卷

很多人经常问”当我使用一个卷时,Docker在哪里存储我的数据呢?“如果你想知道,你可以用docker volume inspect命令

1
2
3
4
5
6
7
8
9
10
11
12
$docker volume inspect todo-db
[
{
"CreatedAt": "2023-06-09T07:20:34Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/todo-db/_data",
"Name": "todo-db",
"Options": null,
"Scope": "local"
}
]

Mountpoint是数据在硬盘中的实际位置。记住,在大部分计算机上,你将需要root权限才能从主机中访问此目录。但这就是它所在的位置

通过Docker Desktop 直接访问卷数据

当运行在Docker Desktop时,Docker命令其实实际上是运行在一个我们计算机的一个小虚拟机上面的。如果你想看挂载点目录的实际内容,你需要在虚拟机上查看。

使用绑定挂载

上一章节,你使用了一个卷挂载(volume mount)方式来持久化你数据库中的数据。当你需要持久化存储应用程序中数据时,卷挂载是一个很好选择。

绑定挂载(bind mount)是另一种挂载类型,它允许你将主机文件系统中的目录共享到容器内部。在开发应用程序时,您可以使用绑定挂载来将代码挂载到容器中。一旦您保存了文件,容器可以立即看到您对代码所做的更改。这意味着您可以在容器中运行进程来监听文件系统的变化并对其进行响应。

在这一章节, 你将看到你如何使用绑定挂载和一个名为nodemon的工具去查看文件的改变,和自动重启应用。在大部分语言和框架中都有类似功能的工具。

卷类型的快速对比

下面的表格概述了卷挂载和绑定挂载之间的主要区别

区别类型 Named volums 命名卷 Bind mounts 绑定挂载
主机位置 Docker决定 你自己决定
挂载示例(使用 --mount type=volume,src=my-volume,target=/usr/local/data type=bind,src=/path/to/data,target=/usr/local/data
用容器内容填充新卷
支持卷驱动

尝试使用绑定挂载

在探寻如何使用绑定挂载的来开发你的应用前,你可以运行一个快速试验来实际了解绑定挂载是如何工作的。

  1. 打开一个终端和改变目录到get started仓库的app目录

  2. 运行下面的命令去启动使用绑定挂载的ubuntu容器中的bash

    1
    $docker run -it --mount type=bind,src="$(pwd)",target=/src ubuntu bash
    1
    $docker run -it --mount "type=bind,src=$pwd,target=/src" ubuntu bash

    --mount选项Docker去创建一个绑定挂载,src是你计算机主机的当前工作目录(getting-started/app),target是应该出现在容器内部的目录(/src

  3. 在运行命令后,Docker会在容器系统的根目录启动一个交互式的bash会话

    1
    2
    3
    4
    5
    root@3b60bc876163:/# pwd
    /
    root@3b60bc876163:/# ls
    bin dev home lib32 libx32 mnt proc run src sys usr
    boot etc lib lib64 media opt root sbin srv tmp var
  4. 改变目录到src目录

    这是你启动容器的时挂载的目录,这个你挂载的目录的内容列表会展示出你计算机主机getting-started/app目录相同的文件。

    1
    2
    root@3b60bc876163:/src# ls
    Dockerfile package.json spec src yarn.lock
  5. 创建一个新的文件名为myfile.txt

    1
    2
    3
    root@3b60bc876163:/src# touch myfile.txt
    root@3b60bc876163:/src# ls
    Dockerfile myfile.txt package.json spec src yarn.lock
  6. 在主机上打开app目录会发现myfile.txt也出现在这个目录中

    1
    2
    3
    4
    5
    6
    7
    8
    ├── app/
    │ ├── Dockerfile
    │ ├── myfile.txt
    │ ├── node_modules/
    │ ├── pacakge.json
    │ ├── spec/
    │ ├── src/
    │ └── yarn.lock
  7. 从主机上删除myfile.txt文件

  8. 在容器中再次列出app目录的内容,你会观察到文件现在已经消失了。

    1
    2
    root@3b60bc876163:/src# ls
    Dockerfile node_modules package.json spec src yarn.lock
  9. Ctrl + D停止这个交互式容器会话。

以上所有内容仅仅是绑定挂载的简短介绍。该过程演示了如何在主机和容器之间共享文件,以及这些变化是如何立刻映射到双方的。现在你可以使用绑定挂载去开发软件了。

开发容器

在本地开发安装中使用Docker的绑定挂载是十分常见的。这样做的优点是开发机器不需要所有的构建工具和已经安装的环境。用一个单独的docker run命令,Docker会拉取所有的依赖和工具。

在开发容器中运行你的应用

下面的步骤将描述如何去启动一个使用了绑定挂载的开发容器去做以下的事情:

  • 挂载你的源代码到容器里面
  • 安装所有依赖
  • 启动nodemon去查看文件系统的更改

你可以使用CLI或者Docker Desktop去启动带有绑定挂载的容器

  1. 确保你没有任何getting-started容器当前在运行
  2. getting-started/app目录运行下面的命令

1
2
3
4
$docker run -dp 3000:3000 \
-w /app --mount type=bind,src="$(pwd)",target=/app \
node:18-alpine \
sh -c "yarn install && yarn run dev"

在PowerShell运行下面命令

1
2
3
4
$docker run -dp 3000:3000 `
-w /app --mount "type=bind,src=$pwd,target=/app" `
node:18-alpine `
sh -c "yarn install && yarn run dev"
</div></div>

下面是该命令的详细说明:

  • -dp 3000:3000 - 和前面说的一样。独立运行(在后台)和创建一个端口映射
  • -w /app - 设置“工作目录(working directory)”或者说命令执行的当前目录
  • --mount "type=bind,src=$pwd,target=/app" - 绑定主机中的当前目录到容器中的 /app 目录
  • node:18-alpine - 使用的镜像。注意这是Dockerfile中用于构建你的应用的基础镜像
  • sh -c "yarn install && yarn run dev" - 一些指令。使用sh(由于Alpine镜像中没有bash)启动一个shell和运行yarn install 去安装包,然后运行 yarn run dev 去启动开发服务器。如果你查看 package.json,你会发现dev 脚本启动了nodemon
  1. 你可以使用docker logs <container-id>查看日志。当年你看到下面的信息时,说明你已经准备好进行下一步了:

    1
    2
    3
    4
    5
    6
    7
    8
    $docker logs -f <container-id>
    nodemon src/index.js
    [nodemon] 2.0.20
    [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. 确保你没有任何getting-started容器当前在运行

  2. 用绑定挂载运行镜像

    a. 选中Docker Desktop的顶部搜索栏

    b. 在搜索窗口,选中Image

    c. 在搜索栏,指定容器名,getting-started

    使用搜索过滤器过滤镜像然后选择只显示Local images

    d. 选择你的镜像然后点击Run

    e. 选择Optional settings

    f. 在Host path,指定路径到你主机上的app目录

    g. 在Container Path,指定/app

    h. 点击Run

    image-20230612171801667

  3. 你可以通过Docker Desktop来查看容器日志

    a. 在Docker Desktop中选择Containers

    b. 选择你的容器名

    当年你看到下面的信息时,说明你已经准备好进行下一步了:

    1
    2
    3
    4
    5
    6
    7
    8
    $docker logs -f <container-id>
    nodemon src/index.js
    [nodemon] 2.0.20
    [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

在开发容器中开发你的应用

在你的主机上更新你的应用,然后看看这些更改在容器中的反映。

  1. src/static/js/app.js文件第109行,改变“Add Item” 按钮为简洁的“Add”:
1
2
- {submitting ? 'Adding...' : 'Add Item'}
+ {submitting ? 'Adding...' : 'Add'}
  1. 刷新你的网页页面,你应该立刻就能看到更改的反映。Node服务器可能需要几秒钟去重启。如果发生了错误,几秒之后重新刷新一下。

image-20230612174458539

  1. 不要担心你做出的任何更改。每一次你做出了更改和保存一个文件,nodemon进程会重启应用和自动放入容器。当你做完所有更改时,停止容器然后使用下面的命令构建你的新镜像:

    1
    $docker build -t getting-started .

多容器应用

到目前为止,您一直在使用单容器应用程序。但是,现在你将添加MySQL到应用程序栈中。下面的问题经常会被问到:MySQL会在哪里运行?在相同的容器内安装它还是单独安装它?总的来说,每个容器应该只做一件事然后把它做好。下面的是关于为什么它要运行在独立的容器的一些理由:

  • 很有可能你必须以不同于数据库的方式扩展 API 和前端。
  • 分离容器可以你的版本和更新版本相互隔离
  • 虽然你可以在本地使用容器来管理数据库,但在生产环境中,你可能希望使用托管服务来管理数据库
  • 运行多线程需要一个进程管理器(容器只能启动一个进程):它增加了容器启动/关闭的复杂性

除此之外还有很多原因。因此,就像下图一样,运行你的应用在多容器是最好的选择。

image-20230613103459238

容器网络

记住,容器默认是隔离运行的,它不知道任何其他跟它处于同一主机的进程或者容器。所以,你要如何让一个容器跟其他容器通话呢?答案是网络。如果你放置老哥容器在同一网络下,它们就可以相互通话了。

启动MySQL

这里有两个方式去放置一个容器到网络中:

  • 启动容器时分配网络
  • 连接一个已经运行的容器到网络中

下面的步骤,你首先将创建一个网络,然后在启动时将MySQL容器附加到其中

  1. 创建一个网络

    1
    $docker network create todo-app
  2. 启动一个MySQL容器然后将其附加到网络中。你同样需要去定义一些数据库用于初始化数据库的环境变量。如果你想了解更多MySQL的环境变量,请看MySQL Docker Hub listing的“Environment Variables”章节。

    1
    2
    3
    4
    5
    6
    $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:8.0

    在PowerShell中运行以下命令

    1
    2
    3
    4
    5
    6
    $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:8.0

    在上面的命令中,你可以看到--network-alias标签。在接下来的学习中,你将了解更多这个标签

    在上面的命令中,你应该注意到有一个名为todo-mysql-data的卷被挂载到了/var/lib/mysql上,用于给MySQL存储它的数据。然而你从来没有运行过一个docker volume create命令。Docker意识到你想使用一个命名卷时会自动为你创建它。

  3. 为了确保你的数据库已经装好并运行,连接到这个数据库然后验证它的链接。

    1
    $docker exec -it <mysql-container-id> mysql -u root -p

    当出现密码提示时,输入secret。在MySQL shell,列出所有的数据库然后确认你看到todos数据库

    1
    mysql> SHOW DATABASES;

    你应该会看到类似这样的输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    +--------------------+
    | Database |
    +--------------------+
    | information_schema |
    | mysql |
    | performance_schema |
    | sys |
    | todos |
    +--------------------+
    5 rows in set (0.00 sec)
  1. 退出MySQL shell返回到你计算机的shell

    1
    mysql> exit

    你现在已经有了一个todos数据库了,它已经准备好被你使用了。

连接到MySQL

现在你已经知道MySQL已经装好并在运行中了,你可以使用它了。但是你要怎么使用它呢?如果你在相同的网络内运行另一个容器,你如何找到这个容器呢?记住每个容器有自己的IP地址。

为了解答上面的问题和更好地理解容器网络,你将要去使用nicolaka/netshoot容器,该容器附带了许多工具,这些工具对于解决或调试网络问题非常有用。

  1. 启动一个使用了nicolaka/netshoot镜像的容器。确保连接它到相同的网络。

    1
    $docker run -it --network todo-app nicolaka/netshoot
  2. 在容器内部,你将使用dig命令,它是一个有用的DNS工具。你可以用它查找主机名为mysql的IP地址

    1
    $dig mysql

    你应该看到下面类似的输出。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    ; <<>> DiG 9.18.13 <<>> mysql
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 53229
    ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

    ;; QUESTION SECTION:
    ;mysql. IN A

    ;; ANSWER SECTION:
    mysql. 600 IN A 172.18.0.2

    ;; Query time: 10 msec
    ;; SERVER: 127.0.0.11#53(127.0.0.11) (UDP)
    ;; WHEN: Tue Jun 13 06:50:15 UTC 2023
    ;; MSG SIZE rcvd: 44

    在“ANSWER SECTION”,你可以看到一个A记录,解析为172.18.0.2(你的IP地址很可能有一个不一样的值)。虽然mysql不是一个常见的合法主机名,Docker依旧可以将其解析具有这个网络别名(network alias)的容器的IP地址。记住,你之前使用了--network-alias

    这意味着你的应用只要连接到名为mysql的主机,它将会帮你连接到数据库

运行你的应用去使用MySQL

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

  • MYSQL_HOST - 运行MySQL服务器的主机名
  • MYSQL_USER - 用于连接的用户名
  • MYSQL_PASSWORD - 用于连接的密码
  • MYSQL_DB - 连接后要使用的数据库

虽然使用环境变量去设置连接配置是在开发中是可以接受的,但是强烈建议应用程序在生产环境中运行时不要这样做。Diogo Monica,一个Docker安全方面的前任领导,在wrote a fantastic blog post中解释了为什么

更加安全的机制是使用容器编排框架提供的 secret 支持。在大多数情况下,这些 secret 以文件的形式挂载到正在运行的容器中。你会看到很多应用程序(包括MySQL镜像和todo应用)也支持带有 _FILE 后缀的环境变量,用于指向包含变量的文件。

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

你现在可以启动你准备好用于开发的容器。

  1. 指定好上面的每一个环境变量,同时连接容器到你的应用网络。确保你在getting-startted/app目录运行这些命令。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $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:18-alpine \
    sh -c "yarn install && yarn run dev"

    在PowerShell中运行下面的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $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:18-alpine `
    sh -c "yarn install && yarn run dev"
  2. 如果你看一下容器日志(docker logs -f <container-id>),你应该会看到下面类似的信息输出,它表明了你的应用正在使用mysql数据库

    1
    2
    3
    4
    5
    6
    7
    $nodemon src/index.js
    [nodemon] 2.0.20
    [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
  3. 打开你浏览器的应用添加一些内容到你的todo列表

    image-20230613154240980

  4. 连接到你的mysql数据库然后证明你添加的内容以及写入数据库中了。记住,密码是secret

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

    进入mysql shell之后,运行下面的命令:

    1
    2
    3
    4
    5
    6
    7
    8
    mysql> select * from todo_items;
    +--------------------------------------+-------+-----------+
    | id | name | completed |
    +--------------------------------------+-------+-----------+
    | 065d5ae9-9b16-4360-8fee-f5c404f57efe | work | 0 |
    | c30f730d-ff90-4f0e-bd45-320a848dfdab | game | 0 |
    | 3687557f-22b4-43e8-a9c2-9e76c89d07be | study | 0 |
    +--------------------------------------+-------+-----------+

    你的表格根据你添加的项目可能看起来和我的不太一样。但是你应该可以看到它们被存储在这里。

使用Docker Compose

Docker Compose是一个用于帮助定义和分享多容器应用的开发工具。利用Compose(组件),我们可以创建一个YAML文件去定义服务和用一个单独的命令可以启动所有服务或者关闭它们全部

使用Compose的最大的好处是你可以定义你的应用栈(app stack)到一个文件里面,保存它在你项目仓库根目录下(它现在受版本控制),这让其他人可以非常容易去贡献你的项目。其他人只需要克隆你的仓库和启动compose应用。实际上, 你可以看到一些GitHub/GitLab的项目现在就是这样做的。

所以,我们要如何开始学习呢?

安装Docker Compose

如果你在Windows,Mac,或者Linux上已经安装了Docker Desktop,那么你已经拥有了Docker Compose了!Play with Docker实例已经安装了Docker Compose。

单独安装Docker Engine需要把Docker Compose作为一个分离的包安装,具体请看Install the Compose plugin

在安装后,你应该可以运行下面的命令去查看版本信息

1
$docker compose version

创建Compose文件

  1. /getting-started/app文件夹,创建一个名为docker-compose.yml的文件

  2. 在compose文件中,我们将首先定义要作为应用程序一部分运行的服务(或容器)列表。

    1
    services:

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

定义应用服务

记住,这是用于定义我们应用容器的命令:

1
2
3
4
5
6
7
8
9
$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:18-alpine \
sh -c "yarn install && yarn run dev"
  1. 首先,让我们定义服务入口和容器使用的镜像。我们可以给服务起任何名字。这个服务会自动变成一个网络别名,这对于我们定义我们的MySQL服务非常有用。

    1
    2
    3
    services:
    app:
    image: node:18-alpine
  2. 通常来说,你会看到commandimage定义附近,尽管这是没有明文规定的。所以让我们继续,将这写入我们的文件。

    1
    2
    3
    4
    services:
    app:
    image: node:18-alpine
    command: sh -c "yarn install && yarn run dev"
  3. 让我们通过在服务中定义ports来迁移-p 3000:3000这部分的命令。我们在这里将使用short syntax(短语法),但也有一个更详细的long syntax(长语法)

    1
    2
    3
    4
    5
    6
    services:
    app:
    image: node:18-alpine
    command: sh -c "yarn install && yarn run dev"
    ports:
    - 3000:3000
  4. 接下来,我们将通过使用working_dirvolumes定义来迁移工作目录(-w/app)和卷目录(-v "$(pwd):/app")。卷同样有短语法长语法

    Docker Compose卷定义的一个好处是我们可以基于当前目录使用相对路径。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    services:
    app:
    image: node:18-alpine
    command: sh -c "yarn install && yarn run dev"
    ports:
    - 3000:3000
    working_dir: /app
    volumes:
    - ./:/app
    1. 最终,我们使用environment关键字来迁移环境变量定义

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      services:
      app:
      image: node:18-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的服务。下面是我们用于该容器的命令:

1
2
3
4
5
6
$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:8.0
  1. 首先我们定义一个新服务,将其命名为mysql,这样它可以自动获取这个网络别名。接着我们继续指定我们要使用的镜像。

    1
    2
    3
    4
    5
    services:
    app:
    # The app service definition
    mysql:
    image: mysql:8.0
    1. 接下来,我们将定义一个卷映射。当我们用docker run运行容器时,命名卷会自动创建。然而,这样的事情不会在Docker Compose中发生。我们需要在服务配置顶层的volumes:部分去定义卷然后指定挂载点。简单地只提供卷名和使用默认配置选项。尽管这里有更多可选配置选项

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      services:
      app:
      # The app service definition
      mysql:
      image: mysql:8.0
      volumes:
      - todo-mysql-data:/var/lib/mysql

      volumes:
      todo-mysql-data:
  2. 最终,我们只需要指定环境变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    services:
    app:
    # The app service definition
    mysql:
    image: mysql:8.0
    volumes:
    - todo-mysql-data:/var/lib/mysql
    environment:
    MYSQL_ROOT_PASSWORD: secret
    MYSQL_DATABASE: todos

    volumes:
    todo-mysql-data:

到最后,我们完整的docker-compose.yaml应该看起来跟这一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
services:
app:
image: node:18-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:8.0
volumes:
- todo-mysql-data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: todos

volumes:
todo-mysql-data:

运行应用栈

现在我们拥有了我们自己的docker-compose.yml文件,我们可以把它启动起来了!

  1. 首先确保没有其他应用程序或者数据库备份在运行(docker ps查看,然后docker rm -f <ids>移除)

  2. docker compose up命令启动我们的应用栈。我们将用-d标签在后台运行程序。

    1
    $docker compose up -d

    当我们运行命令后,我们应该可以看到如下类似的输出:

    1
    2
    3
    4
    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文件中定义一个网络)

  3. 让我们使用docker compose logs -f命令看看日志。你会发现来自各个服务的日志都插入一个单独的系统里面了。这在你遇到跟时间相关的问题时非常有用。-f标签用于“follow(跟踪)”日志,所以会在生成时给你实时输出。

    如果你的命令已经运行完了,你将看到如下类似的输出

    1
    2
    3
    4
    mysql_1  | 2019-10-03T03:07:16.083639Z 0 [Note] mysqld: ready for connections.
    mysql_1 | Version: '8.0.31' 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

    在行初的展示的服务名(经常是带颜色的)用于帮助区分信息。如果你想查看指定服务的日志。你可以添加服务名在logs命令的最后面。(例如,docker compose logs -f app)

在Docker仪表盘中查看应用栈

如果我们查看Docker仪表盘,我们会发现这有一个名为app的组。这是Docker Compose里面“project name(项目名)”,同时用于给容器分组。默认情况下,Docker Compose项目的名称就是存放docker-compose.yml文件所在的目录名称

image-20230614151558748

如果你点击app旁边展开箭头,你会看我们在定义在compose里面的两个容器。。这些名称也更具描述性,因为它们遵循<service-name>-<replica-number>的模式。因此,您可以很容易地快速了解哪个容器是我们的应用程序,哪个容器是MySQL数据库

image-20230614160002850

彻底销毁

当你准备去彻底销毁它们,简单地运行docker compose down或者在Docker仪表盘中对整个应用(app)点击垃圾桶。容器就会停止,网络就会被移除

移除卷

默认地,当你运行docker compose down时,你compose文件中的命名卷并没有被移除。如果你想移除卷,你将需要加上--volumes标签

在你删除应用栈时,Docker仪表盘不会删除任何卷

当年你销毁完成,你可以切换到另一个项目,运行docker compose up然后准备为了这个项目做贡献吧!没有比这更简单的事了!

镜像构建的最佳实践

镜像分层

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

  1. 使用docker image history命令查看你之前在教程中创建的getting-started镜像的每个分层

    1
    $docker image history getting-started

    你应该看类似输出(数据和ID可能有所不同)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    IMAGE          CREATED        CREATED BY                                       SIZE      COMMENT
    d55aba52353a 46 hours ago EXPOSE map[3000/tcp:{}] 0B buildkit.dockerfile.v0
    <missing> 46 hours ago CMD ["node" "src/index.js"] 0B buildkit.dockerfile.v0
    <missing> 46 hours ago RUN /bin/sh -c yarn install --production # b… 83.1MB buildkit.dockerfile.v0
    <missing> 46 hours ago COPY . . # buildkit 57.5MB buildkit.dockerfile.v0
    <missing> 46 hours ago WORKDIR /app 0B buildkit.dockerfile.v0
    <missing> 9 days ago /bin/sh -c #(nop) CMD ["node"] 0B
    <missing> 9 days ago /bin/sh -c #(nop) ENTRYPOINT ["docker-entry… 0B
    <missing> 9 days ago /bin/sh -c #(nop) COPY file:4d192565a7220e13… 388B
    <missing> 9 days ago /bin/sh -c apk add --no-cache --virtual .bui… 7.77MB
    <missing> 9 days ago /bin/sh -c #(nop) ENV YARN_VERSION=1.22.19 0B
    <missing> 9 days ago /bin/sh -c addgroup -g 1000 node && addu… 160MB
    <missing> 9 days ago /bin/sh -c #(nop) ENV NODE_VERSION=18.16.0 0B
    <missing> 5 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
    <missing> 5 weeks ago /bin/sh -c #(nop) ADD file:7625ddfd589fb824e… 7.33MB

    每一行代表了镜像中的一层。这里的显示中,基础层在底部,最新层在顶部。使用这个命令,你可以快速查看每一层的大小,用于帮助诊断大型镜像。

  2. 您会注意到有几行被截断了。如果您添加--no trunk标志,你将获得完整的输出(有趣的是,你正在使用一个截断的标签来获得未截断的输出)

    1
    $docker image history --no-trunc getting-started

分层缓存

既然您已经看到了分层的实际操作,那么有一个重要的教训可以帮助减少容器镜像的构建时间。

一旦某个分层改变,所有的下游层都需要重新创建。

让我们看看这个已经使用了很多次的Dockerfile

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

回到image history的输出,我们看到在 Dockerfile 的每个命令都会在镜像中变成新的一个分层。你应该记得,但我们对镜像做出改变时,yarn的依赖都不得不重新安装。是不是有什么方法可以修复这个问题呢?每次构建时都装载一次相同的依赖毫无意义,对吧?

为了解决这个问题,我们需要重构我们的Dockerfile,来支持依赖缓存功能。对于基于Node的应用程序,这些依赖是定义在package.json文件中的。所以如果我们只在一开始只复制这个文件,安装依赖,然后再复制其他内容。这样,我们只在package.json文件改变时重建依赖。这样做有意义吗?

  1. 更新Dockerfile去先只复制package.json里面的内容,安装依赖,以及复制其他内容。

    1
    2
    3
    4
    5
    6
    7
    # syntax=docker/dockerfile:1
    FROM node:18-alpine
    WORKDIR /app
    COPY package.json yarn.lock ./
    RUN yarn install --production
    COPY . .
    CMD ["node", "src/index.js"]
  2. 在Dockerfile相同的文件夹下创建一个名为.dockerignore的文件,添加下面的内容

    1
    node_modules

    .dockerignore文件是一种简单的方法,可以选择性地只复制与镜像相关的文件。你可以在这里阅读更多关于这方面的信息。在这种情况下,node_modules文件夹应该在第二个COPY步骤中省略,不然它可能会覆盖由RUN步骤的命令创建的文件。有关为什么建议Node.js应用程序和其他最佳实践,请阅读如何容器化一个Node.js web应用程序指南

  3. 使用docker build构建新镜像

    1
    $docker build -t getting-started .

    你应该可以看到下面类似的输出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    [+] Building 23.6s (13/13) FINISHED
    => [internal] load .dockerignore 0.1s
    => => transferring context: 52B 0.0s
    => [internal] load build definition from Dockerfile 0.1s
    => => transferring dockerfile: 220B 0.0s
    => resolve image config for docker.io/docker/dockerfile:1 3.9s
    => [auth] docker/dockerfile:pull token for registry-1.docker.io 0.0s
    => CACHED docker-image://docker.io/docker/dockerfile:1@sha256:39b85bbfa7536a5feceb7372a0817649ecb2724562a38360f4 0.0s
    => [internal] load metadata for docker.io/library/node:18-alpine 0.0s
    => [1/5] FROM docker.io/library/node:18-alpine 0.0s
    => [internal] load build context 1.5s
    => => transferring context: 3.28kB 1.5s
    => CACHED [2/5] WORKDIR /app 0.0s
    => [3/5] COPY package.json yarn.lock ./ 0.1s
    => [4/5] RUN yarn install --production 15.7s
    => [5/5] COPY . . 0.1s
    => exporting to image 1.3s
    => => exporting layers 1.2s
    => => writing image sha256:fa9c226a146f055c01e5e75d6c37d1a9d54f4f89799122cabd1ee6a98c671c12 0.0s
    => => naming to docker.io/library/getting-started 0.0s

    你会看到所有的层都被重构了。非常好,这是因为我们改变了很多Dockfile的内容。

  4. 现在,对src/static/index.html做出一些改变(例如改变<title>之类的)

  5. 使用docker build -t getting-started .再次构建Docker镜像。这一次,你的输出应该看起来会有所不同

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    [+] Building 1.5s (12/12) FINISHED
    => [internal] load .dockerignore 0.0s
    => => transferring context: 52B 0.0s
    => [internal] load build definition from Dockerfile 0.0s
    => => transferring dockerfile: 220B 0.0s
    => resolve image config for docker.io/docker/dockerfile:1 1.1s
    => CACHED docker-image://docker.io/docker/dockerfile:1@sha256:39b85bbfa7536a5feceb7372a0817649ecb2724562a38360f4 0.0s
    => [internal] load metadata for docker.io/library/node:18-alpine 0.0s
    => [1/5] FROM docker.io/library/node:18-alpine 0.0s
    => [internal] load build context 0.0s
    => => transferring context: 3.46kB 0.0s
    => CACHED [2/5] WORKDIR /app 0.0s
    => CACHED [3/5] COPY package.json yarn.lock ./ 0.0s
    => CACHED [4/5] RUN yarn install --production 0.0s
    => [5/5] COPY . . 0.1s
    => exporting to image 0.1s
    => => exporting layers 0.1s
    => => writing image sha256:533db22721850e13ec3a68b8dcd028215d707540140bbbe1ee57754b075b674c 0.0s
    => => naming to docker.io/library/getting-started 0.0s

    首先,你应该发现这次构建非常非常快!在这之后,你会发现有几个步骤都是使用了已经缓存的分层。所以,万岁!我们正在使用生成缓存。推送和拉取这个镜像以及更新它也会更快,好极了!