[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 projectdocker build -t goal-node .
# ignore building processdocker 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 projectdocker build -t goal-react .
# ignore the building processdocker 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 容器管理,那么模块与模块之间的引用——在不涉及到浏览器访问的情况下——是可以直接使用容器名称进行访问的

04-29 02:17