如何使用Docker Compose处理多容器NodeJS应用程序?

2021年11月28日06:27:00 发表评论 1,285 次浏览

Docker 在发布不到十年的时间里已经成为开发人员离不开的工具。Docker 提供了轻量级容器来运行与系统中其他进程隔离的服务。如果你还没有正确地介绍 Docker,请 在继续本教程之前按照我们的Docker 介绍指南进行操作。

Docker Compose如何处理多容器NodeJS应用程序?在今天的教程中,我们将学习如何使用Docker Compose将需要多个 Docker 容器的应用程序容器化 。尽管可以仅使用 Docker 来设置此系统,但我们可以使用 Docker Compose 更轻松、更高效地完成此任务。

那么让我们开始吧。这是将 Docker Compose 与多容器 Node.js 应用程序结合使用的指南。

你可以在GitHub 上查看最终项目 。


什么是 Docker Compose?

Docker Compose 是 Docker 平台中的一个工具,用于定义和运行多容器应用程序。例如,如果你的应用程序遵循微服务架构,那么 Compose 是在单独的容器中隔离和运行每个微服务的最佳工具。

Compose 采用一个 YAML 文件,该文件定义和配置与每个应用程序服务相关的设置。然后,你可以使用单个命令启动和停止应用程序的所有服务。


本文计划

Docker Compose与多容器NodeJS应用程序示例教程:正如我之前所说,我们将使用 Docker Compose 对多容器 Node.js 应用程序进行 dockerize。该应用程序使用两个容器,一个用于应用程序 API,一个用于运行后端数据库。所以,我们将要遵循的步骤是这些。

  1. 创建带有逻辑的 Node.js API 以存储和检索数据库中的数据。
  2. 创建 Dockerfile。
  3. 将 Docker Compose 配置添加到 YAML 文件。
  4. 启动并运行应用程序。

先决条件

在继续本教程之前,你应该在系统中下载并安装以下工具。

你还可以安装Docker Desktop ,其中包括 Docker Engine 和 Docker Compose,而不是单独安装。


创建 Node.js 应用程序

我们正在创建一个简单的 Node.js 应用程序,用于管理“产品”对象的创建、更新和显示。它连接到 MySQL 数据库以存储和检索数据。

首先,让我们添加建立数据库连接的代码。

//db.js

const mysql = require("mysql2");

const pool = mysql.createPool({
    host: process.env.MYSQL_HOST,
    user: process.env.MYSQL_USER,
    password: process.env.MYSQL_ROOT_PASSWORD,
    database: process.env.MYSQL_DATABASE
});


//Convert pool object to promise based object
const promisePool = pool.promise();

module.exports = promisePool;

然后,我们将为“产品”定义数据库模型。

//models/product.js

const db = require("../db");

//Create a table for products in the database if it doesn't exist at application start
!async function createTable() {
    const tableQuery = `CREATE TABLE IF NOT EXISTS products (
        id INT PRIMARY KEY AUTO_INCREMENT,
        name VARCHAR(255) NOT NULL,
        price DECIMAL(8,2) NOT NULL,
        description VARCHAR(255))`;
   
    await db.query(tableQuery);    
}();

exports.findAll = async function () {
    const results = await db.query("SELECT * FROM products");
    return results[0];
}

exports.findOne = async function (id) {
    const result = await db.query("SELECT * FROM products WHERE id=?", id);
    return result[0];
}

exports.create = async function (name, price, description) {
    await db.query("INSERT INTO products(name, price, description) VALUES (?, ?, ?)", [name, price, description]);
}

exports.update = async function (id, name, price, description) {
    await db.query("UPDATE products SET name=?, price=?, description=? WHERE id=?",
        [name, price, description, id]);
}

最后,定义 API 端点并在 app.js 文件中创建应用程序服务器。

//app.js

require("dotenv").config();
const express = require("express");
const productModel = require("./models/product");

const app = express();
app.use(express.json());

app.get("/product", async (req, res) => {
    try {
        const products = await productModel.findAll();
        res.status(200).json(products);
    }
    catch (err) {
        res.status(500).json({message: err.message});
    }
});

app.post("/product", async (req, res) => {
    try {
        const {name, price, description} = req.body;
        await productModel.create(name, price, description);
        res.status(200).json({message: "product created"});
    }
    catch (err) {
        res.status(500).json({message: err.message});
    }
});

app.get("/product/:id", async (req, res) => {
    try {
        const product = await productModel.findOne(req.params.id);
        if (product != null) {
            res.status(200).json(product);
        }
        else {
            res.status(404).json({message: "product does not exist"});
        }
    }
    catch (err) {
        res.status(500).json({message: err.message});
    }
});

app.put("/product/:id", async (req, res) => {
    try {
        const {name, price, description} = req.body;
        await productModel.update(req.params.id, name, price, description);
        res.status(200).json({message: "product updated"});
    }
    catch (err) {
        res.status(500).json({message: err.message});
    }
});

app.listen(process.env.NODE_DOCKER_PORT, () => {
    console.log(`application running on port ${process.env.NODE_DOCKER_PORT}`)
});

我们还应该创建一个 .env 包含应用程序代码中使用的环境变量的文件。一些变量在稍后创建 docker-compose.yml 文件时也很有用。

//.env

MYSQL_USER=root
MYSQL_ROOT_PASSWORD=root
MYSQL_DATABASE=store
MYSQL_LOCAL_PORT=3306
MYSQL_DOCKER_PORT=3306

NODE_LOCAL_PORT=3000
NODE_DOCKER_PORT=3000

除了这些文件之外,我们的项目文件夹还包含在项目初始化期间创建的 package.json 文件。


创建 Dockerfile

Docker Compose如何处理多容器NodeJS应用程序:Dockerfile 是容器化应用程序过程中的主要成分之一。它定义了 Docker 应该执行以设置应用程序环境的命令列表。

由于我们也在使用 docker-compose.yml,因此我们不必在 Dockerfile 中定义所有配置命令。尽管如此,我们还是应该定义最基本的来配置 Node 应用程序。

FROM node:latest
WORKDIR /app/
COPY package.json .
RUN npm install
COPY . .

让我们一步一步地了解每个命令的含义。

  • FROM指令告诉 Docker 安装和使用最新的 Node.js 版本的镜像。如果你使用 Node 以外的语言,例如 Python,那么你应该使用此命令来安装 Python 的映像。
  • WORKDIR设置应用程序在 Docker 容器内的工作目录的路径。
  • COPY命令用于将文件从本地系统内的某个位置复制到容器内的某个位置。因此,使用第一个 COPY 命令,我们将 package.json 文件复制到容器中。第二个 COPY 命令复制项目目录中的所有文件。
  • RUN命令用于在容器内执行命令行指令。在这种情况下,我们正在运行npm install以安装package.json.

创建 Dockerfile 后,你可以构建应用程序映像,docker build然后使用docker run命令运行它。但是它不会启动 Node 服务器,因为我们没有将启动命令传递给 Docker。我们还没有创建应用程序数据库。我们可以使用 Docker Compose 完成这两项任务。


添加 Docker Compose 配置

Docker Compose与多容器NodeJS应用程序示例教程:作为添加 Compose 配置的第一步docker-compose.yml,在项目目录的根级别创建文件。

在向Docker Compose文件添加配置时,我们将遵循 Docker 定义的Compose文件版本 3语法。你可以在此语法参考中找到 Compose 提供的所有配置选项。

遵循第 3 版的语法,我们 Compose 文件的基本结构将如下所示。

version: '3.8'
services: 
    web:

    mysqldb:

volumes:
  • Version 指定我们遵循的文件语法版本。
  • 我们应该定义应用程序中的各个服务,这些服务应该在services. 我们的应用程序有两项服务,一项用于 Node 应用程序,一项用于数据库。我们将在下一节中看到如何配置这些服务中的每一个。
  • volumes部分用于列出配置单个服务时定义的命名卷。在我们继续之前,我们应该更多地了解卷,因为它将成为我们 Docker 应用程序的一个组成部分。

Docker 中的卷是什么?

在 Docker 容器内运行应用程序时,卷提供了一种持久化数据更改的方法。它在本地系统中安装一个位置来保存数据和容器运行时所做的更改。

但是为什么我们需要卷?让我们考虑一下我们正在创建的应用程序。如果我们应用程序中使用的数据库不使用卷来持久化数据,那么当容器停止或重新启动时,应用程序存储在数据库中的每条记录都会丢失给我们。为了避免这种情况,我们必须在数据库服务配置下定义卷。

Docker Compose 提供了多种定义卷的方法。

volumes:
    # Just specify a path and let the Docker Engine create a volume on the local system
    - /var/lib/mysql
    
    # Map the local location of the volume to the and Docker container location.
    - ./opt/data:/var/lib/mysql
    
    # Named volume
    - datavolume:/var/lib/mysql

虽然卷是在每个服务下定义的,但在我们的 Compose 文件中,我们必须在顶级卷部分下列出命名的卷。


配置网络服务

现在,是时候在我们的应用程序中配置两个服务中的第一个 Web 服务了。

下面是我们将如何设置 Web 服务。

web:
    build:
        context: .
    env_file: ./.env
    command: npm start
    volumes: 
        - .:/app/
        - /app/node_modules
    ports:
        - $NODE_LOCAL_PORT:$NODE_DOCKER_PORT
    depends_on: 
        - mysqldb
    environment: 
        MYSQL_HOST: mysqldb

Docker Compose与多容器NodeJS应用程序示例教程:我们的第一个配置操作是运行Dockerfile. 我们build用来完成这个。我们可以将相对路径传递给Dockerfileusing 上下文。

在服务配置和应用程序代码内部,Node 应用程序使用.env文件内部定义的环境变量。因此,我们应该在env_file.

Docker Compose 使用command我们提供的 npm start 来启动 Node 应用程序。

Web 服务定义了两个volumes持久化数据,一个用于项目目录,另一个用于 Docker 运行时创建的 node_modules 目录npm install

然后,我们使用ports将公共本地端口映射到内部 docker 端口。通过这种映射,本地端口应该用于从外部访问应用程序。同时,Node 服务器监听 Docker 端口。我们不直接提供端口号,而是提供对 .env 中存储的值的引用,以使代码易于维护。

depends_on 选项用于告诉 Docker Compose 当前服务是否依赖于 Compose 文件中定义的任何其他服务。由于我们的 Node 应用程序依赖 MySQL 数据库来存储和检索数据,因此我们需要在配置中指定这一点。

当 Compose 知道服务之间的依赖关系时,它会在应用程序启动和停止期间按照依赖关系顺序启动和停止服务。此外,如果你只启动一项服务,Compose 会自动启动该服务的依赖项。

最后,我们需要定义一个environment不在 .env 文件中的变量。由于 Docker 会动态分配 MySQL 容器的 IP 地址,因此我们无法在 .env 文件中存储确切的主机地址。相反,我们需要在 Compose 文件中分配这个环境变量的值。当我们传递数据库服务的名称时,Compose 会自动将其更新为数据库容器的 IP,然后 Node 就可以从应用程序代码中访问该变量。


配置数据库服务

数据库服务选项遵循与 Web 服务类似的模式。

mysqldb:
    image: mysql
    env_file: ./.env
    environment: 
        MYSQL_ROOT_PASSWORD: $MYSQL_ROOT_PASSWORD
        MYSQL_DATABASE: $MYSQL_DATABASE
    ports:
        - $MYSQL_LOCAL_PORT:$MYSQL_DOCKER_PORT
    volumes:
        - mysql:/var/lib/mysql
        - mysql_config:/etc/mysql

首先,我们应该提供 MySQL 数据库的名称image。接下来,我们将路径传递到 .env 文件,就像我们在上一节中所做的那样。

设置MySQL服务,需要传递root用户访问数据库的密码和数据库名称作为环境变量。你可以environment在此Docker Hub MySQL 文档 中找到有关其他变量选项的更多信息 。

与我们的 Node 应用程序类似,我们为 MySQL 容器映射本地和 Docker 端口。最后,我们定义了两个命名卷。mysql 卷保存了容器中 /var/lib/mysql 路径下存储的数据库的数据文件。mysql_config 卷保留为 MySQL 设置的全局配置选项。

我们还需要在 Compose 文件的顶级卷部分下添加这两个命名卷。

这是我们为应用程序创建的最终 docker-compose.yml 文件。

version: '3.8'
services: 
    web:
        build:
            context: .
        env_file: ./.env
        command: npm start
        volumes: 
            - .:/app/
            - /app/node_modules
        ports:
            - $NODE_LOCAL_PORT:$NODE_DOCKER_PORT
        depends_on: 
            - mysqldb
        environment: 
            MYSQL_HOST: mysqldb
    mysqldb:
        image: mysql
        env_file: ./.env
        environment: 
            MYSQL_ROOT_PASSWORD: $MYSQL_ROOT_PASSWORD
            MYSQL_DATABASE: $MYSQL_DATABASE
        ports:
            - $MYSQL_LOCAL_PORT:$MYSQL_DOCKER_PORT
        volumes:
            - mysql:/var/lib/mysql
            - mysql_config:/etc/mysql

volumes:
    mysql:
    mysql_config:

使用 Docker Compose 运行应用程序

Docker Compose如何处理多容器NodeJS应用程序?现在,我们剩下要做的就是启动并运行我们的应用程序。使用 Docker Compose,它只是一个命令。

docker compose up

当你在命令行中运行这个命令时,Docker 会拉取 MySQL 和 Node 镜像(如果它们不在你的系统中),安装 npm 包,设置这些配置,然后开始运行 mysqldb 和 web 服务。

如果你想在后台运行服务,你可以使用

docker compose up -d
$ docker compose up -d
[+] Running 2/0
 ⠿ Container nodejs-docker-compose_mysqldb_1  Created                                                                                0.0s
 ⠿ Container nodejs-docker-compose_web_1      Created                                                                                0.0s
Attaching to nodejs-docker-compose_mysqldb_1, nodejs-docker-compose_web_1
mysqldb_1  | 2021-05-13 13:05:28+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.25-1debian10 started.
mysqldb_1  | 2021-05-13 13:05:28+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'
mysqldb_1  | 2021-05-13 13:05:28+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.25-1debian10 started.
mysqldb_1  | 2021-05-13T13:05:29.133664Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.25) starting as process 1
mysqldb_1  | 2021-05-13T13:05:29.146329Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
mysqldb_1  | 2021-05-13T13:05:29.286817Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
mysqldb_1  | 2021-05-13T13:05:29.383092Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Bind-address: '::' port: 33060, socket: /var/run/mysqld/mysqlx.sock
web_1      | 
web_1      | > nodejs-docker-compose@1.0.0 start
web_1      | > node app.js
web_1      | 
mysqldb_1  | 2021-05-13T13:05:29.483669Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
mysqldb_1  | 2021-05-13T13:05:29.483853Z 0 [System] [MY-013602] [Server] Channel mysql_main configured to support TLS. Encrypted connections are now supported for this channel.
mysqldb_1  | 2021-05-13T13:05:29.486598Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
mysqldb_1  | 2021-05-13T13:05:29.506594Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.25'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server - GPL.
web_1      | application running on port 3000

使用 docker ps 命令,我们可以查看正在运行的容器的详细信息。

$ docker ps
CONTAINER ID   IMAGE                       COMMAND                  CREATED         STATUS          PORTS                               NAMES
8da4858ff02e   nodejs-docker-compose_web   "docker-entrypoint.s…"   2 minutes ago   Up 25 seconds   0.0.0.0:3000->3000/tcp              nodejs-docker-compose_web_1
4371bfe94b40   mysql                       "docker-entrypoint.s…"   2 minutes ago   Up 25 seconds   0.0.0.0:3306->3306/tcp, 33060/tcp   nodejs-docker-compose_mysqldb_1

Docker Compose与多容器NodeJS应用程序示例教程:随着 Node 和 MySQL 容器的运行,我们现在可以使用我们的应用程序来创建新产品、获取产品列表和更新产品。

让我们尝试使用Postman测试这些 API 端点 。

获取/产品

测试 GET /product 端点
测试 GET /product 端点

发布/产品

测试 POST /product 端点
测试 POST /product 端点

停止应用程序

你可以使用单个命令停止所有正在运行的容器。

docker compose down
$ docker compose down
[+] Running 3/3
 ⠿ Container nodejs-docker-compose_web_1      Removed  0.8s
 ⠿ Container nodejs-docker-compose_mysqldb_1  Removed  1.5s
 ⠿ Network "nodejs-docker-compose_default"    Removed  0.1s

概括

Docker Compose如何处理多容器NodeJS应用程序?在本教程中,我们学习了如何使用 Docker Compose 来管理多容器 Node 应用程序。它让我们只需几步即可快速完成任务。如果我们只使用 Docker,Docker Compose 使这个过程变得更加复杂。

使用你在本教程中学到的概念,你现在可以尝试使用更多服务容器化更大的 Node 应用程序。但是,不仅是 Node,你在任何其他语言平台上遵循的过程与我们在这里所做的没有什么不同。所以,现在是继续使用 Docker 和 Docker Compose 的时候了。

木子山

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: