[docker] 多容器项目
相当于把之前学的一些东西全都整合一下,做一个小型的项目:
这里的数据库、前端、后端可以为任何框架,并不指定为特定框架
project structure
省略掉了一些实现,大概结构如下:
❯ tree
.
├── backend
│ ├── Dockerfile
│ ├── app.js
│ ├── logs
│ │ └── access.log
│ ├── models
│ │ └── goal.js
│ ├── node_modules
│ ├── package-lock.json
│ └── package.json
└── frontend
├── Dockerfile
├── README.md
├── package-lock.json
├── package.json
├── public
└── src
├── App.js
├── components
├── index.css
└── index.js
11 directories, 31 files
这里主要讲的是 Docker,所以代码部分不会涉及
创建网络
为 [docker] 网络连接 的内容
❯ docker network create goals-net
e57f83cac82e93cdcbab09f515bb037c1990cafeaf77a3622a48059aace1d744
mongodb 容器化及持久化
为 [docker] 网络连接,[docker] 数据的持久化 - Volume & bind mounts 和 [docker] volume 补充 & 环境变量 & 参数 的内容
仅容器化
❯ docker run -d --name mongodb --rm --network goals-net mongo
fc6ac2296a12b31a68b96fe7beb4f5a34a22c13c9f925b5242d8ab477f877920
❯ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
fc6ac2296a12 mongo "docker-entrypoint.s…" 12 seconds ago Up 11 seconds 27017/tcp mongodb
这一部分的实现是起一个 mongodb 的容器,不存在任何的持久化和安全的实现
也就是说:
- 任何人都可以访问当前 mongodb 实例
- 一旦容器被删除,新的 mongodb 容器生成,那么数据就会消失
持久化数据
这里会使用 volume 去对数据进行一个持久化,这样新起来一个 mongodb 依旧会使用 volume 的数据
❯ docker run -d --name mongodb --rm --network goals-net -v data:/data/db mongo
a376dd9549cc5cb1a45d74abcd8ae50fd838d8b730f6f278eecf9287fbc6da3c
❯ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a376dd9549cc mongo "docker-entrypoint.s…" 3 seconds ago Up 2 seconds 27017/tcp mongodb
4ac01a93d3d3 goal-react "docker-entrypoint.s…" 2 hours ago Up 2 hours 0.0.0.0:3000->3000/tcp goals-frontend
d4e6b80883e5 goal-node "docker-entrypoint.s…" 2 hours ago Up 2 hours 0.0.0.0:80->80/tcp goals-backend
这里实现的功能室:
-
数据不与 mongodb 容器的生命周期进行绑定
只要容器使用
data
这个 volume,即可持久化数据
添加安全
这里的实现是根据 docerhub official mongo image 下的 security 部分添加:
❯ docker run -d --name mongodb --rm --network goals-net -v data:/data/db -e MONGO_INITDB_ROOT_USERNAME=root -e MONGO_INITDB_ROOT_PASSWORD=root mongo
6a3addfd1f2a0b8be89565eecc2e26a97c6e0a3917040505a4b64e10b7dfe325
⚠️:如果之前已经绑定过 named volume,那么需要重新删除该 volume,否则用户名密码可能无法重写 volume 中已经存在的默认数据
dockerize 后端
Dockerfile 如下:
FROM node
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
EXPOSE 80
ENV MONGODB_USERNAME=root
ENV MONGODB_PASSWORD=root
CMD [ "npm", "start" ]
这里实现的就是比较基础的功能,就是 cv package.json,跑 npm install
,cv 剩下的文件,暴露 80 端口,以及运行 npm start
。这里之所以运行 npm start
而不是 node app.js
的原因是因为配置了 nodemon,这样可以让本机修改代码的时候,不用重新 rebuild/run docker 容器
这里来会 build 来回切换需要注意的一点就是,一定要确保自己在对应的目录下。打包后端就要在 backend
目录下,打包前端则要在 frontend
目录下
运行大致如下:
# make sure you are in backend project
❯ docker build -t goal-node .
# ignore building process
❯ docker run --name goals-backend --rm -p 80:80 --network goals-net -d -v logs:/app/logs -v "$(pwd):/app" -v /app/node_modules goal-node
73662e085fc8cefd9e0a9b7c6d06d418559764b8419784809542fa4a4373282a
❯ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
73662e085fc8 goal-node "docker-entrypoint.s…" 10 seconds ago Up 8 seconds 0.0.0.0:80->80/tcp goals-backend
fc6ac2296a12 mongo "docker-entrypoint.s…" 2 minutes ago Up 2 minutes 27017/tcp mongodb
⚠️:这里暴露了 80 端口,因为前端项目最终还是通过浏览器实现,所以 docker 没有办法正确监听和 map 容器,因此最终前端还是需要通过浏览器访问 localhost
nodemon 是否成功运行可以通过修改一下代码,然后查看一下 log:
❯ docker logs goals-backend
> backend@1.0.0 start
> nodemon app.js
[nodemon] 3.1.0
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node app.js`
CONNECTED TO MONGODB
❯ docker logs goals-backend
> backend@1.0.0 start
> nodemon app.js
[nodemon] 3.1.0
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node app.js`
CONNECTED TO MONGODB
[nodemon] restarting due to changes...
[nodemon] starting `node app.js`
CONNECTED TO MONGODB!
修改代码肯定导致 log 变长,那就说明 nodemon 配置好了,同时也说明 bind mounts 成功了
env 使用补充
ENV 是可以在代码里被检测到的,所以这里也可以使用 process.env
去获取用户名和密码
mongoose.connect(
`mongodb://${process.env.MONGODB_USERNAME}:${process.env.MONGODB_PASSWORD}@mongodb:27017/course-goals?authSource=admin`,
{
useNewUrlParser: true,
useUnifiedTopology: true,
},
(err) => {
if (err) {
console.error("FAILED TO CONNECT TO MONGODB");
console.error(err);
} else {
console.log("CONNECTED TO MONGODB!");
app.listen(80);
}
}
);
同样,也可以使用参数去重写环境变量,如在运行 docker run
时添加 -e MONGODB_USERNAME=root
dockerize 前端
Dockerfile 基本一致,除了不需要访问 mongodb,所以没有设置 ENV
,以及暴露的端口不一样:
FROM node
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
EXPOSE 3000
CMD [ "npm", "start" ]
关于前端的 HMR……这个基本取决于用什么脚手架和什么框架,如果是通过脚手架构建的项目,基本都是自带 HMR,不需要自行配置。同样,需要注意终端运行的地址,是前端所在的文件夹
# make sure you are in frontend project
❯ docker build -t goal-react .
# ignore the building process
❯ docker run --name goals-frontend --rm -d -p 3000:3000 -it -v "$(pwd)/src:/app/src" goal-react
2e6f1d536066c45cf3b05cb456948c01009a23ddafdeb521cd8e4bcced2bb8a2
❯ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4ac01a93d3d3 goal-react "docker-entrypoint.s…" 2 seconds ago Up 2 seconds 0.0.0.0:3000->3000/tcp goals-frontend
d4e6b80883e5 goal-node "docker-entrypoint.s…" 45 seconds ago Up 45 seconds 0.0.0.0:80->80/tcp goals-backend
fc6ac2296a12 mongo "docker-entrypoint.s…" 16 minutes ago Up 16 minutes 27017/tcp mongodb
补充
补充一点说明信息
端口使用
如果查看一下端口,结果大概是这样的:
❯ lsof -i :80
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
Google 2410 user 37u IPv6 0x51114bdb801afbef 0t0 TCP localhost:53403->localhost:http (CLOSE_WAIT)
com.docke 8888 user 277u IPv6 0x51114bdb7e8fc3ef 0t0 TCP *:http (LISTEN)
❯ lsof -i :3000
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
Google 2410 user 33u IPv6 0x51114bdb7e9033ef 0t0 TCP localhost:53404->localhost:hbci (ESTABLISHED)
Google 2410 user 35u IPv6 0x51114bdb7e9043ef 0t0 TCP localhost:53401->localhost:hbci (CLOSE_WAIT)
Google 2410 user 38u IPv6 0x51114bdb7e18fbef 0t0 TCP localhost:53405->localhost:hbci (CLOSE_WAIT)
com.docke 8888 user 260u IPv6 0x51114bdb7e195bef 0t0 TCP *:hbci (LISTEN)
com.docke 8888 user 287u IPv6 0x51114bdb7e1903ef 0t0 TCP localhost:hbci->localhost:53404 (ESTABLISHED)
❯ lsof -i :27017
端口暴露
这里涉及到 docker 是怎么管理网络的,以上面的端口为例,可以看到 27017
是没有被暴露的,这个原因也很简单,容器虽然暴露了 27017
,但是这只会被 docker 在对应的 network 进行管理,host machine 上自然没有办法检测到暴露的端口
也正是因为 docker 内部会对 network 进行管理,所以在后端项目可以直接使用容器名称去访问 mongodb,即这一段代码:
mongoose.connect(
`mongodb://${process.env.MONGODB_USERNAME}:${process.env.MONGODB_PASSWORD}@mongodb:27017/course-goals?authSource=admin`
);
但是前端的代码是通过浏览器进行访问的,而浏览器所在的位置是 host machine,就像 host machine 不在 docker 管理的 network 上,无法访问到端口为 27017
的 mongodb 一样,浏览器也无法访问到在同一个 network 上的后端
假设如果现在这是一个微前端项目,每个前端模块都通过 docker 容器管理,那么模块与模块之间的引用——在不涉及到浏览器访问的情况下——是可以直接使用容器名称进行访问的