使用Docker Compose 将 Node.js 应用程序进行容器化,用于开发
介绍
如果你正在积极开发一个应用程序,使用Docker可以简化你的工作流程和将应用程序部署到生产环境的过程。在开发过程中使用容器有以下好处:
- Environments are consistent, meaning that you can choose the languages and dependencies you want for your project without worrying about system conflicts.
- Environments are isolated, making it easier to troubleshoot issues and onboard new team members.
- Environments are portable, allowing you to package and share your code with others.
本教程将向您展示如何使用Docker为Node.js应用程序设置开发环境。您将使用Docker Compose创建两个容器-一个用于Node应用程序,另一个用于MongoDB数据库。由于该应用程序需要使用Node和MongoDB,您的设置将执行以下操作:
- Synchronize the application code on the host with the code in the container to facilitate changes during development.
- Ensure that changes to the application code work without a restart.
- Create a user and password-protected database for the application’s data.
- Persist this data.
在本教程的最后,您将拥有一个在Docker容器中运行的可用的鲨鱼信息应用程序。
前提条件
要遵循这个教程,你需要:
- A development server running Ubuntu 18.04, along with a non-root user with sudo privileges and an active firewall. For guidance on how to set these up, please see this Initial Server Setup guide.
- Docker installed on your server, following Steps 1 and 2 of How To Install and Use Docker on Ubuntu 18.04.
- Docker Compose installed on your server, following Step 1 of How To Install Docker Compose on Ubuntu 18.04.
步骤1——克隆项目并修改依赖项。
搭建此设置的第一步是克隆项目代码并修改其 package.json 文件,其中包含了项目的依赖项。您将在项目的 devDependencies 中添加 nodemon,并指定在开发过程中将使用它。使用 nodemon 运行应用程序可确保在您对代码进行更改时自动重新启动。
首先,从Silicon Cloud社区的GitHub账户克隆nodejs-mongo-mongoose存储库。该存储库包含了在《如何将MongoDB与Node应用程序集成》中描述的设置的代码,该说明了如何使用Mongoose将MongoDB数据库与现有的Node应用程序集成。
将存储库克隆到名为node_project的目录中。
- git clone https://github.com/do-community/nodejs-mongo-mongoose.git node_project
请前往 node_project 目录。
- cd node_project
使用nano或者你喜欢的编辑器打开项目的package.json文件。
- nano package.json
在项目依赖之下、闭合大括号之上,创建一个新的devDependencies对象,其中包括nodemon。
以下是~/node_project/package.json的原文中文释义:
“~” 代表用户的 home 目录
“node_project” 是一个文件夹名字
“package.json” 是一个 JSON 格式的配置文件。
...
"dependencies": {
"ejs": "^2.6.1",
"express": "^4.16.4",
"mongoose": "^5.4.10"
},
"devDependencies": {
"nodemon": "^1.18.10"
}
}
当你编辑完成后,保存并关闭文件。如果你使用的是nano文本编辑器,按下CTRL+X,然后按Y,最后按ENTER。
已经设置了项目代码并修改了相关依赖项,你可以开始重构代码以适应容器化工作流程。
第二步-配置您的应用程序以与容器一起工作
将您的应用程序修改为容器化工作流意味着使您的代码更加模块化。容器在不同环境之间提供可移植性,因此您的代码应尽可能与底层操作系统解耦。为了实现这一点,您将重构代码,更多地利用Node的process.env属性。该属性返回一个包含运行时用户环境信息的对象。您可以在代码中使用该对象,通过环境变量在运行时动态分配配置信息。
从app.js开始,您的主应用程序入口点。打开文件:
- nano app.js
在内部,您将看到一个端口常量的定义,以及一个使用该常量来指定应用程序将监听的端口的监听函数。
~/home/node_project/app.js
...
const port = 8080;
...
app.listen(port, function () {
console.log('Example app listening on port 8080!');
});
通过使用 process.env 对象在运行时允许动态分配端口常量,重新定义端口常量。对常量定义和监听函数进行以下更改:
~/home/node_project/app.js 可以用以下的方式在中文中表达:
家目录/节点项目/app.js
...
const port = process.env.PORT || 8080;
...
app.listen(port, function () {
console.log(`Example app listening on ${port}!`);
});
您的新常量定义会动态地使用运行时传入的值或8080端口来分配端口。同样地,您已经重写了监听函数,使用了模板文字,当监听连接时,它将插值端口值。因为您将在其他地方映射您的端口,这些修改将避免您不断修改此文件以适应环境变化。
当你编辑完成后,保存并关闭文件。
接下来,您将修改数据库连接信息,以删除任何配置凭据。打开包含此信息的db.js文件。
- nano db.js
目前,该文件具有以下功能:
- Imports Mongoose, the Object Document Mapper (ODM) that you’re using to create schemas and models for your application data.
- Sets the database credentials as constants, including the username and password.
- Connects to the database using the mongoose.connect method.
有关此文件的更多信息,请参见《如何将MongoDB与您的Node应用程序集成》第3步。
您修改文件的第一步将是重新定义包含敏感信息的常量。当前,这些常量的样式如下:
...
const MONGO_USERNAME = 'sammy';
const MONGO_PASSWORD = 'your_password';
const MONGO_HOSTNAME = '127.0.0.1';
const MONGO_PORT = '27017';
const MONGO_DB = 'sharkinfo';
...
可以使用 process.env 对象来捕获这些常量的运行时值,而不是将这些信息硬编码。将该块修改如下:
...
const {
MONGO_USERNAME,
MONGO_PASSWORD,
MONGO_HOSTNAME,
MONGO_PORT,
MONGO_DB
} = process.env;
...
编辑完成后,请保存并关闭文件。
在这一点上,您已经修改了db.js以与您的应用程序环境变量配合使用,但您仍然需要一种将这些变量传递给您的应用程序的方式。创建一个.env文件,并提供值以在运行时传递给您的应用程序。
打开文件。
- nano .env
这个文件将包含你从db.js中移除的信息:你的应用程序数据库的用户名和密码,以及端口设置和数据库名称。记得使用你自己的信息更新这里列出的用户名、密码和数据库名称。
MONGO_USERNAME=sammy
MONGO_PASSWORD=your_password
MONGO_PORT=27017
MONGO_DB=sharkinfo
请注意,你已经删除了原始出现在db.js中的主机设置。现在,你需要在Docker Compose文件的级别上定义你的主机,同时提供其他关于你的服务和容器的信息。
在您完成编辑后,请保存并关闭此文件。
由于您的.env文件包含敏感信息,您将希望确保它被包含在您项目的.dockerignore和.gitignore文件中,以防止其复制到版本控制或容器中。
打开你的 .dockerignore 文件。
- nano .dockerignore
请将以下内容添加到文件的底部:
只需要一种选项,以下是对“~/node_project/.dockerignore”进行中文本地化解释:
「只需一个路径选择,表示“~/node_project/.dockerignore”。」
...
.gitignore
.env
在你完成编辑后保存并关闭文件。
这个代码仓库中的.gitignore文件已经包含了.env文件,但是请随意检查它是否存在。
- nano .gitignore
...
.env
...
在这一点上,您已成功从项目代码中提取出敏感信息,并采取措施来控制这些信息的复制方式和位置。现在,您可以优化数据库连接代码,以适应容器化工作流程。
第三步 – 修改数据库连接设置
你下一步需要做的是通过添加处理应用程序无法连接到数据库的代码,使你的数据库连接方法更加强健可靠。当使用Compose来工作容器时,引入这种弹性水平到你的应用程序代码是一种推荐的做法。
打开db.js进行编辑。
- nano db.js
注意之前添加的代码,以及用于Mongo连接URI的URL常量和Mongoose连接方法。
...
const {
MONGO_USERNAME,
MONGO_PASSWORD,
MONGO_HOSTNAME,
MONGO_PORT,
MONGO_DB
} = process.env;
const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`;
mongoose.connect(url, {useNewUrlParser: true});
目前,你的connect方法接受一个选项,告诉Mongoose使用Mongo的新URL解析器。你可以在这个方法中添加选项来定义重新连接尝试的参数。通过创建一个options常量来包括相关信息,除了新的URL解析器选项。在你的Mongo常量下方,添加以下对options常量的定义:
~/node_project/db.js~/node_project/db.js 改写为:~/节点项目/数据库.js
...
const {
MONGO_USERNAME,
MONGO_PASSWORD,
MONGO_HOSTNAME,
MONGO_PORT,
MONGO_DB
} = process.env;
const options = {
useNewUrlParser: true,
reconnectTries: Number.MAX_VALUE,
reconnectInterval: 500,
connectTimeoutMS: 10000,
};
...
reconnectTries选项告诉Mongoose无限期地继续尝试连接,而reconnectInterval以毫秒为单位定义连接尝试之间的时间间隔。connectTimeoutMS将Mongo驱动程序在连接尝试失败之前等待10秒钟。
现在你可以在Mongoose的connect方法中使用新的选项常量来微调你的Mongoose连接设置。你还需要添加一个承诺来处理可能的连接错误。
目前,Mongoose 的连接方法长这样:
...
mongoose.connect(url, {useNewUrlParser: true});
删除现有的连接方法,并用包含options常量和promise的以下代码替换。
~ /node_project/db.js~/node_project/db.js
请将以上路径更改为本地文件路径:~/node_project/db.js。
...
mongoose.connect(url, options).then( function() {
console.log('MongoDB is connected');
})
.catch( function(err) {
console.log(err);
});
在成功连接的情况下,您的函数将记录一条适当的消息;否则,它将捕获并记录错误,以便您进行故障排除。
完成的文件将如下所示:
~/node_project/db.js
const mongoose = require('mongoose');
const {
MONGO_USERNAME,
MONGO_PASSWORD,
MONGO_HOSTNAME,
MONGO_PORT,
MONGO_DB
} = process.env;
const options = {
useNewUrlParser: true,
reconnectTries: Number.MAX_VALUE,
reconnectInterval: 500,
connectTimeoutMS: 10000,
};
const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`;
mongoose.connect(url, options).then( function() {
console.log('MongoDB is connected');
})
.catch( function(err) {
console.log(err);
});
在编辑完成后,保存并关闭文件。
您现在已经为应用程序代码添加了弹性,以处理应用程序无法连接到数据库的情况。有了这段代码,您可以继续使用Compose定义您的服务。
第四步 – 使用Docker Compose定义服务
在你重构了代码之后,你可以开始编写docker-compose.yml文件来定义你的服务。Compose中的服务是运行着的容器,而服务定义——你将在docker-compose.yml文件中包含的信息——包含了每个容器镜像将如何运行的信息。Compose工具允许你定义多个服务来构建多容器应用。
在定义您的服务之前,您将向您的项目添加一个名为“wait-for”的工具,以确保您的应用程序仅在数据库启动任务完成后才尝试连接数据库。这个包装脚本使用netcat来轮询特定的主机和端口是否接受TCP连接。使用它可以通过测试数据库是否准备好接受连接来控制应用程序连接数据库的尝试。
尽管Compose允许您使用depends_on选项来指定服务之间的依赖关系,但这种顺序是基于容器是否正在运行,而不是它的就绪状态。使用depends_on对于您的设置来说并不是最佳选择,因为您希望仅在数据库启动任务完成(包括向管理员身份验证数据库添加用户和密码)后,应用程序才连接。有关使用wait-for和其他工具控制启动顺序的更多信息,请参阅Compose文档中的相关建议。
打开一个名为wait-for.sh的文件。
- nano wait-for.sh
输入以下代码到文件中以创建轮询函数:
#!/bin/sh
# original script: https://github.com/eficode/wait-for/blob/master/wait-for
TIMEOUT=15
QUIET=0
echoerr() {
if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi
}
usage() {
exitcode="$1"
cat << USAGE >&2
Usage:
$cmdname host:port [-t timeout] [-- command args]
-q | --quiet Do not output any status messages
-t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout
-- COMMAND ARGS Execute command with args after the test finishes
USAGE
exit "$exitcode"
}
wait_for() {
for i in `seq $TIMEOUT` ; do
nc -z "$HOST" "$PORT" > /dev/null 2>&1
result=$?
if [ $result -eq 0 ] ; then
if [ $# -gt 0 ] ; then
exec "$@"
fi
exit 0
fi
sleep 1
done
echo "Operation timed out" >&2
exit 1
}
while [ $# -gt 0 ]
do
case "$1" in
*:* )
HOST=$(printf "%s\n" "$1"| cut -d : -f 1)
PORT=$(printf "%s\n" "$1"| cut -d : -f 2)
shift 1
;;
-q | --quiet)
QUIET=1
shift 1
;;
-t)
TIMEOUT="$2"
if [ "$TIMEOUT" = "" ]; then break; fi
shift 2
;;
--timeout=*)
TIMEOUT="${1#*=}"
shift 1
;;
--)
shift
break
;;
--help)
usage 0
;;
*)
echoerr "Unknown argument: $1"
usage 1
;;
esac
done
if [ "$HOST" = "" -o "$PORT" = "" ]; then
echoerr "Error: you need to provide a host and port to test."
usage 2
fi
wait_for "$@"
当你完成添加代码后,请保存并关闭文件。
将脚本设为可执行。
- chmod +x wait-for.sh
接下来,打开docker-compose.yml文件。
- nano docker-compose.yml
首先,通过将以下代码添加到文件中来定义Node.js应用程序服务:
version: '3'
services:
nodejs:
build:
context: .
dockerfile: Dockerfile
image: nodejs
container_name: nodejs
restart: unless-stopped
env_file: .env
environment:
- MONGO_USERNAME=$MONGO_USERNAME
- MONGO_PASSWORD=$MONGO_PASSWORD
- MONGO_HOSTNAME=db
- MONGO_PORT=$MONGO_PORT
- MONGO_DB=$MONGO_DB
ports:
- "80:8080"
volumes:
- .:/home/node/app
- node_modules:/home/node/app/node_modules
networks:
- app-network
command: ./wait-for.sh db:27017 -- /home/node/app/node_modules/.bin/nodemon app.js
Node.js 服务定义包括以下选项:
- build: This defines the configuration options, including the context and dockerfile, that will be applied when Compose builds the application image. If you wanted to use an existing image from a registry like Docker Hub, you could use the image instruction instead, with information about your username, repository, and image tag.
- context: This defines the build context for the image build — in this case, the current project directory.
- dockerfile: This specifies the Dockerfile in your current project directory as the file Compose will use to build the application image. For more information about this file, please see How To Build a Node.js Application with Docker.
- image, container_name: These apply names to the image and container.
- restart: This defines the restart policy. The default is no, but you have set the container to restart unless it is stopped.
- env_file: This tells Compose that you would like to add environment variables from a file called .env, located in your build context.
- environment: Using this option allows you to add the Mongo connection settings you defined in the .env file. Note that you are not setting NODE_ENV to development, since this is Express’s default behavior if NODE_ENV is not set. When moving to production, you can set this to production to enable view caching and less verbose error messages.
Also note that you have specified the db database container as the host, as discussed in Step 2. - ports: This maps port 80 on the host to port 8080 on the container.
- volumes: You are including two types of mounts here:The first is a bind mount that mounts your application code on the host to the /home/node/app directory on the container. This will facilitate rapid development, since any changes you make to your host code will be populated immediately in the container.
The second is a named volume, node_modules. When Docker runs the npm install instruction listed in the application Dockerfile, npm will create a new node_modules directory on the container that includes the packages required to run the application. The bind mount you just created will hide this newly created node_modules directory, however. Since node_modules on the host is empty, the bind will map an empty directory to the container, overriding the new node_modules directory and preventing your application from starting. The named node_modules volume solves this problem by persisting the contents of the /home/node/app/node_modules directory and mounting it to the container, hiding the bind.
Note
使用这种方法时,请记住以下几点:
您的绑定将把容器中的node_modules目录的内容挂载到主机上,并且该目录的所有者将为root,因为这个命名卷是由Docker创建的。
如果主机上已经存在一个预先存在的node_modules目录,则会覆盖在容器中创建的node_modules目录。本教程中构建的设置假设您在主机上没有预先存在的node_modules目录,也不会在主机上使用npm。这符合应用程序开发的十二要素方法,该方法最大限度地减少执行环境之间的依赖关系。
- networks: This specifies that your application service will join the app-network network, which you will define at the bottom of the file.
- command: This option lets you set the command that should be executed when Compose runs the image. Note that this will override the CMD instruction that you set in our application Dockerfile. Here, you are running the application using the wait-for script, which will poll the db service on port 27017 to test whether the database service is ready. Once the readiness test succeeds, the script will execute the command you have set, /home/node/app/node_modules/.bin/nodemon app.js, to start the application with nodemon. This will ensure that any future changes you make to your code are reloaded without your having to restart the application.
接下来,在应用程序服务定义之后添加以下代码来创建数据库服务。
~/node_project/docker-compose.yml项目路径下的docker-compose.yml文件。
...
db:
image: mongo:4.1.8-xenial
container_name: db
restart: unless-stopped
env_file: .env
environment:
- MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME
- MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD
volumes:
- dbdata:/data/db
networks:
- app-network
对于nodejs服务,一些设置保持不变,但您还对图像、环境和卷的定义进行了以下更改:
- image: To create this service, Compose will pull the 4.1.8-xenial Mongo image from Docker Hub. You are pinning a particular version to avoid possible future conflicts as the Mongo image changes. For more information about version pinning, please see the Docker documentation on Dockerfile best practices.
- MONGO_INITDB_ROOT_USERNAME, MONGO_INITDB_ROOT_PASSWORD: The mongo image makes these environment variables available so that you can modify the initialization of your database instance. MONGO_INITDB_ROOT_USERNAME and MONGO_INITDB_ROOT_PASSWORD together create a root user in the admin authentication database and ensure that authentication is enabled when the container starts. You have set MONGO_INITDB_ROOT_USERNAME and MONGO_INITDB_ROOT_PASSWORD using the values from your .env file, which you pass to the db service using the env_file option. Doing this means that your sammy application user will be a root user on the database instance, with access to all the administrative and operational privileges of that role. When working in production, you will want to create a dedicated application user with appropriately scoped privileges.
Note
- dbdata:/data/db: The named volume dbdata will persist the data stored in Mongo’s default data directory, /data/db. This will ensure that you don’t lose data in cases where you stop or remove containers.
将db服务也添加到了app-network网络中,使用networks选项。
作为最后一步,将容量和网络定义添加到文件底部。
~/node_project/docker-compose.yml
...
networks:
app-network:
driver: bridge
volumes:
dbdata:
node_modules:
用户定义的桥接网络app-network能够在同一Docker守护主机上的容器之间实现通信。这简化了应用程序内部的流量和通信,因为它在相同的桥接网络上打开了所有端口以便容器间通信,同时不向外界暴露任何端口。因此,您的数据库和Node.js容器可以相互通信,您只需要暴露80号端口以便前端访问应用程序。
您的顶级卷密钥定义了卷dbdata和node_modules。当Docker创建卷时,卷的内容存储在由Docker管理的主机文件系统的一部分/var/lib/docker/volumes/中。每个卷的内容存储在/var/lib/docker/volumes/下的一个目录中,并且被挂载到使用该卷的任何容器中。通过这种方式,您的用户创建的鲨鱼信息数据将持久保存在dbdata卷中,即使您删除并重新创建db容器。
最终的docker-compose.yml文件将如下所示:
~/node_project/docker-compose.yml :
请把上述文件的内容翻译成中文的一个选项:
version: '3'
services:
nodejs:
build:
context: .
dockerfile: Dockerfile
image: nodejs
container_name: nodejs
restart: unless-stopped
env_file: .env
environment:
- MONGO_USERNAME=$MONGO_USERNAME
- MONGO_PASSWORD=$MONGO_PASSWORD
- MONGO_HOSTNAME=db
- MONGO_PORT=$MONGO_PORT
- MONGO_DB=$MONGO_DB
ports:
- "80:8080"
volumes:
- .:/home/node/app
- node_modules:/home/node/app/node_modules
networks:
- app-network
command: ./wait-for.sh db:27017 -- /home/node/app/node_modules/.bin/nodemon app.js
db:
image: mongo:4.1.8-xenial
container_name: db
restart: unless-stopped
env_file: .env
environment:
- MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME
- MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD
volumes:
- dbdata:/data/db
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
dbdata:
node_modules:
在你完成编辑后保存并关闭文件。
在您已经设置了服务定义之后,您就可以开始应用程序了。
第五步 -测试应用程序
当您的docker-compose.yml文件就位后,您可以使用docker-compose up命令创建您的服务。您还可以通过使用docker-compose down命令停止和删除容器来测试您的数据是否会持久保存。
首先,通过运行带有-d标志的docker-compose up命令,构建容器镜像并创建服务,然后将在后台运行nodejs和db容器。
- docker-compose up -d
输出确认您的服务已经创建。
… Creating db … done Creating nodejs … done
您还可以通过显示服务的日志输出来获取关于启动过程的更详细信息。
- docker-compose logs
如果一切开始正确,以下是输出结果。
… nodejs | [nodemon] starting `node app.js` nodejs | Example app listening on 8080! nodejs | MongoDB is connected … db | 2019-02-22T17:26:27.329+0000 I ACCESS [conn2] Successfully authenticated as principal sammy on admin
您也可以使用docker-compose ps命令来检查您的容器状态。
- docker-compose ps
输出表明你的容器正在运行。
Name Command State Ports ———————————————————————- db docker-entrypoint.sh mongod Up 27017/tcp nodejs ./wait-for.sh db:27017 — … Up 0.0.0.0:80->8080/tcp
启动您的服务后,您可以在浏览器中访问http://your_server_ip。
点击“获取鲨鱼信息”按钮,进入一个包含输入表单的页面,您可以在页面上提交鲨鱼名称和鲨鱼一般特征描述。
在表格中,添加您选择的一种鲨鱼。为了进行演示,请将巨牙鲨添加到鲨鱼名称栏,并在鲨鱼特征栏中添加“古老”。
点击提交按钮,会将包含这些鲨鱼信息的页面显示给您。
作为最后一步,测试一下你刚刚输入的数据是否会在删除数据库容器后继续存在。
回到你的终端,输入以下命令来停止和删除你的容器和网络:
- docker-compose down
请注意,您没有包括“–volumes”选项,因此您的“dbdata”卷不会被删除。
以下输出确认您的容器和网络已被移除。
Stopping nodejs … done Stopping db … done Removing nodejs … done Removing db … done Removing network node_project_app-network
重新创建容器
- docker-compose up -d
现在返回到鲨鱼信息表:
选择一种新的鲨鱼进入。本例将使用座头鲸鲨和大型:
一旦您点击提交按钮,您会注意到新的鲨鱼已经添加到您的数据库的鲨鱼收藏中,而且您已经输入的数据没有丢失。
你的应用程序现在正在运行在启用了数据持久化和代码同步的Docker容器上。
结论
通过遵循这个教程,你已经使用 Docker 容器为你的 Node 应用程序建立了开发环境。通过提取敏感信息并将应用程序状态与应用程序代码解耦,你使得你的项目更加模块化和易于携带。你还配置了一个模板化的 docker-compose.yml 文件,可以根据你的开发需求和要求进行修改。
随着你的发展,你可能对学习如何设计容器化和云原生工作流的应用程序更感兴趣。请参阅有关 Kubernetes 应用架构和 Kubernetes 应用现代化的更多信息。
请查看《如何使用Docker构建Node.js应用程序》和《如何将MongoDB集成到您的Node应用程序中》以了解本教程中使用的代码。如果想了解使用容器在Nginx反向代理上部署Node应用程序的信息,请查看《如何使用Nginx、Let’s Encrypt和Docker Compose保护容器化的Node.js应用程序》。