type
status
date
slug
summary
tags
category
icon
password
docker
docker
海面上,一艘满载集装箱的货轮正在驶离港口……

1. 前言

一个月前,我写了一篇《前端工程师也应该了解的docker》,之后就不断有小伙伴千呼万唤!
催更
催更
催更
催更
这次我们的目标是部署一个全栈项目,而这一篇涉及的内容有:
  • docker-compose.yml 容器编排配置文件
  • volume 数据卷持久化
  • network 网络(bridge 模式)
  • nginx 反向代理
这一部分的内容还是很难的,但是没关系,看到 3.1 节,你不可能不会。
对了,这次我在 github 上准备了源码,按照顺序分成了多个 tag 版本的代码,方便大家参考:https://github.com/Knight174/docker-compose-demo
注意:本文我并不打算解释服务端代码,如果你想要实践成功的话,强烈建议下载源码。

2. docker compose

2.1 什么是 docker compose?

上篇说到利用 docker 可以实现单个容器的部署,当遇到相互有联系的容器呢?比如一个 Web 应用,不仅仅有客户端,还有服务器端和数据库需要部署。在这种生产环境级别的情况下,如果一个一个服务启动起来,未免太麻烦了,那么应该如何应对?部署多容器的应用,就要用到 docker 官方提供的容器编排工具 —— docker compose。

2.2 需求设计

设计一个很简单的需求:
  • 统计首页访问量
  • 访问首页可以看到所有人员信息

2.3 编排设计

现在,整个应用的编排设计大体如下:
  • 创建四个容器,分别启动 postgres、redis、express、nginx 服务。
  • postgres 暴露 5432 端口、redis 暴露 6379 端口,让 express 可以访问数据;express 暴露 3000 端口让 nginx 进行反向代理;最后,用户通过访问 nginx 的 80 端口进入网站。
  • 数据库的数据通过 volume 实现持久化。
  • 定义 network 实现服务间的通信,避免占用过多端口。
compose design
compose design

3. docker 单容器部署

docker compose 的下载安装:略。
在使用 docker compose 进行部署之前,我们先用 docker 单容器的方式去部署,然后再过渡到 docker compose。如果只对 docker compose 感兴趣,请直接去第 4 节。
在此之前,请在本地准备好以下镜像:
  • node:18-alpine
  • postgres:14-alpine
  • redis:6.0.20-alpine
  • nginx:stable-alpine
相关指令:docker pull <image_name>:<image_tag>

3.1 构建网络

如果想让容器之间能够通信,那么就需要构建一个虚拟网络:
如果不构建,后面运行容器时就会报错:docker: Error response from daemon: network network1 not found.
这种网络模式实际上就是 bridge 模式(桥接模式)。当容器启动时,docker 会为容器分配一个 IP 地址,并将容器连接到默认的桥接网络。通过桥接网络,容器可以相互通信,也可以与宿主机上的其他服务进行通信。
大概可以理解为——牛郎织女鹊桥相会,在这段感人的神话传说里,好在有鹊桥,牛郎织女一家人才得以见面团聚。Nginx 容器代表织女、Express App 代表牛郎、PostgreSQL 和 Redis 分别代表一儿一女,而 network1 代表鹊桥。
颐和园长廊彩绘-鹊桥相会
颐和园长廊彩绘-鹊桥相会
记住这个故事,继续往下看。

3.2 PostgreSQL 服务器

3.2.1 启动容器

在数据库服务中将用到 volume 数据卷(可以理解为本地的一个文件夹,服务启动后会用这个本地文件夹里的数据,这样即使容器被干掉了,数据还在,这就做到了持久化。),同样也要用到 network
使用镜像 postgres:14-alpine 启动一个 Postgres 数据库服务:
  • -d:以“分离模式”启动容器,允许容器在后台运行。
  • --name:指定容器名称,这里名称为 my-postgres
  • -v "$PWD/db-data":/var/lib/postgresql/data:将主机的文件或目录与容器内的文件或目录进行挂载。在这里,$PWD/db-data 是主机上的当前工作目录下的 "db-data" 目录,/var/lib/postgresql/data 是容器内的 PostgreSQL 数据目录。这样做的目的是使数据库的数据持久化,方便在容器重新启动后保留数据。
  • --network network1:指定容器连接的网络。在这里,容器将连接到名为 "network1" 的网络中,这样可以与其他容器或主机进行通信。
  • -p 5432:5432:将主机的端口与容器的端口进行映射。在这里,主机的 5432 端口将映射到容器的 5432 端口。这样做是为了允许在主机上通过 5432 端口访问运行在容器内的 PostgreSQL 数据库。
  • -e POSTGRES_USER=myuser -e POSTGRES_PASSWORD=mypassword:指定在容器内创建的 PostgreSQL 数据库的用户名和密码。在这里,用户名为 myuser,密码为 mypassword。这些参数将被用于在容器启动时设置数据库的访问凭据。

3.2.2 创建并连接数据库

进入容器,创建并连接数据库 dev。(生产环境用 prod)
如果数据库不是首次创建,直接使用 psql -U myuser -d dev 连接即可。

3.2.3 创建表

一开始 users 表里是没有数据的,需要数据播种。

3.2.4 数据播种

创建好数据库后,就可以运行 express 服务了,首次进入会进行数据播种。我使用了 @faker-js/faker 这个包来实现,具体可以查看 db/seed.ts 里的代码内容。

3.3 Redis 服务器

使用镜像 redis:6.0.20-alpine 启动一个 Redis 服务:
  • -name my-redis:指定容器名称,这里名称为 my-redis
  • v "$PWD/redis-data":/data:创建数据卷。"$PWD/redis-data" 表示当前目录下的 "redis-data" 目录,将其映射到容器内的 "/data" 目录。这样可以持久化存储 Redis 数据,即使容器被删除或重新创建,数据仍然可用。
  • -network network1:指定容器连接的网络。在这里,容器将连接到名为 "network1" 的网络中,这样可以与其他容器或主机进行通信。
  • redis-server --appendonly yes --requirepass 12345678:在容器内运行的命令。redis-server 是 Redis 服务器启动命令,-appendonly yes 启用了 AOF 持久化,-requirepass 12345678 设置了 Redis 的访问密码为 "12345678"。
到这里,我们可以通过 docker ps 指令看到当前正在运行的容器:
正在运行的容器
正在运行的容器
在这里,眼睛雪亮的朋友可以发现容器列表中的 PORTS 还是分别将端口映射出来了,这是为了方便在本地开发,直接连接端口。如果是生产环境,创建容器时则不建议加上 -p 参数,容器间直接通过 network1 以及对应的服务名称进行连接即可,具体可以看 3.4.2 节的 .env 文件。

3.4 Express App 服务

数据库准备好后,开始创建一个 express + ts 项目,前端部分使用 react + ts,并完成相关需求:略。
直接下载运行这个 tag 版本的代码即可:
v1.0.0
v1.0.0
后端接口 /api/users 我写了增删改查,前端比较繁琐,就写了 users 的列表获取,大家可以 fork 后帮我贡献代码。
notion image

3.4.1 package.json

package.json

3.4.2 .env

.env 文件存放了需要使用的环境参数。在开发环境下,记得把这里的 NODE_ENV 对应的值改为 dev,至于环境切换配置可以查看源码中的 /server/config/config.ts
如果不暴露端口,所有服务处于 network1 网络下,使用启动容器时指定的 name 名称作为 host 地址,因为 docker 会通过这个 name 将容器内部的 host 和容器内部 ip 做关联。例如,PSQL_HOST 和 REDIS_HOST 的值分别为 "my-postgres" 和 "my-redis"。

3.4.3 构建 express 服务

在 server 目录下新建生产环境的 Dockerfile:
以当前目录下的 Dockerfile 为基础打镜像:
express-app 镜像来启动一个容器服务:
同样需要连接网络 network1。
此时,使用 docker ps 查看容器运行状态:
docker ps
docker ps
注意:这里的两个数据库服务我用了 prod 的方式进行部署,所以没有 port 映射。

3.5 小结

到这里我们就实现了单容器的部署,按照上面的步骤执行下来,当你访问 http://localhost:3000 时,你应该能够看到这样的页面了:
单容器部署
单容器部署
恭喜你,成功了!
对了,当前端访问 http://localhost:3000/api/users 则可以拿到 users 列表的数据:
access /api/users
access /api/users
最后,再来整理一下思路:
  • 启动 PostgreSQL 数据库服务 + 初始化数据库 + 连接数据库
  • 启动 Redis 数据库服务
  • 启动 App
这里留了一个坑,就是用户并不能通过访问 80 端口就访问到网站,这需要 Nginx 的反向代理来实现,我放在 docker compose 里说。

4. docker compose 多容器部署

如果说 docker 启动的容器是一个个乐高的组件 🧩,那么 docker compose 就是由这些组件共同构成的一个完整的乐高玩具。
lego
lego
docker-compose.yml 是进行 docker 编排任务的配置文件,就类似于 Dockerfile 是 Docker 镜像的配置文件一样,有了这份文件就能根据配置组建对应的服务了。

4.1 常用指令

docker compose 的指令和 docker 的指令差不多。
  • 启动容器
根据 docker-compose.yml 文件中的配置启动所有定义的容器。
  • 关闭容器
停止并删除所有相关的容器、网络和卷。
  • 构建镜像
根据 docker-compose.yml 文件中的配置构建所有定义的镜像。
  • 查看容器状态
显示正在运行的容器的状态。
  • 查看容器日志
显示容器的日志输出。
  • 进入容器
在指定的服务中执行给定的命令。

4.2 构建服务

在 server 目录下新建配置文件:docker-compose.yml
关键注释:
  • container_name:指定容器的名称。
  • build:指定构建镜像所需的上下文路径。可以是一个包含 Dockerfile 的目录路径,或者一个包含具有构建指令的 URL。
  • ports:定义将容器内部端口映射到主机上的端口。格式为:<host>:<container>,前者表示主机端口,后者表示容器端口。
  • image:指定使用的镜像的名称。
  • depends_on:指定服务之间的依赖关系,这里的服务需要先启动,这样就可以保证服务启动顺序。
  • env_file:指定环境变量文件的路径。该文件中定义的环境变量将传递给容器,使您可以在容器内部使用这些变量。
  • networks:指定容器连接的网络。
  • restart:定义服务的重启策略。可以设置为always(始终重启)、no(不重启)、on-failure(仅在非零退出代码时重启)和unless-stopped(除非手动停止,否则重启)。
  • environment:设置容器的环境变量,通过 ${变量名} 的形式可以访问到 env 文件中的环境变量。可以在此处指定键值对,将环境变量传递给容器。格式为KEY=VALUE。如果不想写这个,也可以通过配置 env_file 直接使用对应变量,不过要注意 key 是否一致。
  • volumes:定义容器与主机之间的文件或目录挂载。可以将主机的路径映射到容器内的路径,实现数据持久化或文件共享。
根据 docker-compose.yml 来构建多容器服务(如果这里运行不起来,请重启 docker):
docker-compose 构建服务
docker-compose 构建服务

4.3 小结

在第 3 节,我们用 docker 分别构建了三个容器服务,这三个服务就对应上面 yml 文件中的 servers 中的三个服务。比如第一个服务 express-app 就对应着上面 3.4.3 节的一系列操作:
express-app
express-app
my-postgres 和 my-redis 这两个服务同理,由此大家可以发现 docker 到 docker compose 几乎是无缝衔接的。
访问 http://localhost:3000:
多容器部署
多容器部署
没问题!
另外,通过指令 docker compose ps 可以查看当前编排任务的容器信息:
docker compose ps
docker compose ps
如果想移除任务,执行 docker-compose down 即可。
notion image
至于其他的 docker compose 指令,和 docker 指令简直如出一辙,大家自行探索吧~

5. Nginx 反向代理

还差最后一步,在访问网站时,我们似乎没有碰到还要用户在域名后面还要加上 3000 端口的说法,一般都是访问默认的 80 端口或者 443 端口。那么如何实现呢?加一个网关。

5.1 添加 nginx 服务

在原有的 docker-compose.yml 中添加 nginx 服务:
  • 在 nginx 服务中使用了两个数据卷来持久化,一个是 nginx 配置文件,一个是本地的静态文件。
  • 暴露 80 端口,供用户访问
  • 暴露 3000 端口,供数据调试

5.2 nginx.conf

在 server 目录下新建 nginx.conf:
  • 通过设置 proxy_pass 即可设置反向代理。当用户访问 http://localhost:80 时,就会被代理到 express-app 服务上,实际上就变成了访问 http://localhost:3000,但是用户是无感知的。
  • 通过设置静态文件缓存以及 gzip 压缩等,可以给网站提升性能,具体如下:
performance optimization
performance optimization

5.3 重新构建服务

移除之前的服务后,重新执行构建命令:
docker compose 构建服务 - 添加了 nginx
docker compose 构建服务 - 添加了 nginx
除了通过 docker compose ps 来查看容器运行情况,也可以直接打开 docker 去 containers 中查看:
docker containers
docker containers
访问 http://localhost:
加上 nginx,访问 80 端口
加上 nginx,访问 80 端口
填坑完毕!

6. 总结

到这里,鹊桥相会的故事就讲完了。现在再去回顾 2.3 节的编排设计,相信你会有更深刻的理解。
与 docker 比起来,docker compose 可以让我们更加快速地上线一个全栈项目。
  • 数据持久化
  • bridge 网络模式的 network
  • 使用 docker 单容器部署
  • 使用 docker compose 编排多容器服务
network 还有其他的模式,比如 host 模式、覆盖网络模式;数据库操作推荐使用 Prisma,可以更加方便地进行 CRUD。这些都可以参考我的另一个仓库:https://github.com/Knight174/docker-compose-kit
虽然这一篇我拿 express.js 举例,但是其实诸如 next.js、nest.js 等 node 框架也大同小异,数据库也是同理,但这并不是说没有工作量。
至于云端部署,大家可以自己研究一下,把本地主机看成服务器就可以了,或者来群里催更。
以上,如有谬误,还请斧正。
记得三连哦,你的鼓励是我创作的最大动力,感谢您的阅读~
 
 
你不知道的 JS 单线程什么?!前端工程师还不会 Docker?
Eric 见嘉
Eric 见嘉
Less is more.
公告
type
status
date
slug
summary
tags
category
icon
password
💭
合抱之木,生于毫末;九层之台,起于累土;千里之行,始于足下。

关于我
土木转行的前端开发工程师,陆续分享技术干货。
联系我
微信公众号:见嘉 Being Dev