介绍
Docker平台允许开发者将应用程序打包并以容器的形式运行。容器是在共享操作系统上运行的单个进程,是虚拟机的轻量级替代方案。虽然容器并非新生事物,但随着越来越多的开发者采用分布式应用程序架构,它们所提供的优势(包括进程隔离和环境标准化)将变得日益重要。.
使用 Docker 构建和扩展应用程序时,通常首先要为应用程序创建一个镜像,然后才能在容器中运行该镜像。该镜像包含应用程序代码、库、配置文件、环境变量和运行时环境。使用镜像可以确保容器环境标准化,并且仅包含构建和运行应用程序所必需的内容。.
在本教程中,您将创建一个使用 Express 和 Bootstrap 框架的静态网站应用程序镜像。然后,您将使用该镜像构建一个容器,并将其推送到 Docker Hub 以供将来使用。最后,您将从 Docker Hub 仓库拉取已保存的镜像,并将其构建到另一个容器中,从而演示如何重构和扩展您的应用程序。.
先决条件
- 服务器运行 Ubuntu 系统,需要一个具有 sudo 权限的非 root 用户,并且防火墙已启用。有关如何设置此系统的说明,请从列表中选择您的发行版,并按照我们的初始服务器安装指南进行操作。.
- 您的服务器上已安装 Docker。.
- Node.js 和 npm 已安装。.
- 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 <[email protected]>",
"license": "MIT",
"main": "app.js",
"keywords": [
"nodejs",
"bootstrap",
"express"
],
"dependencies": {
"express": "^4.16.4"
}
}此文件包含项目名称、作者以及分发所依据的许可证。npm 建议您保持项目名称简短且具有描述性,并避免在 npm 注册表中出现重复。我们在许可证部分列出了 MIT 许可证,该许可证允许免费使用和分发程序代码。此外,该文件还指定了:
- “main”:应用程序的入口点,即 app.js 文件。稍后您将创建此文件。.
- “依赖项”:项目依赖项——在本例中为 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 应用程序对象,并将基本目录和端口定义为常量:
const express = require('express'); const app = express(); const router = express.Router(); const path = __dirname + '/views/'; const port = 8080;
`require` 函数加载 Express 模块,然后我们使用该模块创建应用程序和路由对象。路由对象负责应用程序的路由功能,当我们定义 HTTP 方法路由时,我们会将它们添加到此对象中,以指定应用程序如何处理请求。.
文件这一部分还包含几个常量, 小路 和 港口 套:
- path: 定义当前项目目录中 views 目录下的基本目录。.
- 端口:告诉程序监听并连接到端口 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');
});router.use 函数会加载一个中间件函数,该函数会记录路由器请求并将其转发到应用程序路由。这些路由定义在以下函数中,其中指定了对项目根 URL 的 GET 请求应返回 index.html 页面,而对 /sharks 路由的 GET 请求应返回 sharks.html 页面。.
最后,挂载路由器中间件和应用程序的静态资源,并告诉应用程序监听 8080 端口:
...
app.use(express.static(path));
app.use('/', router);
app.listen(port, function () {
console.log('Example app listening on port 8080!')
})最终生成的 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 并创建一个巨幕组件,其中包含指向 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>
<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>顶部导航栏允许用户在“首页”和“鲨鱼”页面之间切换。在 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>
...此外,我们还在大屏幕按钮上创建了一个指向我们鲨鱼信息页面的链接:
...
<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://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.digitalocean.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.digitalocean.com/articles/docker_node_image/sammy.png" alt="Sammy the Shark">
</p>
</div>
</div>
</div>
</html>请注意,在此文件中,我们再次使用 active 类来显示当前页面。完成后,请保存并关闭文件。.
最后,创建你在 index.html 和 sharks.html 中链接的自定义 CSS 样式表,首先在 views 目录中创建一个 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;
}除了设置字体和颜色外,此文件还限制了图像的大小,将最大宽度设置为 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 DockerfileDocker 镜像由层层叠加的镜像组成。我们的第一步是添加应用程序的基础镜像,这将是构建应用程序的起点。.
让我们使用 node:10-alpine 镜像,因为它是撰写本文时 Node.js 的推荐 LTS 版本。Alpine 镜像取自 Alpine Linux 项目,有助于我们减小镜像大小。有关 Alpine 镜像是否适合您的项目的更多信息,请参阅 Docker Hub Node 镜像页面“镜像变体”部分的完整讨论。.
添加以下 FROM 指令以设置应用程序的基础镜像:
FROM node:10-alpine此镜像包含 Node.js 和 npm。每个 Dockerfile 都必须以 FROM 指令开头。.
默认情况下,Docker Node 镜像包含一个非 root 的 node 用户,您可以利用该用户防止应用程序容器以 root 用户身份运行。避免以 root 用户身份运行容器,并将容器内的权限限制在运行其进程所需的权限范围内,是一种推荐的安全措施。因此,我们将 node 用户的家目录用作应用程序的工作目录,并在容器内将其设置为我们的用户。有关使用 Docker Node 镜像的最佳实践的更多信息,请参阅此最佳实践指南。.
为了微调容器中应用程序代码的权限,我们需要在 `/home/node` 目录下创建一个名为 `node_modules` 的子目录,与应用程序目录放在一起。创建这些目录可以确保它们拥有我们想要的权限,这在我们之后使用 `npm install` 在容器中创建本地 Node 模块时非常重要。除了创建这些目录之外,我们还需要将它们的所有权设置为我们的 Node 用户:
...
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 使用现有的镜像层,从而跳过重新安装 Node 模块的步骤。.
为确保所有应用程序文件(包括 node_modules 目录的内容)都归非 root 用户所有,请在运行 npm install 之前将用户更改为 node:
...
USER node复制项目依赖项并切换用户后,我们可以运行 npm install:
...
RUN npm install接下来,将您的应用程序代码连同相应的权限一起复制到容器上的应用程序目录中:
...
COPY --chown=node:node . .这样可以确保应用程序文件的所有者是非root用户。.
最后,将容器端口设置为 8080 并启动应用程序:
...
EXPOSE 8080
CMD [ "node", "app.js" ]EXPOSE 并不会暴露端口,而是用于记录容器运行时可用的端口。CMD 执行启动应用程序的命令——在本例中为 `node app.js`。请注意,每个 Dockerfile 中只能包含一个 CMD 命令。如果包含多个 CMD 命令,则只会执行最后一个。.
Dockerfile 的功能非常丰富。如需查看完整的操作说明,请参阅 Docker 的 Dockerfile 参考文档。.
完整的 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 文件。.dockerignore 文件的作用类似于 .gitignore 文件,它指定项目目录中哪些文件和目录不应该被复制到容器中。.
打开 .dockerignore 文件:
nano .dockerignore在文件中,添加本地 Node 模块、npm 日志、Dockerfile 和 .dockerignore 文件:
node_modules
npm-debug.log
Dockerfile
.dockerignore
如果您使用 Git,您还需要添加 .git 目录和 .gitignore 文件。.
完成后保存并关闭文件。.
现在您可以使用 `docker build` 命令构建应用程序镜像了。使用 `-t` 标志可以为镜像添加一个易于记忆的名称。由于我们将把镜像推送到 Docker Hub,因此请在标签中包含您的 Docker Hub 用户名。我们将镜像标记为 `nodejs-image-demo`,但您可以随意将其替换为您选择的名称。此外,请记住将 `your_dockerhub_username` 替换为您的 Docker Hub 用户名:
sudo docker build -t your_dockerhub_username/nodejs-image-demo .. 指定构建上下文为当前目录。.
生成图像需要一两分钟。完成后,请检查您的图像:
sudo docker images您将得到以下输出:
Output
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您将得到以下输出:
Output
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 凭据。.
现在,您可以使用之前创建的标签 your_dockerhub_username/nodejs-image-demo 将应用程序镜像推送到 Docker Hub:
sudo docker push your_dockerhub_username/nodejs-image-demo让我们通过销毁当前应用程序容器和镜像,然后使用存储库中的镜像重新构建它们来测试镜像注册表工具。.
首先,列出正在运行的容器:
sudo docker ps您将得到以下输出:
Output
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使用输出中列出的容器 ID 停止正在运行的应用程序容器。请务必将下方高亮显示的 ID 替换为您自己的容器 ID:
sudo docker stop e50ad27074a7使用 -a 列出所有图像:
docker images -a您将获得以下输出,其中包含您的镜像名称、your_dockerhub_username/nodejs-image-demo,以及 Node 镜像和构建中的其他镜像:
Output
REPOSITORY TAG IMAGE ID CREATED SIZE
your_dockerhub_username/nodejs-image-demo latest 1c723fb2ef12 7 minutes ago 73MB
<none> <none> 2e3267d9ac02 4 minutes ago 72.9MB
<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输出结果将包含您的应用程序图像:
Output
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
Output
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 仓库重新创建它们。.












