学习 Docker:构建微服务
如果您想亲自动手学习 Docker 的所有知识,那么就不要错过了!
如果您想亲自动手学习 Docker 的所有知识,那么就不要错过了!
在本文中,我将向您展示 Docker 是如何工作的,它究竟有什么吸引力,以及 Docker 如何帮助完成一项基本的开发任务——构建微服务。
我们将以一个简单的 Node.js 服务和一个 MySQL 后端为例,从本地运行代码到运行微服务和数据库的容器。
什么是 Docker?
其核心而言,Docker 是一种软件,可以创建镜像(类似于虚拟机的模板),然后在此镜像的容器中运行实例。
Docker 维护着一个庞大的镜像仓库,称为 Docker Hub,您可以将其用作起点,也可以用作自己镜像的免费存储空间。您可以安装 Docker,选择一个您想使用的镜像,然后在容器中运行其一个实例。
在本文中,我们将构建镜像、从镜像创建容器等等。
安装 Docker
要继续阅读并使用本文,您需要 Docker。
请参阅您平台的安装指南,docs.docker.com/engine/installation。
如果您使用的是 Mac 或 Windows,可以考虑使用虚拟机。我在 Mac OS X 上使用 Parallels 来运行 Ubuntu 虚拟机,用于大多数开发活动。能够创建快照、弄乱东西然后再恢复,在实验时非常方便。
试一试
输入此命令
docker run -it ubuntu
稍作等待后,您将看到类似以下的提示
root@719059da250d:/#
尝试几个命令,然后退出容器
root@719059da250d:/# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 14.04.4 LTS
Release: 14.04
Codename: trusty
root@719059da250d:/# exit
这看起来不算什么,但已经发生了很多事情!
您现在看到的是一个隔离的容器的 bash shell,它运行着 Ubuntu,就在您的机器上。您可以随意处理它——您可以安装东西、运行软件,随心所欲。
下面是刚才发生的事情的图解和说明
- 我们发出一个 docker 命令
docker
:运行 docker 客户端run
:运行新容器的命令-it
:为容器提供交互式终端的选项ubuntu
:用于构建容器的基础镜像
- 在主机(我们的机器)上运行的 docker 服务会检查我们本地是否有请求镜像的副本——没有。
- docker 服务会检查公共仓库(docker hub)是否有一个名为
ubuntu
的可用镜像——有。 - docker 服务会下载镜像并将其存储在本地镜像缓存中(为下次做准备)。
- docker 服务会创建一个新的容器,基于
ubuntu
镜像。
尝试以下任何一种
docker run -it haskell
docker run -it java
docker run -it python
我们今天不会使用 Haskell,但您可以看到,运行环境非常容易。
构建我们自己的镜像,在上面放上我们的应用程序或服务、数据库,以及我们需要的任何东西,都非常简单。然后我们可以在任何安装了 Docker 的机器上运行它们——镜像将以相同、可预测的方式运行。我们可以将我们的软件以及它运行的环境作为代码来构建,并轻松部署。
让我们以一个简单的微服务为例。
简介
我们将构建一个微服务,使用 Node.js 和 MySQL 来管理电子邮件地址到电话号码的目录。
入门
为了进行本地开发,我们需要安装 MySQL 并创建一个测试数据库供我们...
...不行。
创建本地数据库并运行脚本很容易入门,但可能会变得混乱。有很多不受控制的事情在发生。它可能会起作用,我们甚至可以用一些添加到我们仓库的 shell 脚本来控制它,但如果其他开发人员已经安装了 MySQL 怎么办?如果他们已经有一个名为 'users' 的创意数据库,而我们想创建它呢?
第一步:在 Docker 中创建测试数据库服务器
这是一个很好的 Docker 用例。我们可能不希望在 Docker 中运行生产数据库(例如,我们可能只使用 Amazon RDS),但我们可以立即在 Docker 容器中启动一个干净的 MySQL 数据库用于开发——让我们的开发机器保持干净,并保持我们所做的一切可控和可重复。
运行以下命令
docker run --name db -e MYSQL_ROOT_PASSWORD=123 -p 3306:3306 mysql:latest
这会启动一个运行中的 MySQL 实例,允许通过端口 3306 使用 root 密码 123 进行访问。
docker run
告诉引擎我们要运行一个镜像(镜像在最后,mysql:vlatest--name db
将此容器命名为db
。-d
分离——即,在后台运行容器。-e MYSQL_ROOT_PASSWORD=123
-e
是一个标志,告诉 docker 我们要提供一个环境变量。它后面的变量是 MySQL 镜像检查以设置默认 root 密码的。-p 3306:3306
告诉引擎我们想将容器内的端口 3306 映射到外部端口 3306。
最后一部分非常重要——即使这是 MySQL 的默认端口,如果我们不明确告诉 docker 我们想映射它,它也会阻止通过该端口访问(因为容器是隔离的,直到您告诉它们您想要访问)。
此函数的返回值是容器 ID,是容器的引用,您可以使用它来停止、重新启动、在其上发出命令等。让我们看看哪些容器正在运行
$ docker ps
CONTAINER ID IMAGE ... NAMES
36e68b966fd0 mysql:latest ... db
关键信息是容器 ID、镜像和名称。让我们连接到这个镜像,看看里面有什么
$ docker exec -it db /bin/bash
root@36e68b966fd0:/# mysql -uroot -p123
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
+--------------------+
1 rows in set (0.01 sec)
mysql> exit
Bye
root@36e68b966fd0:/# exit
这也很聪明
docker exec -it db
告诉 docker 我们想在名为db
的容器上执行命令(我们也可以使用 ID,或者 ID 的前几个字母)。-it
确保我们有一个交互式终端。mysql -uroot -p123
我们实际在容器中作为进程运行的命令,在这种情况下就是 mysql 客户端。
我们可以创建数据库、表、用户,以及我们需要的任何东西。
总结测试数据库
在容器中运行 MySQL 已经介绍了一些 Docker 技巧,但让我们在这里暂停一下,继续处理服务。目前,我们将创建一个test_database文件夹,其中包含一个启动数据库、停止数据库和设置测试数据的脚本。
test_database\setup.sh
test_database\start.sh
test_database\stop.sh
启动很简单
#!/bin/sh
# Run the MySQL container, with a database named 'users' and credentials
# for a users-service user which can access it.
echo "Starting DB..."
docker run --name db -d \
-e MYSQL_ROOT_PASSWORD=123 \
-e MYSQL_DATABASE=users -e MYSQL_USER=users_service -e MYSQL_PASSWORD=123 \
-p 3306:3306 \
mysql:latest
# Wait for the database service to start up.
echo "Waiting for DB to start up..."
docker exec db mysqladmin --silent --wait=30 -uusers_service -p123 ping || exit 1
# Run the setup script.
echo "Setting up initial data..."
docker exec -i db mysql -uusers_service -p123 users < setup.sql
此脚本在一个分离的容器(即在后台)中运行数据库镜像,并设置一个用户来访问 users
数据库,然后等待数据库服务器启动,然后运行一个setup.sql脚本来设置初始数据。
setup.sql是
create table directory _
(user_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, email TEXT, phone_number TEXT);
insert into directory (email, phone_number) _
values ('homer@thesimpsons.com', '+1 888 123 1111');
insert into directory (email, phone_number) _
values ('marge@thesimpsons.com', '+1 888 123 1112');
insert into directory (email, phone_number) _
values ('maggie@thesimpsons.com', '+1 888 123 1113');
insert into directory (email, phone_number) _
values ('lisa@thesimpsons.com', '+1 888 123 1114');
insert into directory (email, phone_number) _
values ('bart@thesimpsons.com', '+1 888 123 1115');
stop.sh脚本将停止容器并将其删除(Docker 默认会保留容器,以便可以快速重启,在我们的示例中我们不需要这个功能)。
#!/bin/sh
# Stop the db and remove the container.
docker stop db && docker rm db
我们稍后会让它变得更流畅,进一步简化。请查看此时的代码,方法是查看仓库的 step1 分支。
第二步:在 Node.js 中创建微服务
这篇文章主要关注学习 Docker,所以我不会花很长时间在 Node.js 微服务上。相反,我将重点介绍一些关键领域和要点。
test-database/ # contains the code seen in Step 1
users-service/ # root of our node.js microservice
- package.json # dependencies, metadata
- index.js # main entrypoint of the app
- api/ # our apis and api tests
- config/ # config for the app
- repository/ # abstraction over our db
- server/ # server setup code
让我们逐一分解。第一个要看的部分是 repository
。将数据库访问包装在某种类或抽象中可能很有用,以便进行测试时可以模拟它。
// repository.js
//
// Exposes a single function - 'connect', which returns
// a connected repository. Call 'disconnect' on this object when you're done.
'use strict';
var mysql = require('mysql');
// Class which holds an open connection to a repository
// and exposes some simple functions for accessing data.
class Repository {
constructor(connection) {
this.connection = connection;
}
getUsers() {
return new Promise((resolve, reject) => {
this.connection.query('SELECT email, phone_number FROM directory', (err, results) => {
if(err) {
return reject(new Error("An error occured getting the users: " + err));
}
resolve((results || []).map((user) => {
return {
email: user.email,
phone_number: user.phone_number
};
}));
});
});
}
getUserByEmail(email) {
return new Promise((resolve, reject) => {
// Fetch the customer.
this.connection.query_
('SELECT email, phone_number FROM directory WHERE email = ?', [email],
(err, results) => {
if(err) {
return reject(new Error("An error occured getting the user: " + err));
}
if(results.length === 0) {
resolve(undefined);
} else {
resolve({
email: results[0].email,
phone_number: results[0].phone_number
});
}
});
});
}
disconnect() {
this.connection.end();
}
}
// One and only exported function, returns a connected repo.
module.exports.connect = (connectionSettings) => {
return new Promise((resolve, reject) => {
if(!connectionSettings.host) throw new Error("A host must be specified.");
if(!connectionSettings.user) throw new Error("A user must be specified.");
if(!connectionSettings.password) throw new Error("A password must be specified.");
if(!connectionSettings.port) throw new Error("A port must be specified.");
resolve(new Repository(mysql.createConnection(connectionSettings)));
});
};
也许有很多更好的方法可以做到这一点!但基本上,我们可以创建一个 Repository
对象,如下所示。
repository.connect({
host: "127.0.0.1",
database: "users",
user: "users_service",
password: "123",
port: 3306
}).then((repo) => {
repo.getUsers().then(users) => {
console.log(users);
});
repo.getUserByEmail('homer@thesimpsons.com').then((user) => {
console.log(user);
})
// ...when you are done...
repo.disconnect();
});
在 repository/repository.spec.js 文件中还有一组单元测试。现在我们有了仓库,就可以创建一个服务器。这是 server/server.js
// server.js
var express = require('express');
var morgan = require('morgan');
module.exports.start = (options) => {
return new Promise((resolve, reject) => {
// Make sure we have a repository and port provided
if(!options.repository) throw new Error
("A server must be started with a connected repository.");
if(!options.port) throw new Error("A server must be started with a port.");
// Create the app, add some logging
var app = express();
app.use(morgan('dev'));
// Add the APIs to the app
require('../api/users')(app, options);
// Start the app, creating a running server which we return
var server = app.listen(options.port, () => {
resolve(server);
});
});
};
此模块公开了一个 start
函数,我们可以像这样使用它。
var server = require('./server/server);
server.start({port: 8080, repo: repository}).then((svr) => {
// we've got a running http server :)
});
请注意,server.js 使用了 api/users/js?在这里。
// users.js
//
// Defines the users API. Add to a server by calling:
// require('./users')
'use strict';
// Only export - adds the API to the app with the given options.
module.exports = (app, options) => {
app.get('/users', (req, res, next) => {
options.repository.getUsers().then((users) => {
res.status(200).send(users.map((user) => { return {
email: user.email,
phoneNumber: user.phone_number
};
}));
})
.catch(next);
});
app.get('/search', (req, res) => {
// Get the email.
var email = req.query.email;
if (!email) {
throw new Error("When searching for a user, the email must be specified,
e.g: '/search?email=homer@thesimpsons.com'.");
}
// Get the user from the repo.
options.repository.getUserByEmail(email).then((user) => {
if(!user) {
res.status(404).send('User not found.');
} else {
res.status(200).send({
email: user.email,
phoneNumber: user.phone_number
});
}
})
.catch(next);
});
};
这两个文件都有相邻的源文件进行单元测试。
我们需要config。与其使用专门的库,不如使用一个简单的文件——config/config.js
// config.js
//
// Simple application configuration. Extend as needed.
module.exports = {
port: process.env.PORT || 8123,
db: {
host: process.env.DATABASE_HOST || '127.0.0.1',
database: 'users',
user: 'users_service',
password: '123',
port: 3306
}
};
我们可以根据需要 require
配置。目前,大多数配置都是硬编码的,但正如您从 port
中看到的,很容易添加环境变量作为选项。
最后一步——使用一个 index.js 文件将所有内容串联起来,它组合了所有内容。
// index.js
//
// Entrypoint to the application. Opens a repository to the MySQL
// server and starts the server.
var server = require('./server/server');
var repository = require('./repository/repository');
var config = require('./config/config');
// Lots of verbose logging when we're starting up...
console.log("--- Customer Service---");
console.log("Connecting to customer repository...");
// Log unhandled exceptions.
process.on('uncaughtException', function(err) {
console.error('Unhandled Exception', err);
});
process.on('unhandledRejection', function(err, promise){
console.error('Unhandled Rejection', err);
});
repository.connect({
host: config.db.host,
database: config.db.database,
user: config.db.user,
password: config.db.password,
port: config.db.port
}).then((repo) => {
console.log("Connected. Starting server...");
return server.start({
port: config.port,
repository: repo
});
}).then((app) => {
console.log("Server started successfully, running on port " + config.port + ".");
app.on('close', () => {
repository.disconnect();
});
});
我们有一些基本的错误处理,除此之外,我们只是加载配置,创建一个仓库并启动我们的服务器。
这就是微服务。它允许我们获取所有用户,或搜索用户。
HTTP GET /users # gets all users
HTTP GET /search?email=homer@thesimpons.com # searches by email
如果您检出代码,您会发现有一些可用的命令。
cd ./users-service
npm install # setup everything
npm test # unit test - no need for a test database running
npm start # run the server - you must have a test database running
npm run debug # run the server in debug mode, opens a browser with the inspector
npm run lint # check to see if the code is beautiful
除了您已看到的代码,我们还有
- Node Inspector 用于调试
- Mocha/shoud/supertest 用于单元测试
- ESLint 用于代码检查
就是这样!
使用以下命令运行测试数据库
cd test-database/
./start.sh
然后是服务
cd ../users-service/
npm start
您可以在浏览器中访问 localhost:8123/users,看看它的运行情况。
我们很快就完成了服务构建。如果您想在我们继续之前查看此代码,请查看 step2 分支。
第三步:Docker 化我们的微服务
好了,现在开始有趣的部分了!
所以我们有一个微服务,可以在开发机器上运行,只要它安装了兼容版本的 Node.js。我们想要做的是设置我们的服务,以便我们可以从中创建一个Docker 镜像,从而能够在支持 Docker 的任何地方部署我们的服务。
我们的做法是创建一个Dockerfile。Dockerfile 是一个配方,告诉 Docker 引擎如何构建您的镜像。我们将在 users-service
目录中创建一个简单的 Dockerfile,并开始探索如何根据我们的需求对其进行调整。
创建 Dockerfile
在 users-service/ 中创建一个名为 Dockerfile 的新文本文件,内容如下。
# Use Node v4 as the base image.
FROM node:4
# Run node
CMD ["node"]
现在运行以下命令来构建镜像并从中运行一个容器。
docker build -t node4 . # Builds a new image
docker run -it node4 # Run a container with this image, interactive
让我们先看看构建命令。
docker build
告诉引擎我们要创建一个新镜像。-t node4
用标签node4
标记此镜像。从现在开始,我们可以通过标签引用此镜像。.
使用当前目录来查找Dockerfile
。
经过一些控制台输出后,您将看到我们创建了一个新镜像。您可以使用 docker images
查看系统上的所有镜像。接下来的命令应该和我们到目前为止做的事情非常熟悉。
docker run
从镜像运行新容器。-it
使用交互式终端。node4
我们想在容器中使用的镜像的标签。
当我们运行这个镜像时,我们会得到一个 node repl,像这样检查当前版本。
> process.version
'v4.4.0'
> process.exit(0)
这可能与您当前机器上的 Node.js 版本不同。
检查 Dockerfile
看看 Dockerfile,我们可以很容易地看到发生了什么。
FROM node:4
我们在 Dockerfile 中首先指定的是基础镜像。快速搜索一下会找到 Docker Hub 上的 Node.js 组织页面,其中显示了所有可用的镜像。这基本上是一个带有 Node.js 安装的 Ubuntu 基础系统。RUN ["node"]
RUN
命令告诉 docker 这个镜像应该运行 node 可执行文件。当可执行文件终止时,容器就会关闭。
通过添加一些额外的命令,我们可以更新 Dockerfile,使其能够运行我们的服务。
# Use Node v4 as the base image.
FROM node:4
# Add everything in the current directory to our image, in the 'app' folder.
ADD . /app
# Install dependencies
RUN cd /app; \
npm install --production
# Expose our server port.
EXPOSE 8123
# Run our app.
CMD ["node", "/app/index.js"]
这里唯一的添加是我们使用 ADD
命令将当前目录中的所有内容1 复制到容器内的 app/ 文件夹中。然后我们使用 RUN
在镜像中运行命令,该命令安装我们的模块。最后,我们 EXPOSE
服务器端口,告诉 docker 我们打算支持 8123
上的入站连接,然后运行我们的服务器代码。
确保测试数据库服务正在运行,然后再次构建并运行镜像。
docker build -t users-service .
docker run -it -p 8123:8123 users-service
如果您在浏览器中导航到 localhost:8123/users,您应该会看到一个错误。查看控制台会显示我们的容器报告了一些问题。
--- Customer Service---
Connecting to customer repository...
Connected. Starting server...
Server started successfully, running on port 8123.
GET /users 500 23.958 ms - 582
Error: An error occured getting the users: Error: connect ECONNREFUSED 127.0.0.1:3306
at Query._callback (/app/repository/repository.js:21:25)
at Query.Sequence.end (/app/node_modules/mysql/lib/protocol/sequences/Sequence.js:96:24)
at /app/node_modules/mysql/lib/protocol/Protocol.js:399:18
at Array.forEach (native)
at /app/node_modules/mysql/lib/protocol/Protocol.js:398:13
at nextTickCallbackWith0Args (node.js:420:9)
at process._tickCallback (node.js:349:13)
糟糕!因此,从我们的 users-service
容器到 test-database
容器的连接被拒绝了。我们可以尝试运行 docker ps
来查看所有正在运行的容器。
CONTAINER ID IMAGE PORTS NAMES
a97958850c66 users-service 0.0.0.0:8123->8123/tcp kickass_perlman
47f91343db01 mysql:latest 0.0.0.0:3306->3306/tcp db
它们都在那里,那是什么情况?
链接容器
我们遇到的问题实际上是意料之中的。Docker 容器应该被隔离,所以如果我们可以在没有明确允许的情况下在容器之间建立连接,那就没有意义了。
是的,我们可以从我们的机器(主机)连接到容器,因为我们已经为此打开了端口(例如,使用了 -p 8123:8123
参数)。如果我们允许容器以同样的方式互相通信,那么在同一台机器上运行的两个容器就可以通信,即使开发人员没有意图,而且这会导致灾难,特别是当我们可能有一个机器集群负责运行不同应用程序的容器时。
如果我们要在容器之间建立连接,我们需要链接它们,这告诉 docker 我们明确希望允许它们之间的通信。有两种方法可以做到这一点,第一种是“老式”但非常简单的方法,第二种我们稍后会看到。
使用“link”参数链接容器
当我们运行一个容器时,我们可以使用 link
参数告诉 docker 我们打算连接到另一个容器。在我们的例子中,我们可以像这样正确运行我们的服务。
docker run -it -p 8123:8123 --link db:db -e DATABASE_HOST=DB users-service
docker run -it
在容器中运行一个 docker 镜像,并带有一个交互式终端。-p 8123:8123
将主机端口 8123 映射到容器端口 8123。link db:db
链接到名为db
的容器,并将其称为db
。-e DATABASE_HOST=db
将DATABASE_HOST
环境变量设置为db
。users-service
要在我们的容器中运行的镜像的名称。
现在当我们访问 localhost:8123/users
时,一切都正常了。
工作原理
还记得服务config文件吗?它允许我们通过环境变量指定数据库主机。
// config.js
//
// Simple application configuration. Extend as needed.
module.exports = {
port: process.env.PORT || 8123,
db: {
host: process.env.DATABASE_HOST || '127.0.0.1',
database: 'users',
user: 'users_service',
password: '123',
port: 3306
}
};
当我们运行容器时,我们将此环境变量设置为 DB
,这意味着我们正在连接到名为 DB
的主机。当我们链接到容器时,docker 引擎会自动为我们设置此项。
为了实际看到这一点,请尝试运行 docker ps
来列出所有正在运行的容器。查找运行 users-service
的容器名称,它将是一个随机名称,例如 trusting_jang
。
docker ps
CONTAINER ID IMAGE ... NAMES
ac9449d3d552 users-service ... trusting_jang
47f91343db01 mysql:latest ... db
现在我们可以查看我们容器上可用的主机。
docker exec trusting_jang cat /etc/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.2 db 47f91343db01 # linking magic!!
172.17.0.3 ac9449d3d552
还记得 docker exec
是如何工作的吗?选择一个容器名称,然后后面的任何内容都是您将在容器上执行的命令,在我们的例子中是 cat /etc/hosts。
好的,hosts 文件没有 # linking magic!!
注释,那是为了让您看到——docker 已经将 db
添加到我们的 hosts 文件中,以便我们可以通过主机名引用链接的容器。这是链接的后果之一。这是另一个。
docker exec trusting_jang printenv | grep DB
DB_PORT=tcp://172.17.0.2:3306
DB_PORT_3306_TCP=tcp://172.17.0.2:3306
DB_PORT_3306_TCP_ADDR=172.17.0.2
DB_PORT_3306_TCP_PORT=3306
DB_PORT_3306_TCP_PROTO=tcp
DB_NAME=/trusting_jang/db
从这个命令,我们还可以看到,当 docker 链接容器时,它还会提供一组包含有用信息的环境变量。我们知道主机、TCP 端口和容器名称。
这样第三步就完成了——我们有一个 MySQL 数据库在一个容器中愉快地运行,我们有一个 Node.js 微服务,可以在本地或它自己的容器中运行,并且我们知道如何将它们链接在一起。
您可以通过访问 step3 分支来查看此阶段的代码。
第四步:集成测试环境
我们现在可以编写一个集成测试,该测试调用实际的服务器,运行为一个 docker 容器,并调用容器化的测试数据库。
编写集成测试可以使用任何您想要的语言或平台,在合理范围内,但为了保持简单,我将使用 Node.js,因为我们已经在项目中看到了 Mocha 和 Supertest。
在一个名为 integration-tests 的新文件夹中,我们有一个单独的 index.js 文件。
var supertest = require('supertest');
var should = require('should');
describe('users-service', () => {
var api = supertest('https://:8123');
it('returns a 200 for a known user', (done) => {
api.get('/search?email=homer@thesimpsons.com')
.expect(200, done);
});
});
这将检查 API 调用并显示测试结果2。
只要您的 users-service
和 test-database
正在运行,测试就会通过。然而,在这一点上,服务开始变得有点难以处理。
- 我们必须使用 shell 脚本来启动和停止数据库。
- 我们必须记住一系列命令来启动用户服务并连接到数据库。
- 我们必须直接使用 node 来运行集成测试。
现在我们对 Docker 有了一些了解,我们可以解决这些问题。
简化测试数据库
目前,我们有以下用于测试数据库的文件。
/test-database/start.sh
/test-database/stop.sh
/test-database/setup.sql
现在我们对 Docker 更熟悉了,我们可以改进这一点。查看 Docker Hub 上的 MySQL 镜像文档,其中有一条说明告诉我们,添加到镜像的 /docker-entrypoint-initdb.d 文件夹中的任何 .sql 或 .sh 文件都将在设置数据库时执行。
这意味着我们可以用一个 Dockerfile 替换我们的 start.sh 和 stop.sh 脚本。
FROM mysql:5
ENV MYSQL_ROOT_PASSWORD 123
ENV MYSQL_DATABASE users
ENV MYSQL_USER users_service
ENV MYSQL_PASSWORD 123
ADD setup.sql /docker-entrypoint-initdb.d
现在要运行我们的测试数据库,只需要。
docker build -t test-database .
docker run --name db test-database
组合
构建和运行每个容器仍然有些耗时。我们可以使用 Docker Compose 工具进一步改进。
Docker Compose 允许您创建一个文件,该文件定义系统中的每个容器、它们之间的关系,并构建或运行所有容器。
首先,安装 Docker Compose。现在在项目根目录中创建一个名为 docker-compose.yml 的新文件。
version: '2'
services:
users-service:
build: ./users-service
ports:
- "8123:8123"
depends_on:
- db
environment:
- DATABASE_HOST=db
db:
build: ./test-database
现在来看看这个。
docker-compose build
docker-compose up
Docker Compose 已经构建了我们的应用程序所需的所有镜像,从它们创建了容器,按正确的顺序运行了它们,并启动了整个堆栈!
docker-compose build
命令会构建 docker-compose.yml 文件中列出的每个镜像。
version: '2'
services:
users-service:
build: ./users-service
ports:
- "8123:8123"
depends_on:
- db
environment:
- DATABASE_HOST=db
db:
build: ./test-database
我们每个服务的 build
值告诉 docker 去哪里查找 Dockerfile
。当我们运行 docker-compose up
时,docker 会启动我们所有的服务。从 Dockerfile
中,我们可以看到,我们可以指定端口和依赖关系。实际上,我们可以在这里更改很多配置。
在另一个终端中,运行 docker compose down
来正常关闭容器。
总结
我们在本文中看到了很多 Docker 的内容,但它还有更多。我希望这能展示一些您可以在工作流程中使用 Docker 进行的有趣且有用的事情。
一如既往,欢迎提问和评论!我还要强烈推荐文档 Understanding Docker,以便更深入地了解 Docker 的工作原理。
您可以在 github.com/dwmkerr/node-docker-mircroservice 上查看本文构建的项目的最终源代码。
注释
-
复制所有内容实际上是个坏主意,因为我们也会复制 undefined 文件夹。通常,最好明确列出要复制的文件或文件夹,或者使用一个 undefined 文件,它的工作方式类似于 undefined 文件。 ↩
-
如果服务器未运行,它实际上会显示一个非常烦人的异常,这是由于 supertest 的一个 bug 造成的,请参阅 github.com/visionmedia/supertest/issues/314。 ↩