发布于 2026-01-06 18 阅读
0

从零开始学习 Docker,第五部分:Docker Compose、变量、卷、网络和数据库

从零开始学习 Docker,第五部分:Docker Compose、变量、卷、网络和数据库

欢迎在推特上关注我,我很乐意接受您对话题或改进方面的建议。/克里斯

本文是系列文章之一:

  • Docker入门教程(第一部分)涵盖了Docker的原理、工作方式以及镜像、容器和Dockerfile等基本概念。此外,本部分还介绍了一些用于管理上述概念的基本Docker命令。
  • Docker——从零开始(第二部分),本部分将介绍卷(Volumes),包括卷的定义、用途以及最重要的——如何利用卷来创建一个出色的开发环境。
  • Docker——从头开始,第三部分。本部分将讲解如何在容器化环境中操作数据库,为此我们需要学习链接和网络方面的知识。
  • Docker入门教程(第四部分):本部分将介绍Docker Compose,我们将学习如何管理大量容器,并了解为什么Docker Compose比直接使用Docker命令更好。
  • Docker——从头开始,第五部分,我们在这里

我们将继续推进我们在第四部分中介绍的项目,在此过程中,我们将展示更多 Docker Compose 功能,并逐步完善我们的项目,以涵盖您可能需要的一切。

本部分我们将介绍:

  • 环境变量,我们已经在前面的章节中介绍过了,所以这里主要讲解如何在 Docker Compose 中设置它们。
  • ,与卷类似,虽然之前的文章已经介绍过,但我们仍会提及它们的用途以及如何使用 Docker Compose 来操作它们。
  • 网络与数据库,最后我们将讲解数据库和网络,这部分内容有点复杂,但希望我们已经解释清楚了。

如果您在阅读过程中感到任何困惑,以下是本文所依据的代码仓库:

https://github.com/softchris/docker-compose-experiments

资源

使用 Docker 和容器化技术的目的是将单体应用拆分成微服务。在本系列教程中,我们将学习如何精通 Docker 及其所有命令。迟早您都需要将容器部署到生产环境中,而生产环境通常是云端。当您觉得自己积累了足够的 Docker 经验后,可以参考以下链接,了解如何在云端使用 Docker:

  • 云端容器:一个很棒的概述页面,展示了关于云端容器的其他知识点。
  • 本教程将指导您如何在云端部署容器,并演示如何轻松利用您现有的 Docker 技能,让您的服务在云端运行。
  • 创建容器镜像仓库您的 Docker 镜像可以位于 Docker Hub 中,也可以位于云端的容器镜像仓库中。如果能将镜像存储在某个地方,并在几分钟内从该仓库创建服务,岂不是很棒?

环境变量

我在之前的文章中已经介绍过如何指定环境变量。虽然可以在配置文件中设置环境变量,Dockerfile但我们当然也可以在命令行中设置,从而也可以在 Docker Compose 中设置,具体来说,就是在以下位置设置docker-compose.yaml

// docker-compose.yaml

version: '3'
services:
 product-service:
   build:
     context: ./product-service
   ports:
     - "8000:3000"
   environment:  
     - test=testvalue 
 inventory-service:
   build:
     context: ./inventory-service
   ports:
   - "8001:3000"
Enter fullscreen mode Exit fullscreen mode

上面我们通过定义环境变量,后面跟着一个,来创建一个环境变量-test=testvalue,这意味着我们创建了变量 test,其值为testvalue

我们可以通过读取 app.js 文件中 product-service 目录下的 process.env.test 文件来轻松测试其是否有效。

另一种测试方法是运行 Docker Compose 并查询可用的环境变量,如下所示:

如上所示,我们首先运行 `docker-compose ps` 命令,获取当前 Docker Compose 会话中包含的容器,然后运行命令docker exec [container name] env列出环境变量。第三种方法是进入docker exec -it [container name] bash容器,使用 bash 命令输出变量值。Docker Compose 提供了多种管理环境变量的方法,建议阅读官方文档,了解更多相关信息。

我们在本系列的前几部分中已经介绍过卷册,我们发现它们是实现以下目标的绝佳方式:

  • 创建一个持久空间,这非常适合创建日志文件或数据库输出,以便在容器关闭和运行后保留这些文件。
  • 将我们的开发环境变成一个卷,这样做的好处是我们可以启动一个容器,然后更改我们的代码,并看到这些更改反映出来,而无需重建或拆除我们的容器,这真正节省了时间。

创建持久空间

让我们看看如何在 Docker Compose 中处理卷:

// docker-compose.yml

version: '3.3'
services:
 product-service:
   build:
     context: ./product-service
   ports:
     - "8000:3000"
   environment:
     - test=testvalue
 inventory-service:
   build:
     context: ./inventory-service
   ports:
     - "8001:3000"
  volumes:  
    - my-volume:/var/lib/data

volumes:  
  my-volume:
Enter fullscreen mode Exit fullscreen mode

上面我们使用文件末尾的 `volumes` 命令创建了一个卷,并在第二行将其命名为 `<volume_name>` my-volume。此外,在文件的 `inventory-service` 部分,我们引用了刚刚创建的卷,并创建了一个映射,/var/lib/data该映射指向卷中的一个目录,该目录将在清理过程中被持久化。让我们检查一下映射是否正确:

如上所示,通过上述命令,我们首先进入容器,docker exec然后导航到我们的映射目录,它就在那里,太好了:)。

让我们在数据目录中创建一个文件,以便证明我们的卷映射确实有效:

echo persist > persist.log

上述命令会创建一个persist.log名为 persist 的文件。虽然没什么特别的,但它确实创建了一个文件,以便在容器关闭并重启后我们可以找到它。

现在我们可以退出容器了。接下来,让我们回顾一些常用的卷命令:

Docker 卷 ls

以上列出了所有当前已挂载的卷。我们可以看到我们创建的卷 compose-experiments_my-volume 就在其中。

我们可以深入探讨更多细节:

docker volume inspect compose-experiments_my-volume

好的,它向我们提供了一些关于卷的详细信息,例如Mountpoint,当我们在容器中写入卷映射目录时,文件将持久保存到该目录。

现在让我们用以下方式卸下集装箱:

docker-compose down

这意味着音量应该仍然存在,所以让我们把它们全部调高:

docker-compose up -d

接下来我们进入容器,看看 persist.log 文件是否存在:

哦,对,它有效。

将当前目录转换为卷

好的,为此我们需要添加一个新卷,并指定计算机上的一个目录以及容器中需要同步的位置。您的docker-compose.yaml文件应如下所示:

// docker-compose.yaml

version: '3.3'
services:
  product-service:
    build:
      context: ./product-service
    ports:
      - "8000:3000"
    environment:
      - test=testvalue
    volumes:  
      - type: bind  
      source: ./product-service  
      target: /app  
  inventory-service:
    build:
      context: ./inventory-service
    ports:
      - "8001:3000"
    volumes:
      - my-volume:/var/lib/data

volumes:
  my-volume:
Enter fullscreen mode Exit fullscreen mode

新增内容已添加到配置中product-service。我们可以看到,我们指定了一个包含一个条目的 volumes 命令。让我们来分析一下该条目:

  • type: bind,这将创建一个所谓的绑定挂载点,这种卷更适合用于在本地目录和容器之间同步文件。
  • 源目录,也就是你的文件所在位置,正如你所看到的,我们已经指出来了./product-service。这意味着,一旦我们更改该目录下的任何文件,Docker 就会立即检测到。
  • target 指的是容器中的目录,现在 source 和 target 已经同步,如果我们对 source 进行更改,target 也会发生同样的更改。

网络和数据库

好的,这是本文要介绍的最后一部分。我们先从数据库开始。所有主流数据库供应商都提供了 Docker 镜像,例如 SQL Server、Postgres、MySQL 等等。这意味着我们无需构建即可启动并运行它们,但我们需要设置一些环境变量,当然还需要开放端口以便与它们交互。接下来,我们来看看如何将 MySQL 数据库添加到我们的解决方案中,也就是我们的配置docker-compose.yml文件中。

添加数据库

添加数据库就像docker-compose.yaml添加一个预先构建好的镜像。幸运的是,MySQL 已经提供了一个现成的镜像。要添加它,我们只需要在 services 下添加另一个条目,如下所示:

// docker-compose.yaml

product-db:
  image: mysql
  environment:
    - MYSQL_ROOT_PASSWORD=complexpassword
  ports:
    - 8002:3306
Enter fullscreen mode Exit fullscreen mode

让我们来详细分析一下:

  • product-db是我们新服务条目的名称,我们选择这个名称
  • image是一个新命令,我们用它来代替 build 命令。当镜像已经构建完成后,我们会使用它,这种情况适用于大多数数据库。
  • environment大多数数据库都需要设置一些变量才能连接,例如用户名、密码以及数据库名称(具体要求因数据库类型而异)。在本例中,我们设置了 `MYSQL_ROOT_PASSWORD`,以便告知 MySQL 实例 root 用户的密码。我们应该考虑创建多个具有不同访问权限的用户。
  • 端口,这里暴露了将要开放的端口,从而为我们与数据库通信提供了入口。通过输入,8002:3306我们指定容器的端口3306应映射到外部端口。8002

我们来看看能否让数据库和其他服务正常运行:

docker-compose up -d

让我们通过以下方式核实:

docker-compose ps 或 docker ps

看起来不错,我们的数据库服务experiments_product-db_1似乎已经在端口上启动并运行了8002。接下来我们看看能否连接到数据库。下面的命令将帮助我们连接到数据库,祈祷一切顺利 ;)

mysql -uroot -pcomplexpassword -h 0.0.0.0 -P 8002
Enter fullscreen mode Exit fullscreen mode

获胜者是……

太好了,我们成功了。接下来,我们看看能不能更新一下我们的某个服务,让它连接到数据库。

连接到数据库

我们可以通过以下三种主要方式连接到数据库:

  • 我们已经使用 Docker 客户端尝试过这种方法了。mysql -uroot -pcomplexpassword -h 0.0.0.0 -P 8002
  • 进入我们的容器,我们使用 `and` 来实现docker exec -it [name of container] bash,然后mysql在容器内输入内容。
  • 接下来,我们将使用 NPM 库 mysql 来研究如何通过我们的应用程序进行连接。

我们将重点讨论第三种选择,即通过我们的应用程序连接到数据库。数据库和应用程序将存在于不同的容器中。那么,我们如何才能让它们连接起来呢?答案是:

  • 两个容器要相互通信,它们必须位于同一网络中。
  • 数据库需要准备就绪,启动数据库需要一些时间,为了让你的应用能够与数据库通信,你需要确保数据库已正确启动。在我弄明白之前,这真是一段既有趣又痛苦的经历,所以别担心,我懂你,我们一定能成功 :)
  • 创建连接对象app.js,确保我们在for 语句中正确设置连接对象。product-service

我们先来看第一个问题。如何将数据库和容器放在同一个网络中?很简单,我们创建一个网络,并将每个容器放在该网络中。下面我们来演示一下docker-compose.yaml

// excerpt from docker-compose.yaml

networks:
  products:
Enter fullscreen mode Exit fullscreen mode

我们需要将此网络分配给每个服务,如下所示:

// excerpt from docker-compose.yaml

services:
  some-service:
    networks:  
      - products
Enter fullscreen mode Exit fullscreen mode

现在,关于第二点,我们如何知道数据库初始化完成呢?我们有一个名为 `wait` 的属性depends_on,通过这个属性,我们可以指定一个容器应该等待另一个容器先启动。也就是说,我们可以这样指定:

// excerpt from docker-compose.yaml

services:
 some-service:
   depends_on: db
 db:
   image: mysql
Enter fullscreen mode Exit fullscreen mode

太好了,这样就解决了吗?不不不,别着急:

在 Docker Compose 版本 2 中,曾经有一种替代方案,我们可以检查服务的健康状况,如果健康状况良好,我们就可以启动容器。它看起来像这样:

depends_on:
 db:
   condition: service_healthy
Enter fullscreen mode Exit fullscreen mode

这意味着我们可以等待数据库完全初始化。但好景不长,在版本 3 中,这个选项被移除了。这里有一个文档页面解释了原因:控制启动和关闭顺序。简而言之,现在我们需要自行判断数据库何时完成并准备好连接。Docker 提供了几个脚本来实现这一点:

所有这些脚本都有一个共同点:监听特定的请求hostport并在收到响应后运行我们的应用程序。那么,我们需要做什么才能实现这一点呢?让我们选择其中一个脚本,wait-for-it并列出我们需要执行的操作:

  • 将此脚本复制到您的服务容器中
  • 赋予脚本执行权限
  • 指示Docker 文件运行脚本,并将数据库主机和端口作为参数传递,然后在脚本执行成功后运行服务。

首先,我们将 GitHub 上的脚本复制到我们的product-service目录中,使其看起来像这样:

/product-service
  wait-for-it.sh
  Dockerfile
  app.js
  package.json
Enter fullscreen mode Exit fullscreen mode

现在我们打开文件Dockerfile并添加以下内容:

// Dockerfile

FROM node:latest

WORKDIR /app

ENV PORT=3000

COPY . .

RUN npm install

EXPOSE $PORT

COPY wait-for-it.sh /wait-for-it.sh

RUN chmod +x /wait-for-it.sh
Enter fullscreen mode Exit fullscreen mode

上面我们将wait-for-it.sh文件复制到容器中,下面一行赋予它执行权限。值得注意的是,我们还ENTRYPOINT从容器中移除了 `<script>` 标签Dockerfile,而是指示容器从该docker-compose.yaml文件启动。接下来让我们看一下这个文件:

// excerpt from docker-compose.yaml

services:
 product-service:
 command: ["/wait-for-it.sh", "db:8002", "--", "npm", "start"]
 db:
 // definition of db service below
Enter fullscreen mode Exit fullscreen mode

上面我们指示程序运行该wait-for-it.sh文件,并使用参数作为参数。db:8002在程序收到满意的响应后,我们就可以继续运行,npm start从而启动我们的服务。听起来不错,它能正常工作吗?

为了全面披露信息,我们不妨展示一下我们的完整docker-compose.yaml文件:

version: '3.3'
  services:
    product-service:
      depends_on:
        - "db"
      build:
        context: ./product-service
      command: ["/wait-for-it.sh", "db:8002", "--", "npm", "start"]
    ports:
      - "8000:3000"
    environment:
      - test=testvalue
      - DATABASE_PASSWORD=complexpassword
      - DATABASE_HOST=db
    volumes:
      - type: bind
      source: ./product-service
      target: /app
    networks:
      - products
   db:
     build: ./product-db
       restart: always
     environment:
       - "MYSQL_ROOT_PASSWORD=complexpassword"
       - "MYSQL_DATABASE=Products"
     ports:
       - "8002:3306"
     networks:
       - products
   inventory-service:
     build:
       context: ./inventory-service
     ports:
       - "8001:3000"
     volumes:
       - my-volume:/var/lib/data

volumes:
 my-volume:

networks:
 products:
Enter fullscreen mode Exit fullscreen mode

好的,总结一下,我们已经将服务器部署product-servicedb网络中products,下载了脚本wait-for-it.sh,并让它在启动应用程序之前运行。脚本会在运行过程中监听数据库的主机和端口,以便在数据库准备就绪后立即响应。这意味着我们只剩最后一步了,我们需要调整配置app.js文件product-service,所以让我们打开这个文件:

// app.js

const express = require('express')
const mysql = require('mysql');
const app = express()
const port = process.env.PORT || 3000;
const test = process.env.test;

let attempts = 0;

const seconds = 1000;

function connect() {
  attempts++;

  console.log('password', process.env.DATABASE_PASSWORD);
  console.log('host', process.env.DATABASE_HOST);
  console.log(`attempting to connect to DB time: ${attempts}`);

 const con = mysql.createConnection({  
   host: process.env.DATABASE_HOST,  
   user: "root",  
   password: process.env.DATABASE_PASSWORD,  
   database: 'Products'  
 });

  con.connect(function (err) {  
   if (err) {  
     console.log("Error", err);  
     setTimeout(connect, 30 * seconds);  
   } else {  
     console.log('CONNECTED!');  
   }

  });

  conn.on('error', function(err) {  
    if(err) {  
      console.log('shit happened :)');  
      connect()  
    }   
  });

}
connect();

app.get('/', (req, res) => res.send(`Hello product service, changed ${test}`))

app.listen(port, () => console.log(`Example app listening on port ${port}!`))
Enter fullscreen mode Exit fullscreen mode

上面我们可以看到,我们定义了一个方法,它通过传入一个对象作为参数来connect()创建连接。这个输入参数需要知道 `<connection_name>` `<connection_name> ` 和`<connection_name>`。这看起来完全合理。我们还在方法调用中添加了一些逻辑,即调用 `<connection_name>` ,这意味着它会在 30 秒后尝试建立另一个连接。现在,由于我们使用了该功能,所以实际上并不需要 `<connection_name>`,我们可以完全依赖应用程序代码来确保建立连接。然而,我们仍然调用了 `<connection_name>` ,这样做的原因是连接可能会丢失,我们应该确保能够重新建立连接。createConnection()hostuserpassworddatabaseconnect()setTimeout()wait-for-it.shconn.on('error')

总之,我们已经竭尽全力了,但因为我们引入了一些变化,所以Dockerfile我们需要重建一切,docker-compose build然后再把一切都恢复原状:

docker-compose up

和…。

瞧,休斯顿,我们之间有联系!或者正如我的朋友巴尼喜欢说的那样:

建立数据库——填充结构和数据

好的,您可能想知道我们是如何构建服务数据库的?那部分代码docker-compose.yaml看起来是这样的:

// docker-compose.yaml

db:
  build: ./product-db
  restart: always
  environment:
    - "MYSQL_ROOT_PASSWORD=complexpassword"
    - "MYSQL_DATABASE=Products"
  ports:
    - "8002:3306"
  networks:
    - products
Enter fullscreen mode Exit fullscreen mode

我建议您特别看一下构建部分。我们在本文开头提到过,我们可以拉取现成的数据库镜像。这句话仍然有效,但通过创建我们自己的 Dockerfile,我们不仅可以指定所需的数据库,还可以运行诸如创建数据库结构和插入种子数据之类的命令。让我们仔细看一下目录product-db

/product-db
  Dockerfile
  init.sql
Enter fullscreen mode Exit fullscreen mode

好的,我们Dockerfile先来看这个:

// Dockerfile

FROM mysql:5.6

ADD init.sql /docker-entrypoint-initdb.d
Enter fullscreen mode Exit fullscreen mode

我们指定init.sql要复制并重命名该文件docker-entrypoint-initdb.d,这意味着它将在执行第一个操作时运行。很好,那么该文件的内容是什么呢init.sql

// init.sql

CREATE DATABASE IF NOT EXISTS Products;

# create tables here
# add seed data inserts here
Enter fullscreen mode Exit fullscreen mode

如您所见,它目前包含的内容不多,但我们肯定可以对其进行扩展,这很重要。

概括

本系列文章到此结束,我们已经从头开始讲解了所有内容:基本概念、核心命令、如何处理卷和数据库,以及如何更高效地使用 Docker Compose。当然,本系列文章还会继续,深入探讨 Docker 的方方面面,但希望这些内容能对您有所帮助。感谢您阅读至此。


文章来源:https://dev.to/azure/dockerfrom-the-beginning-part-v-n2c