引言
Docker平台允许开发者将应用程序打包并运行为容器。容器是一个在共享操作系统上运行的隔离进程,提供了一种比虚拟机更轻量级的替代方案。尽管容器并不是新事物,但它们提供的好处——包括进程隔离和环境标准化——随着越来越多的开发者使用分布式应用程序架构,这些好处的重要性日益增加。
在本教程中,你将为一个使用Express框架和Bootstrap的静态网站创建应用程序镜像。然后,你将使用该镜像构建一个容器,并将其推送到Docker Hub以备将来使用。最后,你将从你的Docker Hub仓库中拉取存储的镜像,并构建另一个容器,演示你如何重现和扩展你的应用程序。
前提条件
要遵循本教程,你将需要:
需要一个已设置的Ubuntu服务器,具有非root用户,具有sudo
权限,并启用了防火墙以阻止非必要的端口。
设置完成后,以的非root用户身份登录并进行第一步。
建议使用至少有以下配置的服务器:
4 核心的 CPU,4GB 的内存
选择服务器提供商
为了本教程的演示,我将以一个具体的云服务提供商为例,展示如何进行操作。选择哪个提供商根据个人偏好和需求来决定。
以下步骤仅供参考,请根据实际需求选择配置。
购买云服务器
本示例中,我们选择了香港作为服务器区域。
点击 云产品 → 云服务器 → 立即购买
选择操作系统
在创建服务器实例时,选择 Ubuntu 作为操作系统。
连接到服务器
使用 X-shell 或偏好的 SSH 客户端,通过远程用户名和密码连接到服务器。成功连接后,将看到特定的欢迎信息,表明已成功登录。
- 在你的服务器上安装了Docker
- 安装了Node.js和npm
- 一个Docker Hub账户
使用Docker构建NodeJS应用程序的步骤
- 安装应用程序依赖项
- 创建NodeJS应用程序文件
- 编写Dockerfile
- 创建DockerHub镜像仓库
第1步 — 安装你的应用程序依赖项
要创建你的镜像,你首先需要制作你的应用程序文件,然后你可以将它们复制到你的容器中。这些文件将包括你的应用程序的静态内容、代码和依赖项。
首先,在你的非root用户的主目录中为你的项目创建一个目录。我们将我们的项目目录命名为node_project
,但你可以用其他名称替换:
mkdir node_project
导航到这个目录:
cd node_project
这将是项目的根目录。
接下来,创建一个package.json
文件,包含你的项目依赖项和其他识别信息。用nano
或你喜欢的编辑器打开文件:
nano package.json
添加以下关于项目的信息,包括它的名称、作者、许可证、入口点和依赖项。确保将作者信息替换为你自己的姓名和联系详情:
~/node_project/package.json
{
"name": "nodejs-image-demo",
"version": "1.0.0",
"description": "nodejs image demo",
"author": "Sammy the Shark <sammy@example.com>",
"license": "MIT",
"main": "app.js",
"keywords": [
"nodejs",
"bootstrap",
"express"
],
"dependencies": {
"express": "^4.16.4"
}
}
这个文件包括了项目名称、作者和共享许可证。npm建议使你的项目名称简短且描述性,并避免在npm注册表中重复。我们在许可证字段中列出了MIT许可证,允许自由使用和分发应用程序代码。
此外,文件指定了:
"main"
: 应用程序的入口点,app.js
。你将在下一步创建这个文件。"dependencies"
: 项目依赖项——在这种情况下,是Express 4.16.4或更高版本。
尽管这个文件没有列出仓库,你可以通过遵循这些指南将仓库添加到你的package.json
文件中。如果你正在对应用程序进行版本控制,这是一个好的补充。
完成更改后保存并关闭文件。
要安装项目依赖项,请运行以下命令:
npm install
这将在你的项目目录中安装你在package.json
文件中列出的包。
现在我们可以继续构建应用程序文件。
第2步 — 创建应用程序文件
我们将创建一个网站,为用户提供有关鲨鱼的信息。我们的应用程序将有一个主入口点app.js
,以及一个views
目录,其中将包括项目的静态资产。登陆页面index.html
将为用户提供一些初步信息和一个链接到更详细的鲨鱼信息页面sharks.html
。在views
目录中,我们将创建登陆页面和sharks.html
。
首先,在主项目目录中打开app.js
定义项目的路由:
nano app.js
文件的第一部分将创建Express应用程序和Router对象,并定义基目录和端口作为常量:
~/node_project/app.js
const express = require('express');
const app = express();
const router = express.Router();
const path = __dirname + '/views/';
const port = 8080;
require
函数加载了express
模块,然后我们用它来创建app
和router
对象。router
对象将执行应用程序的路由功能,我们定义HTTP方法路由时,将它们添加到这个对象中,以定义我们的应用程序如何处理请求。
这部分文件还设置了两个常量,path
和port
:
path
: 定义了基目录,它将是当前项目目录中的views
子目录。port
: 告诉应用程序监听并绑定到端口8080
。
接下来,使用router
对象设置应用程序的路由:
~/node_project/app.js
...
router.use(function (req,res,next) {
console.log('/' + req.method);
next();
});
router.get('/', function(req,res){
res.sendFile(path + 'index.html');
});
router.get('/sharks', function(req,res){
res.sendFile(path + 'sharks.html');
});
router.use
函数加载了一个中间件函数,它将记录路由器的请求并将它们传递给应用程序的路由。这些在随后的函数中定义,指定基本项目URL的GET请求应该返回index.html
页面,而对/sharks
路由的GET请求应该返回sharks.html
。
最后,挂载router
中间件和应用程序的静态资产,并告诉应用程序监听端口8080
:
~/node_project/app.js
...
app.use(express.static(path));
app.use('/', router);
app.listen(port, function () {
console.log('Example app listening on port 8080!')
})
完成的app.js
文件将如下所示:
~/node_project/app.js
const express = require('express');
const app = express();
const router = express.Router();
const path = __dirname + '/views/';
const port = 8080;
router.use(function (req,res,next) {
console.log('/' + req.method);
next();
});
router.get('/', function(req,res){
res.sendFile(path + 'index.html');
});
router.get('/sharks', function(req,res){
res.sendFile(path + 'sharks.html');
});
app.use(express.static(path));
app.use('/', router);
app.listen(port, function () {
console.log('Example app listening on port 8080!')
})
完成编辑后保存并关闭文件。
接下来,让我们为应用程序添加一些静态内容。首先创建views
目录:
mkdir views
打开登陆页面文件index.html
:
nano views/index.html
添加以下代码,该代码将导入Bootstrap并创建一个jumbotron组件,其中包含一个链接到更详细的sharks.html
信息页面:
~/node_project/views/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>About Sharks</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<link href="css/styles.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Merriweather:400,700" rel="stylesheet" type="text/css">
</head>
<body>
<nav class="navbar navbar-dark bg-dark navbar-static-top navbar-expand-md">
<div class="container">
<button type="button" class="navbar-toggler collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> <span class="sr-only">Toggle navigation</span>
</button> <a class="navbar-brand" href="#">Everything Sharks</a>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav mr-auto">
<li class="active nav-item"><a href="/" class="nav-link">Home</a>
</li>
<li class="nav-item"><a href="/sharks" class="nav-link">Sharks</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="jumbotron">
<div class="container">
<h1>Want to Learn About Sharks?</h1>
<p>Are you ready to learn about sharks?</p>
<br>
<p><a class="btn btn-primary btn-lg" href="/sharks" role="button">Get Shark Info</a>
</p>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-lg-6">
<h3>Not all sharks are alike</h3>
<p>Though some are dangerous, sharks generally do not attack humans. Out of the 500 species known to researchers, only 30 have been known to attack humans.
</p>
</div>
<div class="col-lg-6">
<h3>Sharks are ancient</h3>
<p>There is evidence to suggest that sharks lived up to 400 million years ago.
</p>
</div>
</div>
</div>
</body>
</html>
顶级导航栏允许用户在Home和Sharks页面之间切换。在navbar-nav
子组件中,我们使用了Bootstrap的active
类来向用户指示当前页面。我们还指定了指向我们的静态页面的路由,这些路由与我们在app.js
中定义的路由相匹配:
...
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav mr-auto">
<li class="active nav-item"><a href="/" class="nav-link">Home</a>
</li>
<li class="nav-item"><a href="/sharks" class="nav-link">Sharks</a>
</li>
</ul>
</div>
...
此外,我们在jumbotron的按钮中创建了一个链接到我们的鲨鱼信息页面:
~/node_project/views/index.html
...
<div class="jumbotron">
<div class="container">
<h1>Want to Learn About Sharks?</h1>
<p>Are you ready to learn about sharks?</p>
<br>
<p><a class="btn btn-primary btn-lg" href="/sharks" role="button">Get Shark Info</a>
</p>
</div>
</div>
...
在头部,我们还链接了一个自定义样式表:
~/node_project/views/index.html
...
<link href="css/styles.css" rel="stylesheet">
...
我们将在这一步的最后创建这个样式表。
保存并关闭文件后,我们可以创建我们的鲨鱼信息页面sharks.html
,它将为感兴趣的用户提供有关鲨鱼的更多信息。
打开文件:
nano views/sharks.html
添加以下代码,该代码导入Bootstrap和自定义样式表,并为用户提供有关某些鲨鱼的详细信息:
~/node_project/views/sharks.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>About Sharks</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="<https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css>" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<link href="css/styles.css" rel="stylesheet">
<link href="<https://fonts.googleapis.com/css?family=Merriweather:400,700>" rel="stylesheet" type="text/css">
</head>
<nav class="navbar navbar-dark bg-dark navbar-static-top navbar-expand-md">
<div class="container">
<button type="button" class="navbar-toggler collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> <span class="sr-only">Toggle navigation</span>
</button> <a class="navbar-brand" href="/">Everything Sharks</a>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav mr-auto">
<li class="nav-item"><a href="/" class="nav-link">Home</a>
</li>
<li class="active nav-item"><a href="/sharks" class="nav-link">Sharks</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="jumbotron text-center">
<h1>Shark Info</h1>
</div>
<div class="container">
<div class="row">
<div class="col-lg-6">
<p>
<div class="caption">Some sharks are known to be dangerous to humans, though many more are not. The sawshark, for example, is not considered a threat to humans.
</div>
<img src="<https://assets..com/articles/docker_node_image/sawshark.jpg>" alt="Sawshark">
</p>
</div>
<div class="col-lg-6">
<p>
<div class="caption">Other sharks are known to be friendly and welcoming!</div>
<img src="<https://assets..com/articles/docker_node_image/sammy.png>" alt="Sammy the Shark">
</p>
</div>
</div>
</div>
</html>
注意,在此文件中,我们再次使用active
类来指示当前页面。
保存并关闭文件后。
最后,通过在views
目录中创建一个css
文件夹来创建你在index.html
和sharks.html
中链接的自定义CSS样式表:
mkdir views/css
打开样式表:
nano views/css/styles.css
添加以下代码,它将为我们的页面设置所需的颜色和字体:
~/node_project/views/css/styles.css
.navbar {
margin-bottom: 0;
}
body {
background: #020A1B;
color: #ffffff;
font-family: 'Merriweather', sans-serif;
}
h1,
h2 {
font-weight: bold;
}
p {
font-size: 16px;
color: #ffffff;
}
.jumbotron {
background: #0048CD;
color: white;
text-align: center;
}
.jumbotron p {
color: white;
font-size: 26px;
}
.btn-primary {
color: #fff;
text-color: #000000;
border-color: white;
margin-bottom: 5px;
}
img,
video,
audio {
margin-top: 20px;
max-width: 80%;
}
div.caption: {
float: left;
clear: both;
}
除了设置字体和颜色外,此文件还通过指定max-width
为80%来限制图像的大小。这将防止它们在页面上占用过多的空间。
保存并关闭文件后。
随着应用程序文件的到位和项目依赖项的安装,你已经准备好启动应用程序了。
如果你按照前提条件中的初始服务器设置教程进行了操作,你将有一个只允许SSH流量的活跃防火墙。要允许流量通过端口8080
,请运行:
sudo ufw allow 8080
要启动应用程序,请确保你在项目的根目录中:
cd ~/node_project
使用node app.js
启动应用程序:
node app.js
导航到http://your_server_ip:8080
。你将加载以下登陆页面:
点击获取鲨鱼信息按钮。以下信息页面将加载:
现在你已经有一个运行中的应用程序。当你准备好时,通过输入CTRL+C
退出服务器。我们现在可以继续创建Dockerfile,它将允许我们重现和扩展这个应用程序。
第3步 — 编写Dockerfile
你的Dockerfile指定了在执行应用程序容器时将包含什么。使用Dockerfile允许你定义容器环境,并避免依赖项或运行时版本的差异。
遵循这些关于构建优化容器的指南,我们将使我们的镜像尽可能高效,通过最小化镜像层的数量和限制镜像的功能到一个单一目的——重现我们的应用程序文件和静态内容。
在项目的根目录中创建Dockerfile:
nano Dockerfile
Docker镜像是使用一系列相互建立的层来创建的。我们的第一步将是添加应用程序的基础镜像,这将形成应用程序构建的起点。
让我们使用node:10-alpine
镜像,因为在撰写本文时这是推荐的Node.js LTS版本。alpine
镜像源自Alpine Linux项目,将帮助我们保持镜像大小的精简。有关是否alpine
镜像适合你的项目的更多信息,请查看
Docker Hub Node镜像页面的镜像变体部分的完整讨论。
添加以下FROM
指令以设置应用程序的基础镜像:
~/node_project/Dockerfile
FROM node:10-alpine
这个镜像包括Node.js和npm。每个Dockerfile必须以FROM
指令开始。
默认情况下,Docker Node镜像包括一个非root的node用户,你可以使用它来避免以root身份运行你的应用程序容器。避免以root身份运行容器,并限制容器内的能力,只限于运行其进程所需的能力,是一种推荐的安全实践。因此,我们将使用node用户的主目录作为我们应用程序的工作目录,并在容器内将它们设置为我们的用户。有关使用Docker Node镜像时的最佳实践的更多信息,请查看这个最佳实践指南。
为了微调容器中应用程序代码的权限,让我们在/home/node
中创建node_modules
子目录和app
目录。创建这些目录将确保它们具有我们想要的权限,这在我们使用npm install
在容器中创建本地node模块时将非常重要。除了创建这些目录外,我们还将它们设置为node用户所有:
~/node_project/Dockerfile
...
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
有关合并RUN
指令的效用的更多信息,请阅读这个关于管理容器层的讨论。
接下来,将应用程序的工作目录设置为/home/node/app
:
~/node_project/Dockerfile
...
WORKDIR /home/node/app
如果没有设置WORKDIR
,Docker将默认创建一个,因此明确设置它是一个好的做法。
接下来,复制package.json
和package-lock.json
(对于npm 5+)文件:
~/node_project/Dockerfile
...
COPY package*.json ./
在运行npm install
或复制应用程序代码之前添加此COPY
指令,允许我们利用Docker的缓存机制。在构建的每个阶段,Docker将检查它是否有特定指令的缓存层。如果我们更改了package.json
,这个层将被重建,但如果我们没有,这个指令将允许Docker使用现有的镜像层并跳过重新安装我们的node模块。
为确保所有应用程序文件都由非root的node用户拥有,包括node_modules
目录中的内容,在运行npm install
之前切换用户到node:
~/node_project/Dockerfile
...
USER node
复制项目依赖项并切换用户后,我们可以运行npm install
:
~/node_project/Dockerfile
...
RUN npm install
接下来,以适当的权限将你的应用程序代码复制到容器中的应用程序目录:
~/node_project/Dockerfile
...
COPY --chown=node:node . .
这将确保应用程序文件由非root的node用户拥有。
最后,暴露容器上的端口8080
并启动应用程序:
~/node_project/Dockerfile
...
EXPOSE 8080
CMD [ "node", "app.js" ]
EXPOSE
不发布端口,而是作为一种记录容器上将在运行时发布的端口的方式。CMD
运行启动应用程序的命令——在这种情况下,是node app.js
。请注意,每个Dockerfile中只能有一个CMD
指令。如果你包括了多个,只有最后一个会生效。
你可以用Dockerfile做很多事情。有关指令的完整列表,请参考Docker的Dockerfile参考文档。
完整的Dockerfile如下所示:
~/node_project/Dockerfile
FROM node:10-alpine
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
WORKDIR /home/node/app
COPY package*.json ./
USER node
RUN npm install
COPY --chown=node:node . .
EXPOSE 8080
CMD [ "node", "app.js" ]
编辑完成后保存并关闭文件。
在构建应用程序镜像之前,让我们添加一个.dockerignore
文件。与.gitignore
文件类似,.dockerignore
指定了哪些文件和目录在你的项目目录中不应该被复制到你的容器中。
打开.dockerignore
文件:
nano .dockerignore
在文件中,添加你的本地node模块、npm日志、Dockerfile和.dockerignore
文件:
~/node_project/.dockerignore
node_modules
npm-debug.log
Dockerfile
.dockerignore
如果你使用Git进行工作,你还会想要添加你的.git
目录和.gitignore
文件。
保存并关闭文件后。
你现在可以使用docker build
命令构建应用程序镜像。使用-t
标志与docker build
将允许你用一个容易记住的名字标记镜像。因为我们将把镜像推送到Docker Hub,让我们在标签中包含我们的Docker Hub用户名。我们将把镜像标记为nodejs-image-demo
,但你可以替换为你自己的选择。记得也把你的Docker Hub用户名替换为your_dockerhub_username
:
sudo docker build -t your_dockerhub_username/nodejs-image-demo .
.
指定构建上下文是当前目录。
构建镜像将需要一两分钟。完成后,检查你的镜像:
sudo docker images
你将收到以下输出:
REPOSITORY TAG IMAGE ID CREATED SIZE
your_dockerhub_username/nodejs-image-demo latest 1c723fb2ef12 8 seconds ago 73MB
node 10-alpine f09e7c96b6de 3 weeks ago 70.7MB
现在你可以使用docker run
命令创建这个镜像的容器。我们将在这个命令中包含三个标志:
p
: 这个标志发布容器上的端口并将其映射到主机上的端口。我们将在主机上使用端口80
,但如果你需要,可以根据需要修改这个端口。有关这是如何工作的更多信息,请查看Docker文档中关于端口绑定的讨论。d
: 这个标志在后台运行容器。-name
: 这个标志允许我们给容器一个容易记住的名字。
运行以下命令构建容器:
sudo docker run --name nodejs-image-demo -p 80:8080 -d your_dockerhub_username/nodejs-image-demo
使用docker ps
检查你的运行中的容器列表:
sudo docker ps
你将收到以下输出:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e50ad27074a7 your_dockerhub_username/nodejs-image-demo "node app.js" 8 seconds ago Up 7 seconds 0.0.0.0:80->8080/tcp nodejs-image-demo
随着你的容器运行,你现在可以通过导航到你的服务器IP(无需端口)来访问你的应用程序:
http://your_server_ip
你的应用程序登陆页面将再次加载。
现在你已经为你的应用程序创建了一个镜像,你可以将其推送到Docker Hub以备将来使用。
第4步 — 使用仓库与镜像一起工作
通过将你的应用程序镜像推送到像Docker Hub这样的注册表,你使其可用于随后的构建和扩展容器。我们将通过将应用程序镜像推送到仓库,然后使用仓库中的镜像重现我们的容器来演示这个过程。
推送镜像的第一步是登录你在前提条件中创建的Docker Hub账户:
sudo docker login -u your_dockerhub_username
当提示时,输入你的Docker Hub账户密码。以这种方式登录将在用户主目录中创建一个~/.docker/config.json
文件,其中包含你的Docker Hub凭据。
你现在可以推送应用程序镜像到Docker Hub,使用你之前创建的标签,your_dockerhub_username/nodejs-image-demo
:
sudo docker push your_dockerhub_username/nodejs-image-demo
让我们测试镜像注册表的实用性,通过销毁当前的应用程序容器和镜像,并使用仓库中的镜像重建它们。
首先,列出你的运行中的容器:
sudo docker ps
你将得到以下输出:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e50ad27074a7 your_dockerhub_username/nodejs-image-demo "node app.js" 3 minutes ago Up 3 minutes 0.0.0.0:80->8080/tcp nodejs-image-demo
使用你的输出中列出的CONTAINER ID
,停止运行中的应用程序容器。确保将下面的高亮ID替换为你自己的CONTAINER ID
:
sudo docker stop e50ad27074a7
列出你所有的容器,使用-a
标志:
docker images -a
你将收到以下输出,其中包含你的镜像名称your_dockerhub_username/nodejs-image-demo
,以及node
镜像和其他构建过程中的镜像:
REPOSITORY TAG IMAGE ID CREATED SIZE
your_dockerhub_username/nodejs-image-demo latest 1c723fb2ef12 7 minutes ago 73MB
<none>
2.9MB
<none> 2e3267d9ac02 4 minutes ago 7
<none> <none> 8352b41730b9 4 minutes ago 73MB
<none> <none> 5d58b92823cb 4 minutes ago 73MB
<none> <none> 3f1e35d7062a 4 minutes ago 73MB
<none> <none> 02176311e4d0 4 minutes ago 73MB
<none> <none> 8e84b33edcda 4 minutes ago 70.7MB
<none> <none> 6a5ed70f86f2 4 minutes ago 70.7MB
<none> <none> 776b2637d3c1 4 minutes ago 70.7MB
node 10-alpine f09e7c96b6de 3 weeks ago 70.7MB
使用以下命令删除已停止的容器和所有镜像,包括未使用或悬挂的镜像:
docker system prune -a
在输出中提示时输入y
确认你想要删除已停止的容器和镜像。请注意,这也会删除你的构建缓存。
你现在已删除了运行你的应用程序镜像的容器和镜像本身。有关删除Docker容器、镜像和卷的更多信息,请查看如何删除Docker镜像、容器和卷。
删除了所有镜像和容器后,你现在可以从Docker Hub拉取应用程序镜像:
docker pull your_dockerhub_username/nodejs-image-demo
再次列出你的镜像:
docker images
你的输出将包含你的应用程序镜像:
REPOSITORY TAG IMAGE ID CREATED SIZE
your_dockerhub_username/nodejs-image-demo latest 1c723fb2ef12 11 minutes ago 73MB
你现在可以再次使用第3步中的命令重建你的容器:
docker run --name nodejs-image-demo -p 80:8080 -d your_dockerhub_username/nodejs-image-demo
列出你的运行中的容器:
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f6bc2f50dff6 your_dockerhub_username/nodejs-image-demo "node app.js" 4 seconds ago Up 3 seconds 0.0.0.0:80->8080/tcp nodejs-image-demo
再次访问http://your_server_ip
来查看你的运行中的应用程序。
结论
在本教程中,你创建了一个使用Express和Bootstrap的静态Web应用程序,以及这个应用程序的Docker镜像。你使用这个镜像创建了一个容器,并将镜像推送到了Docker Hub。从那里,你能够销毁你的镜像和容器,并使用你的Docker Hub仓库重新创建它们。