使用Docker Compose来处理多容器的NodeJS应用的教程

571 阅读5分钟

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

使用Docker Compose引导NodeJS和MySQL应用程序

一些链接可能是联盟链接。这意味着如果你点击链接并购买物品,我们可能会收到佣金,而不会给你带来额外的费用。

Docker自发布以来,在不到十年的时间里,已经成为开发人员离不开的一种工具。Docker提供了轻量级的容器,以与系统中的其他进程隔离的方式运行服务。如果你还没有正确认识Docker,请在继续学习本教程之前,先跟随我们的Docker介绍指南

在今天的教程中,我们将学习如何用Docker Compose将需要多个Docker容器的应用程序容器化。虽然只用Docker也可以建立这个系统,但我们可以用Docker Compose更容易、更有效地完成这个任务。

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

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


什么是Docker Compose?

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

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


攻击计划

正如我前面所说,我们将用Docker Compose对一个多容器的Node.js应用程序进行docker化。该应用程序使用两个容器,一个用于应用程序的API,另一个用于运行后端数据库。所以,我们要遵循的步骤是这样的:

  1. 创建带有逻辑的Node.js API,以存储和检索数据库中的数据。
  2. 创建Docker文件。
  3. 在YAML文件中添加Docker Compose配置。
  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

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配置

作为添加Compose配置的第一步,在项目目录的根层创建docker-compose.yml 文件。

在向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:
    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

我们的第一个配置动作是运行Dockerfile 中定义的命令。 我们使用build 来完成这个任务。我们可以使用上下文将相对路径传递给Dockerfile

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

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

Web服务定义了两个volumes ,以持久化数据,一个用于项目目录,一个用于node_modules目录,当Docker运行npm install

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

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

当Compose意识到服务之间的依赖关系时,它会在应用程序启动和停止过程中按照依赖关系的顺序启动和停止服务。另外,如果你只启动一个服务,Compose会自动启动该服务的依赖关系。

最后,我们需要定义一个.env文件中没有的environment 。由于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服务,你需要传递一个根用户访问数据库的密码和数据库的名称作为环境变量。你可以在这个Docker Hub的MySQL文档中找到更多关于其他environment 变量选项。

与我们的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,这只是一个单一的命令。

docker compose up

当你在命令行中运行这个命令时,如果MySQL和Node镜像还没有在你的系统中,Docker将拉动它们,安装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

随着Node和MySQL容器的运行,我们现在可以使用我们的应用程序来创建新产品,获得产品列表,并更新产品。


停止应用程序

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

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来管理一个多容器的Node应用。它让我们只需几个步骤就能快速完成任务。Docker Compose让这个如果我们只使用Docker的话可能会比较复杂的过程变得更容易操作。

有了本教程中的概念,你现在可以尝试将更大的Node应用与更多的服务进行容器化。不仅仅是Node,对于任何其他语言平台,你所遵循的过程与我们在这里所做的并没有什么不同。所以,现在是时候去玩玩Docker和Docker Compose了。