介绍
Docker平台允许开发人员将应用程序打包和运行为容器 。 容器是一个在共享操作系统上运行的独立进程,为虚拟机提供了更轻量级的替代方案。 虽然容器不是新的,但它们提供的好处 - 包括进程隔离和环境标准化 - 随着更多开发人员使用分布式应用程序体系结构而变得越来越重要。
使用Docker构建和扩展应用程序时,起点通常是为应用程序创建一个映像,然后可以在容器中运行该映像。 该图像包括应用程序代码,库,配置文件,环境变量和运行时。 使用映像可确保容器中的环境标准化,并且仅包含构建和运行应用程序所需的内容。
在本教程中,您将为使用Express框架和Bootstrap的静态网站创建应用程序映像。 然后,您将使用该映像构建容器并将其推送到Docker Hub以供将来使用。 最后,您将从Docker Hub存储库中提取存储的映像并构建另一个容器,演示如何重新创建和扩展应用程序。
先决条件
要学习本教程,您需要:
- 一个Ubuntu 18.04服务器,按照此初始服务器步骤指南进行设置 。
- 根据如何在Ubuntu 18.04上安装和使用Docker的第1步和2,在服务器上安装了Docker 。
- 安装了Node.js和npm,遵循这些有关使用NodeSource管理的PPA进行安装的说明 。
- Docker Hub帐户。 有关如何进行此设置的概述,请参阅有关Docker Hub入门的简介 。
第1步 - 安装应用程序依赖项
要创建映像,首先需要创建应用程序文件,然后将其复制到容器中。 这些文件将包含应用程序的静态内容,代码和依赖项。
首先,在非root用户的主目录中为项目创建一个目录。 我们将调用我们的node_project
,但您可以随意替换它:
mkdir node_project
导航到此目录:
cd node_project
这将是项目的根目录。
接下来,使用项目的依赖项和其他标识信息创建package.json
文件。 使用nano
或您喜欢的编辑器打开文件:
nano 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",
"scripts": {
"start": "node app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"nodejs",
"bootstrap",
"express"
],
"dependencies": {
"express": "^4.16.4"
}
}
此文件包含项目名称,作者和共享它的许可证。 Npm 建议使您的项目名称简短且具有描述性,并避免在npm注册表中重复。 我们在许可证字段中列出了MIT许可证 ,允许免费使用和分发应用程序代码。
此外,该文件指定:
-
"main"
:应用程序的入口点,app.js
。 接下来,您将创建此文件。 -
"scripts"
:使用npm start
时运行的命令会启动应用程序。 -
"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对象,并将基本目录,端口和主机定义为变量:
var express = require("express");
var app = express();
var router = express.Router();
var path = __dirname + '/views/';
const PORT = 8080;
const HOST = '0.0.0.0';
require
函数加载express
模块,然后我们使用它来创建app
和router
对象。 router
对象将执行应用程序的路由功能,当我们定义HTTP方法路由时,我们将它们添加到此对象以定义我们的应用程序将如何处理请求。
该文件的这一部分还设置了一些变量, path
, PORT
和HOST
:
-
path
:定义基目录,该目录将是当前项目目录中的views
子目录。 -
HOST
:定义应用程序将绑定并监听的地址。 除非另有说明,否则将此设置为0.0.0.0
或所有IPv4地址都与Docker将容器暴露于0.0.0.0
的默认行为相对应。 -
PORT
:告诉应用程序监听并绑定到端口8080
。
接下来,使用router
对象设置应用程序的router
:
...
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
:
...
app.use(express.static(path));
app.use("/", router);
app.listen(8080, function () {
console.log('Example app listening on port 8080!')
})
完成的app.js
文件如下所示:
var express = require("express");
var app = express();
var router = express.Router();
var path = __dirname + '/views/';
const PORT = 8080;
const HOST = '0.0.0.0';
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(8080, function () {
console.log('Example app listening on port 8080!')
})
完成后保存并关闭文件。
接下来,让我们为应用程序添加一些静态内容。 首先创建views
目录:
mkdir views
打开登录页面文件index.html
:
nano views/index.html
将以下代码添加到该文件中,该文件将导入Boostrap并创建一个jumbotron组件,其中包含指向更详细的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://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<link href="css/styles.css" rel="stylesheet">
<link href='https://fonts.googleapis.com/css?family=Merriweather:400,700' rel='stylesheet' type='text/css'>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
<nav class="navbar navbar-inverse navbar-static-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Everything Sharks</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav mr-auto">
<li class="active"><a href="/">Home</a></li>
<li><a href="/sharks">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-md-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-md-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"><a href="/">Home</a></li>
<li><a href="/sharks">Sharks</a></li>
</ul>
</div>
...
此外,我们在jumbotron按钮中创建了指向鲨鱼信息页面的链接:
...
<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>
...
标题中还有指向自定义样式表的链接:
...
<link href="css/styles.css" rel="stylesheet">
...
我们将在此步骤结束时创建此样式表。
完成后保存并关闭文件。
随着应用程序登陆页面的到位,我们可以创建我们的鲨鱼信息页面sharks.html
,它将为感兴趣的用户提供有关鲨鱼的更多信息。
打开文件:
nano views/sharks.html
添加以下代码,导入Bootstrap和自定义样式表,并为用户提供有关某些鲨鱼的详细信息:
<!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://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<link href="css/styles.css" rel="stylesheet">
<link href='https://fonts.googleapis.com/css?family=Merriweather:400,700' rel='stylesheet' type='text/css'>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<nav class="navbar navbar-inverse navbar-static-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Everything Sharks</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav mr-auto">
<li><a href="/">Home</a></li>
<li class="active"><a href="/sharks">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-md-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://www.youcl.com/docker_node_image/sawshark.jpg" alt="Sawshark">
</p>
</div>
<div class="col-md-6">
<p>
<div class="caption">Other sharks are known to be friendly and welcoming!</div>
<img src="https://www.youcl.com/docker_node_image/sammy.png" alt="Sammy the Shark">
</p>
</div>
</div>
</div>
</body>
</html>
请注意,在此文件中,我们再次使用active
类来指示当前页面。
完成后保存并关闭文件。
最后,首先在views
目录中创建一个css
文件夹,创建你在index.html
和sharks.html
链接到的自定义CSS样式表:
mkdir views/css
打开样式表:
nano 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
使用npm start
启动应用npm start
:
npm start
将浏览器导航到http:// your_server_ip :8080
。 您将看到以下登录页面:
单击“ 获取鲨鱼信息”按钮。 您将看到以下信息页面:
您现在已启动并运行应用程序。 准备好后,键入CTRL+C
退出服务器。 我们现在可以继续创建Dockerfile,这将允许我们根据需要重新创建和扩展此应用程序。
第3步 - 编写Dockerfile
Dockerfile指定应用程序容器执行时将包含的内容。 使用Dockerfile可以定义容器环境并避免与依赖项或运行时版本的差异。
遵循这些构建优化容器的指导原则 ,我们将通过最小化图像层数量并将图像功能限制为单一目的来重新创建应用程序文件和静态内容,从而使图像尽可能高效。
在项目的根目录中,创建Dockerfile:
nano Dockerfile
Docker图像是使用一系列彼此构建的分层图像创建的。 我们的第一步是为应用程序添加基本映像 ,这将构成应用程序构建的起点。
让我们使用node: 10
图像 ,因为在编写本文时,这是推荐的Node.js的LTS版本 。 添加以下FROM
指令以设置应用程序的基本映像:
FROM node:10
该图像包括Node.js和npm。 每个Dockerfile必须以FROM
指令开头。
默认情况下,Docker Node映像包含一个非root 节点用户,您可以使用该用户来避免以root用户身份运行应用程序容器。 建议的安全做法是避免以root身份运行容器,并将容器内的功能限制为仅运行其进程所需的功能 。 因此,我们将使用节点用户的主目录作为应用程序的工作目录,并将它们设置为容器内的用户。 有关使用Docker Node映像时的最佳实践的详细信息,请参阅此最佳实践指南 。
要微调容器中应用程序代码的权限,让我们在/home/node
创建node_modules
子目录以及app
目录。 创建这些目录将确保它们具有我们想要的权限,这在我们使用npm install
在容器中创建本地节点模块时非常重要。 除了创建这些目录之外,我们还将它们的所有权设置为我们的节点用户:
...
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
有关合并RUN
指令的实用程序的更多信息,请参阅有关如何管理容器层的讨论 。
接下来,将应用程序的工作目录设置为/home/node/app
:
...
WORKDIR /home/node/app
如果未设置WORKDIR
,Docker将默认创建一个,因此最好明确设置它。
接下来,复制package.json
和package-lock.json
(对于npm 5+)文件:
...
COPY package*.json ./
在运行npm install
或复制应用程序代码之前添加此COPY
指令允许我们利用Docker的缓存机制。 在构建的每个阶段,Docker将检查它是否有为该特定指令缓存的层。 如果我们更改package.json
,将重建此图层,但如果我们不重建,则此指令将允许Docker使用现有图像层并跳过重新安装我们的节点模块。
复制项目依赖项后,我们可以运行npm install
:
...
RUN npm install
将应用程序代码复制到容器上的工作应用程序目录:
...
COPY . .
要确保应用程序文件归非根节点用户所有,请将应用程序目录中的权限复制到容器上的目录:
...
COPY --chown=node:node . .
将用户设置为节点 :
...
USER node
在容器上公开端口8080
并启动应用程序:
...
EXPOSE 8080
CMD [ "npm", "start" ]
EXPOSE
不会发布端口,而是用作记录容器上的哪些端口将在运行时发布的方式。 CMD
运行命令来启动应用程序 - 在这种情况下, npm start
。 请注意,每个Dockerfile中只应有一条CMD
指令。 如果包含多个,则只有最后一个生效。
你可以用Dockerfile做很多事情。 有关完整的指令列表,请参阅Docker的Dockerfile参考文档 。
完整的Dockerfile如下所示:
FROM node:10
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
WORKDIR /home/node/app
COPY package*.json ./
RUN npm install
COPY . .
COPY --chown=node:node . .
USER node
EXPOSE 8080
CMD [ "npm", "start" ]
完成编辑后保存并关闭文件。
在构建应用程序映像之前,让我们添加一个.dockerignore
文件 。 .dockerignore
以类似于.gitignore
文件的方式工作,指定不应将项目目录中的哪些文件和目录复制到容器中。
打开.dockerignore
文件:
nano .dockerignore
在文件内部,添加本地节点模块,npm日志,Dockerfile和.dockerignore
文件:
node_modules
npm-debug.log
Dockerfile
.dockerignore
如果您正在使用Git,那么您还需要添加.git
目录和.gitignore
文件。
完成后保存并关闭文件。
您现在可以使用docker build
命令构建应用程序映像了。 将-t
标志与docker build
将允许您使用令人难忘的名称标记图像。 因为我们要将图像推送到Docker Hub,所以让我们在标签中包含我们的Docker Hub用户名。 我们将图像标记为nodejs-image-demo
,但您可以使用自己选择的名称替换它。 还记得将your_dockerhub_username
替换为您自己的Docker Hub用户名:
docker build -t your_dockerhub_username/nodejs-image-demo .
这个.
指定构建上下文是当前目录。
构建图像需要一两分钟。 完成后,检查您的图片:
docker images
您将看到以下输出:
OutputREPOSITORY TAG IMAGE ID CREATED SIZE
your_dockerhub_username/nodejs-image-demo latest 1c723fb2ef12 8 seconds ago 895MB
node 10 f09e7c96b6de 17 hours ago 893MB
现在可以使用docker run
创建具有此图像的容器。 我们将使用此命令包含三个标志:
-
-p
:这将在容器上发布端口并将其映射到主机上的端口。 我们将在主机上使用端口80
,但如果您在该端口上运行另一个进程,则可以根据需要随意修改它。 有关其工作原理的更多信息,请参阅有关端口绑定的Docker文档中的此讨论。 -
-d
:这在后台运行容器。 -
--name
:这允许我们给容器一个令人难忘的名字。
运行以下命令来构建容器:
docker run --name nodejs-image-demo -p 80:8080 -d your_dockerhub_username/nodejs-image-demo
一旦容器启动并运行,您可以使用docker ps
检查正在运行的容器列表:
docker ps
您将看到以下输出:
OutputCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e50ad27074a7 your_dockerhub_username/nodejs-image-demo "npm start" 8 seconds ago Up 7 seconds 0.0.0.0:80->8080/tcp nodejs-image-demo
随着容器的运行,您现在可以通过浏览器导航到http:// your_server_ip
来访问您的应用程序。 您将再次看到您的应用程序登录页面:
现在您已经为应用程序创建了一个映像,您可以将其推送到Docker Hub以供将来使用。
第4步 - 使用存储库处理图像
通过将应用程序映像推送到Docker Hub之类的注册表,可以在构建和扩展容器时将其用于后续使用。 我们将通过将应用程序映像推送到存储库然后使用映像重新创建容器来演示其工作原理。
推送映像的第一步是登录您在先决条件中创建的Docker Hub帐户:
docker login -u your_dockerhub_username -p your_dockerhub_password
以这种方式登录将使用您的Docker Hub凭据在用户的主目录中创建~/.docker/config.json
文件。
您现在可以使用之前创建的标记将应用程序映像推送到Docker Hub, your_dockerhub_username / nodejs-image-demo
:
docker push your_dockerhub_username/nodejs-image-demo
让我们通过销毁我们当前的应用程序容器和映像并使用我们的存储库中的映像重建它们来测试映像注册表的实用程序。
首先,列出正在运行的容器:
docker ps
您将看到以下输出:
OutputCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e50ad27074a7 your_dockerhub_username/nodejs-image-demo "npm start" 3 minutes ago Up 3 minutes 0.0.0.0:80->8080/tcp nodejs-image-demo
使用输出中列出的CONTAINER ID
,停止正在运行的应用程序容器。 请务必使用您自己的CONTAINER ID
替换下面突出显示的CONTAINER ID
:
docker stop e50ad27074a7
使用-a
标志列出您的所有图像:
docker images -a
您将看到以下输出,其中包含图像名称your_dockerhub_username / nodejs-image-demo
,以及node
图像和构建中的其他图像:
OutputREPOSITORY TAG IMAGE ID CREATED SIZE
your_dockerhub_username/nodejs-image-demo latest 1c723fb2ef12 7 minutes ago 895MB
<none> <none> e039d1b9a6a0 7 minutes ago 895MB
<none> <none> dfa98908c5d1 7 minutes ago 895MB
<none> <none> b9a714435a86 7 minutes ago 895MB
<none> <none> 51de3ed7e944 7 minutes ago 895MB
<none> <none> 5228d6c3b480 7 minutes ago 895MB
<none> <none> 833b622e5492 8 minutes ago 893MB
<none> <none> 5c47cc4725f1 8 minutes ago 893MB
<none> <none> 5386324d89fb 8 minutes ago 893MB
<none> <none> 631661025e2d 8 minutes ago 893MB
node 10 f09e7c96b6de 17 hours ago 893MB
使用以下命令删除已停止的容器和所有图像,包括未使用或悬空的图像:
docker system prune -a
在输出中提示时键入y
以确认您要删除已停止的容器和图像。 请注意,这也将删除您的构建缓存。
您现在已经删除了运行应用程序映像的容器和映像本身。 有关删除Docker容器,映像和卷的更多信息,请参阅如何删除Docker镜像,容器和卷 。
删除所有图像和容器后,您现在可以从Docker Hub中提取应用程序映像:
docker pull your_dockerhub_username/nodejs-image-demo
再次列出您的图像:
docker images
您将看到您的应用程序图像:
OutputREPOSITORY TAG IMAGE ID CREATED SIZE
your_dockerhub_username/nodejs-image-demo latest 1c723fb2ef12 11 minutes ago 895MB
您现在可以使用第3步中的命令重建容器:
docker run --name nodejs-image-demo -p 80:8080 -d your_dockerhub_username/nodejs-image-demo
列出正在运行的容器:
docker ps
OutputCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f6bc2f50dff6 your_dockerhub_username/nodejs-image-demo "npm start" 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存储库重新创建它们。
如果您有兴趣了解如何使用Docker Compose和Docker Machine等工具创建多容器设置,可以查看以下指南:
有关使用容器数据的一般提示,请参阅:
如果您对其他与Docker相关的主题感兴趣,请参阅我们完整的Docker教程库。