掌握 Node.js 的 Docker:高级技术和最佳实践
介绍
在当今快节奏的软件开发环境中,容器化已成为一种流行且有效的应用程序打包和部署方式。Docker 是最广泛使用的容器化技术之一,对于希望更高效地构建、测试和部署应用程序的 Node.js 开发者来说,它已成为必不可少的工具。
我在上一篇文章中介绍了 Docker 化 Node.js 应用程序的基础知识,包括安装 Docker、创建最小 Dockerfile 以及运行一些命令来启动它。然而,随着应用程序变得越来越复杂和精密,你需要更高级的技术来充分利用 Docker。
在本文中,我们将探索一些高级 Docker 技术和最佳实践,通过构建一个简单的身份验证 API,帮助您将 Node.js 容器化技术提升到新的水平。我们将讨论如何使用多阶段构建、环境变量、Docker 卷和其他技术来掌握 Docker for Node.js。这些技术将帮助您创建更安全、更可扩展、更高效的 Docker 镜像,以满足您的 Node.js 应用程序的独特需求。所以,系好安全带,准备好将您的 Docker 技能提升到新的水平吧!
先决条件
在深入研究本文介绍的高级 Docker 技术之前,建议您对 Docker 及其基本概念有基本的了解。熟悉 Node.js、Express 和 MongoDB 也很有帮助,但并非必需。如果您是 Docker 新手或需要复习一下,可以阅读我之前的文章“如何在 Node.js 中设置 Docker”,其中介绍了基础知识。此外,您还应该在本地计算机上安装 Docker,并对 Docker 的命令行界面有基本的了解。有了这些先决条件,您就可以开始学习并掌握 Docker for Node.js 了!
构建一个简单的身份验证 API
为了更好地理解本文将介绍的高级 Docker 技术,我们将使用 Node.js、Express 和 MongoDB 创建一个简单的身份验证 API。我们的应用程序将有两个端点 - 一个用于用户注册,另一个用于用户登录。
通过使用 Docker 将我们的应用程序容器化,我们将能够展示各种高级技术,例如多阶段构建、环境变量、Docker 卷等等。这些技术将帮助您创建更高效、更安全、更可扩展的 Docker 镜像,以满足 Node.js 应用程序的独特需求。那么,让我们开始吧!
设置项目
首先,我们需要设置项目目录并安装必要的依赖项。我们将使用 express 和 mongoose 包来构建应用程序,使用 bcrypt 包进行密码哈希处理,以及其他我们将通过以下步骤学习的包:
a. 为您的项目创建一个新目录并导航至该目录:
mkdir docker-node-app && cd docker-node-app
b.使用以下命令初始化新的 Node.js 项目:
npm init -y
c. 运行以下命令安装所需的依赖项:
npm install express mongoose bcrypt jsonwebtoken dotenv nodemon
上述命令安装了身份验证应用程序所需的依赖项。让我们仔细看看每个依赖项:
-
express:用于构建 Web 应用程序和 API 的流行 Node.js 框架。
-
mongoose:一个为 MongoDB 数据建模提供简单基于模式的解决方案的库。
-
bcrypt:用于密码散列和安全存储密码的库。
-
jsonwebtoken:用于生成和验证 JSON web 令牌的库。
-
dotenv:一个零依赖模块,它将环境变量从 .env 文件加载到 process.env 中。我们将使用它来加载应用程序的敏感配置数据。
-
nodemon:Node.js 的一个工具,当代码发生更改时,会自动重启应用程序,让开发更轻松、更高效。
通过安装这些依赖项,我们为身份验证 API 奠定了基础。在下一节中,我们将创建应用程序的基本结构并定义必要的路由。
定义应用程序结构
现在我们已经安装了所有必要的依赖项,让我们继续定义我们的身份验证应用程序的结构。
为了确保代码库更加有序且易于维护,我们将创建一个“src”目录来存放应用程序的文件和文件夹。在“src”目录中,我们将创建一个“routes”目录,并在单独的文件中定义注册和登录路由。我们还将创建一个“models”目录来定义数据库架构以及用于用户注册和身份验证的模型。
使用以下命令创建必要的目录和文件:
mkdir src
cd src
touch server.js
mkdir models routes controllers
touch models/user.js routes/auth.js controllers/authController.js
在 server.js 中,我们将设置基本的 Express 服务器并连接到 MongoDB 数据库:
const express = require("express");
const mongoose = require("mongoose");
const dotenv = require("dotenv");
const router = require("./routes/auth");
dotenv.config();
const app = express();
const port = process.env.PORT || 8080;
const connectDB = async () => {
try {
await mongoose.connect(
process.env.MONGO_URI || "mongodb://localhost:27017/docker-node-app"
);
console.log("MongoDB connected");
} catch (error) {
console.error(error);
}
};
connectDB();
app.use(express.json());
app.use("/api", router);
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});
在 models/user.js 中,通过复制并粘贴以下代码,使用 Mongoose 定义用户模式和模型:
const mongoose = require("mongoose");
const bcrypt = require("bcrypt");
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
},
password: {
type: String,
required: true,
minlength: 8,
},
});
// hash user password before saving into database
userSchema.pre("save", async function (next) {
try {
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(this.password, salt);
this.password = hashedPassword;
next();
} catch (error) {
next(error);
}
});
const User = mongoose.model("User", userSchema);
module.exports = User;
在 routes/auth.js 中,复制并粘贴以下代码以使用 Express 定义身份验证路由:
const express = require("express");
const authController = require("../controllers/authController");
const router = express.Router();
router.post("/register", authController.register);
router.post("/login", authController.login);
module.exports = router;
接下来我们在controllers/authController.js中定义用户注册和登录的控制器函数:
const User = require("../models/user");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const register = async (req, res, next) => {
try {
const { name, email, password } = req.body;
const user = await User.create({ name, email, password });
res.status(201).json({
success: true,
message: "User registered successfully",
data: user,
});
} catch (error) {
next(error);
}
};
const login = async (req, res, next) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) {
return res
.status(401)
.json({ success: false, message: "Invalid email or password" });
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res
.status(401)
.json({ success: false, message: "Invalid email or password" });
}
const token = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET || "secret"
);
res.json({ success: true, token });
} catch (error) {
next(error);
}
};
module.exports = {
register,
login,
};
在上面的authController.js
文件中,我们定义了两个重要的函数——register 和 login。我们先从 register 函数开始。在这里,我们使用User.create()
Mongoose 库中的方法在数据库中创建一个新用户。该方法接收从客户端接收到的用户对象,并将其保存到 MongoDB 数据库中。此外,在将用户密码bcrypt
保存到数据库之前,该方法还会使用 Mongoose 库自动对其进行哈希处理,以确保密码安全且不易被解密。
接下来是登录函数,我们首先使用User.findOne()
Mongoose 中的方法通过电子邮件地址搜索用户。获取到用户对象后,我们使用该bcrypt.compare()
方法检查用户提供的密码是否与数据库中的哈希密码匹配。如果密码正确,则使用包jwt.sign()
中的方法生成一个 JSON Web Token (JWT) jsonwebtoken
。该 token 包含用户的 ID、电子邮件地址和有效期,并被发送回客户端,以供后续的 API 请求使用。
总的来说,这两个函数提供了我们应用程序中用户身份验证所需的基本功能。注册函数允许新用户使用安全的哈希密码创建帐户,而登录函数则验证用户的凭据并生成安全令牌以供将来使用。
最后,使用以下代码更新您的package.json
和文件::server.js
package.json
{
"name": "docker-node-app",
"version": "1.0.0",
"description": "",
"main": "src/server.js",
"scripts": {
"dev": "nodemon src/server.js",
"build": "NODE_ENV=production node server.js"
},
"keywords": [
"docker",
"node"
],
"author": "",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.1.0",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"mongoose": "^7.0.1"
},
"devDependencies": {
"nodemon": "^2.0.21"
}
}
server.js
:
const express = require("express");
const mongoose = require("mongoose");
const dotenv = require("dotenv");
const router = require("./routes/auth");
dotenv.config();
const app = express();
const port = process.env.PORT || 8080;
const connectDB = async () => {
try {
await mongoose.connect(
process.env.MONGO_URI || "mongodb://localhost:27017/docker-node-app"
);
console.log("MongoDB connected");
} catch (error) {
console.error(error);
}
};
connectDB();
app.use(express.json());
app.use("/api", router);
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});
多阶段构建
现在我们已经创建了一个基本的身份验证 API,让我们使用 Docker 将其容器化。我们要做的第一件事是为该应用程序创建一个 Dockerfile。在 Dockerfile 中,我们将使用多阶段构建来优化 Docker 镜像的大小。
什么是多阶段构建?
Docker 中的多阶段构建功能非常实用,它允许我们将构建过程分解为多个阶段,从而优化 Docker 镜像。构建过程的每个阶段本质上都是一个独立的镜像,每个镜像都有自己的基础镜像和指令集。这使得我们可以创建最终的 Docker 镜像,其中仅包含运行应用程序所需的文件,而无需构建过程中使用的任何构建工具或依赖项。
多阶段构建过程由 Dockerfile 中的 FROM 指令启动。每次使用 FROM 指令,实际上都是在构建过程中开启一个新的阶段。每个阶段可以有自己的指令集,例如安装依赖项或编译代码。一个阶段完成后,我们可以使用 COPY 指令将文件从该阶段复制到另一个阶段。这非常实用,因为我们可以只复制应用程序所需的文件,同时保留构建过程中使用过的任何不必要的文件或依赖项。
总而言之,多阶段构建使我们能够创建专门为运行应用程序而定制的优化 Docker 镜像,而不会产生任何不必要的臃肿。通过将构建过程分解为多个阶段,我们可以确保每个阶段尽可能高效且优化,从而缩短构建时间并缩小镜像体积。
创建 Dockerfile
在本节中,我们将介绍为我们的身份验证 API 创建Dockerfile的过程,使用多阶段构建来优化我们的镜像并仅包含运行我们的应用程序所需的文件。
# Build stage
FROM node:18-alpine as build
# set working directory
WORKDIR /app
# copy package.json and package-lock.json
COPY package*.json ./
# install dependencies
RUN npm install
# copy source code
COPY . .
# expose port 8080
EXPOSE 8080
# start app
CMD ["npm", "run", "dev"]
让我们分析一下这个 Dockerfile 中发生的事情:
Dockerfile 的第一行指定了我们将用于构建 Node.js 应用程序的基础镜像。在本例中,我们使用 node:18-alpine 镜像,这是一个基于 Alpine Linux 的轻量级镜像,包含 Node.js 18。
FROM node:18-alpine as build
接下来,我们在 Docker 容器内设置应用程序的工作目录:
WORKDIR /app
然后我们将 package.json 和 package-lock.json 文件复制到工作目录:
COPY package*.json ./
这一步很重要,因为它允许 Docker 缓存应用程序依赖项的安装。如果这些文件自上次构建以来没有更改,Docker 可以跳过安装步骤,直接使用缓存的依赖项。
然后我们使用 npm install 安装应用程序的依赖项:RUN npm install
之后,我们将应用程序的其余源代码复制到 Docker 容器中:
COPY . .
这包括我们应用程序的所有 JavaScript 文件,以及任何静态资产,如图像或样式表。
接下来我们把8080端口暴露给外界:EXPOSE 8080
最后,我们指定 Docker 容器启动时运行的命令。在本例中,我们使用以下命令npm run dev
以开发模式启动应用程序:
CMD ["npm", "run", "dev"]
这将使用文件中指定的开发脚本启动我们的应用程序package.json
。
使用 Docker Compose 定义 Docker 服务
在上一节中,我们为应用创建了 Dockerfile,并使用多阶段构建优化了 Docker 镜像。现在,我们将更进一步,使用Docker Compose为应用定义 Docker 服务。
Docker Compose 是一个允许我们定义和运行多容器 Docker 应用程序的工具。在本节中,我们将定义 Node.js 身份验证 API 所需的服务以及如何使用 Docker Compose 运行它们。
version: '3'
services:
app:
image: docker-node-app
build:
context: .
dockerfile: Dockerfile
restart: always
environment:
NODE_ENV: development
MONGO_URI: mongodb://app-db:27017/docker-node-app
JWT_SECRET: my-secret # you can use any string
ports:
- '8080:8080'
depends_on:
- app-db
app-db:
image: mongo:5.0
restart: always
ports:
- '27017:27017'
volumes:
- app-db-data:/data/db
volumes:
app-db-data:
如果您发现上述文件的内容docker-compose.yml
很奇怪,请不要担心,下面对发生的事情进行详细解释:
服务:
docker-compose.yml 文件的 services 部分定义了组成我们应用程序的不同容器。在本例中,我们有两个服务:app
和app-db
。
应用程序
应用服务负责运行我们的 Node.js 应用程序。以下是关键细节:
-
image:指定用于运行应用服务的 Docker 镜像的名称。在本例中,我们使用前面步骤中构建的 docker-node-app 镜像。
-
build:此部分告诉 Docker Compose 如何为应用服务构建 Docker 镜像。我们将 context 指定为 . (当前目录),并将 dockerfile 指定为 Dockerfile ,因为它位于项目根目录中。
-
restart:这告诉 Docker Compose,如果应用服务失败或停止,则始终重新启动该服务。
-
environment:指定将在应用服务中设置的环境变量。在本例中,我们将
NODE_ENV
变量设置为 development,MONGO_URI
变量mongodb://app-db:27017/docker-node-app
为 MongoDB 数据库的 URL(由 Docker 生成),变量JWT_SECRET
为 my-secret(用于签署 JWT 令牌的密钥字符串)。 -
ports:这指定我们想要
8080
在主机上公开端口并将其映射到8080
应用程序容器中的端口。 -
depends_on:指定应用服务依赖于
app-db
首先启动的服务。
应用程序数据库
该app-db
服务负责运行我们的 MongoDB 数据库。以下是其关键细节:
- image:指定用于运行
app-db
服务的 Docker 镜像的名称。在本例中,我们使用的是mongo:5.0
image。 - restart
app-db
:这告诉 Docker Compose ,如果服务失败或停止,则始终重新启动该服务。 - ports:这指定我们要
27017
在主机上公开端口并将其映射到容器27017
中的端口app-db
。 - 卷:这指定我们要使用一个名为的 Docker 卷
app-db-data
来保存我们的 MongoDB 数据库的数据。
卷
该文件的volumes部分docker-compose.yml
定义了应用程序将使用的Docker卷。在本例中,我们有一个名为的卷app-db-data
,用于保存MongoDB数据库的数据。
添加 .dockerignore 文件进行优化
在为 Node.js 应用程序构建 Docker 镜像时,我们一直使用COPY
中的命令将文件和目录从项目目录复制到镜像中Dockerfile
。但是,并非项目目录中的所有文件和目录都是必需的或值得包含在镜像中的。事实上,包含不必要的文件会使镜像体积膨胀并增加构建时间。这就是.dockerignore
文件的用武之地——它允许我们指定要从 Docker 构建上下文中排除的文件和目录。在本节中,我们将仔细研究如何使用 该.dockerignore
文件来确保仅将必要的文件包含在 Docker 镜像中。
在项目根目录下创建一个.dockerignore文件并粘贴以下代码:
node_modules
npm-debug.log
.DS_Store
.env
.git
.gitignore
README.md
该node_modules
目录包含所有已安装的软件包和模块,我们不需要将它们包含在镜像中,因为我们可以使用npm
install 命令来安装它们Dockerfile
。该npm-debug.log
文件也不需要,可以忽略。
该.DS_Store
文件是由 macOS Finder 创建的隐藏文件,用于存储文件夹特定的元数据。我们也不需要这个文件。
该.env
文件包含环境变量,并且在 Docker 镜像中不需要,因为我们在docker-compose.yml
文件中设置了环境。
我们的图像中也不需要目录.git
和文件。.gitignore
最后,README.md
生产图像中不需要该文件,但我们可能希望在开发过程中保留它以供参考。
通过将.dockerignore
包含上述内容的文件添加到我们的项目目录中,我们可以确保这些文件和目录不包含在 Docker 构建上下文中。这有助于最小化镜像的大小并减少构建时间。
现在,所有设置完毕后,我们可以使用单个命令构建和运行整个应用程序堆栈:
docker compose up
这将为我们的应用程序构建并启动 MongoDB 容器和 Node.js 容器。
在结束之前,测试我们的应用程序以确保一切按预期运行非常重要。您可以使用 Postman 之类的 API 测试工具来测试应用程序。
要测试端点,请向注册端点发送 POST 请求:http://localhost:8080/api/register
使用以下 JSON 有效负载(您可以编辑详细信息):
{
"name": "Test",
"email": "test@email.com",
"password": "password"
}
这应该返回类似于此的 JSON 响应:
{
"success": true,
"message": "User registered successfully",
"data": {
"name": "Test",
"email": "test@email.com",
...
}
}
与注册端点类似,向登录端点发送 POST 请求:http://localhost:8080/api/login
包含注册用户的详细信息:
{
"email": "test@email.com",
"password": "password"
}
这应该返回一个带有类似如下令牌的 JSON 响应:
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR..."
}
结论
到目前为止,我们已经介绍了构建和容器化 Node.js 身份验证 API 的高级 Docker 技术和最佳实践。我们首先设置了一个基本的 Node.js 应用程序,并使用 Dockerfile 对其进行了 docker 化。然后,我们探索了多阶段构建,通过分离构建环境和运行时环境,我们可以缩减 Docker 镜像的大小并提高性能。
然后,我们使用 JSON Web Tokens (JWT) 实现了用户身份验证功能,并向应用程序添加了必要的依赖项。我们还介绍了如何使用环境变量配置 Node.js 应用程序,以及如何使用 Docker secrets 或存储在 .env 文件中的环境变量来管理 secrets。
接下来,我们研究了如何使用 Docker Compose 定义和编排多容器应用程序。我们定义了两个服务:一个用于 Node.js 应用程序,另一个用于 MongoDB 数据库。我们还定义了一个网络和一个卷,以方便服务之间的通信并持久化数据库数据。
我们使用 docker-compose 命令构建并运行应用程序,并使用 Postman 等 API 测试工具对其进行了测试。我们还学习了如何扩展服务以应对日益增长的流量,以及如何使用日志和指标来监控应用程序。
最后,我们讨论了在项目目录中包含一个文件以从 Docker 构建上下文中排除不必要的文件和目录的重要性.dockerignore
,以及这如何帮助减少镜像的大小并缩短构建时间。
该项目的完整代码可在 GitHub 上获取:https://github.com/davydocsurg/docker-node-app。通过遵循这些高级 Docker 技术和最佳实践,我们使用 Node.js 和 Docker 构建了一个可扩展且安全的身份验证 API。希望本文能够帮助您提升 Docker 和 Node.js 的知识和技能,并让您有信心将这些技术应用到自己的项目中。所以,赶快在您的下一个 Node.js 项目中尝试这些技术吧!
文章来源:https://dev.to/davydocsurg/mastering-docker-for-nodejs-advanced-techniques-and-best-practices-55m9