海面上,一艘满载集装箱的货轮正在驶离港口......
1. 前言
一个月前,我写了一篇《前端工程师也应该了解的docker》,之后就不断有小伙伴千呼万唤!
这次我们的目标是部署一个全栈项目,而这一篇涉及的内容有:
docker-compose.yml
容器编排配置文件volume
数据卷持久化network
网络(bridge 模式)nginx
反向代理
这一部分的内容还是很难的,但是没关系,看到 3.1 节,你不可能不会。
对了,这次我在 github 上准备了源码,按照顺序分成了多个 tag 版本的代码,方便大家参考:github.com/Knight174/d...
注意:本文我并不打算解释服务端代码,如果你想要实践成功的话,强烈建议下载源码。
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 实现服务间的通信,避免占用过多端口。
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 构建网络
如果想让容器之间能够通信,那么就需要构建一个虚拟网络:
bash
# docker network create <network_name>
docker network create network1
如果不构建,后面运行容器时就会报错: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 数据库服务:
bash
docker run -d --name my-postgres -v "$PWD/db-data":/var/lib/postgresql/data --network=network1 -p 5432:5432 -e POSTGRES_USER=myuser -e POSTGRES_PASSWORD=mypassword postgres:14-alpine
-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)
bash
docker exec -it my-postgres bash # 进入容器 my-postgres
psql -U myuser # 登录数据库
CREATE DATABASE dev; # 创建数据库 dev
# CREATE DATABASE prod;
\c dev; # 连接数据库 dev
# \c prod;
如果数据库不是首次创建,直接使用 psql -U myuser -d dev
连接即可。
3.2.3 创建表
sql
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL,
deleted BOOLEAN DEFAULT false
);
一开始 users 表里是没有数据的,需要数据播种。
3.2.4 数据播种
创建好数据库后,就可以运行 express 服务了,首次进入会进行数据播种。我使用了 @faker-js/faker
这个包来实现,具体可以查看 db/seed.ts
里的代码内容。
3.3 Redis 服务器
使用镜像 redis:6.0.20-alpine
启动一个 Redis 服务:
bash
docker run -d --name my-redis -v "$PWD/redis-data":/data --network network1 -p 6379:6379 redis:6.0.20-alpine redis-server --appendonly yes --requirepass 12345678
-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 版本的代码即可:
后端接口 /api/users
我写了增删改查,前端比较繁琐,就写了 users 的列表获取,大家可以 fork 后帮我贡献代码。
3.4.1 package.json
package.json
json
{
"scripts": {
"build": "npx tsc",
"start": "node dist/index.js",
"dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/index.js\"",
"start:prod": "npm run build && npm run start"
}
}
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"。
.env
NODE_ENV=production # 环境,开发环境时改成 dev
PORT=3000 # express 暴露端口
PSQL_DB=prod # postgres 数据库
PSQL_USER=myuser # 数据库服务管理员
PSQL_PASS=mypassword # 数据库服务密码
PSQL_HOST=my-postgres # 数据库服务主机
PSQL_PORT=5432 # 数据库服务端口
REDIS_HOST=my-redis # redis 数据库服务主机
REDIS_PASS=12345678 # 密码
REDIS_PORT=6379 # 端口
3.4.3 构建 express 服务
在 server 目录下新建生产环境的 Dockerfile:
Dockerfile
# 使用 Node.js 18 作为基础镜像
FROM node:18-alpine
# 设置工作目录
WORKDIR /app
# 复制 package.json 和 package-lock.json 到容器中
COPY package*.json ./
# 安装应用程序依赖
RUN npm install
# 复制应用程序代码到容器中
COPY . .
# 暴露应用程序的端口
EXPOSE 3000
# 启动应用程序,对用 package.json 中的指令
CMD ["npm", "run", "start:prod"]
以当前目录下的 Dockerfile
为基础打镜像:
bash
docker build -t express-app .
以 express-app
镜像来启动一个容器服务:
bash
docker run -d --name my-express-app --network network1 -p 3000:3000 express-app
同样需要连接网络 network1。
此时,使用 docker ps
查看容器运行状态:
注意:这里的两个数据库服务我用了 prod 的方式进行部署,所以没有 port 映射。
3.5 小结
到这里我们就实现了单容器的部署,按照上面的步骤执行下来,当你访问 http://localhost:3000 时,你应该能够看到这样的页面了:
恭喜你,成功了!
对了,当前端访问 http://localhost:3000/api/users
则可以拿到 users 列表的数据:
最后,再来整理一下思路:
- 启动 PostgreSQL 数据库服务 + 初始化数据库 + 连接数据库
- 启动 Redis 数据库服务
- 启动 App
这里留了一个坑,就是用户并不能通过访问 80 端口就访问到网站,这需要 Nginx 的反向代理来实现,我放在 docker compose 里说。
4. docker compose 多容器部署
如果说 docker 启动的容器是一个个乐高的组件 🧩,那么 docker compose 就是由这些组件共同构成的一个完整的乐高玩具。
docker-compose.yml 是进行 docker 编排任务的配置文件,就类似于 Dockerfile 是 Docker 镜像的配置文件一样,有了这份文件就能根据配置组建对应的服务了。
4.1 常用指令
docker compose 的指令和 docker 的指令差不多。
- 启动容器
根据 docker-compose.yml 文件中的配置启动所有定义的容器。
bash
docker-compose up
# or
docker-compose up -d #(以后台模式运行)
- 关闭容器
停止并删除所有相关的容器、网络和卷。
bash
docker-compose down
- 构建镜像
根据 docker-compose.yml 文件中的配置构建所有定义的镜像。
bash
docker-compose build
- 查看容器状态
显示正在运行的容器的状态。
bash
docker-compose ps
- 查看容器日志
显示容器的日志输出。
bash
docker-compose logs
- 进入容器
在指定的服务中执行给定的命令。
bash
docker-compose exec <service> <command>
4.2 构建服务
在 server 目录下新建配置文件:docker-compose.yml
yaml
version: '3'
# 服务列表
services:
express-app:
container_name: express-app
build:
context: .
dockerfile: Dockerfile
ports:
- 3000:3000
depends_on:
- my-postgres
- my-redis
env_file:
# 从 .env 文件中读取环境变量
- .env
networks:
- network1
my-postgres:
image: postgres:14-alpine
restart: always # Docker在容器退出时自动重启该服务
container_name: my-postgres
environment:
# 从 .env 文件中读取环境变量,使用 ${变量名} 来使用它们
- POSTGRES_DB=${PSQL_DB}
- POSTGRES_USER=${PSQL_USER}
- POSTGRES_PASSWORD=${PSQL_PASS}
volumes:
- ./db-data:/var/lib/postgresql/data
networks:
- network1
my-redis:
image: redis:6.0.20-alpine
restart: always
container_name: my-redis
env_file: .env
volumes:
- ./redis-data:/data
command: redis-server --appendonly yes --requirepass ${REDIS_PASS}
networks:
- network1
# 网络
networks:
network1:
关键注释:
- 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):
bash
docker-compose up -d
4.3 小结
在第 3 节,我们用 docker 分别构建了三个容器服务,这三个服务就对应上面 yml 文件中的 servers 中的三个服务。比如第一个服务 express-app 就对应着上面 3.4.3 节的一系列操作:
my-postgres 和 my-redis 这两个服务同理,由此大家可以发现 docker 到 docker compose 几乎是无缝衔接的。
没问题!
另外,通过指令 docker compose ps
可以查看当前编排任务的容器信息:
如果想移除任务,执行 docker-compose down
即可。
至于其他的 docker compose 指令,和 docker 指令简直如出一辙,大家自行探索吧~
5. Nginx 反向代理
还差最后一步,在访问网站时,我们似乎没有碰到还要用户在域名后面还要加上 3000 端口的说法,一般都是访问默认的 80 端口或者 443 端口。那么如何实现呢?加一个网关。
5.1 添加 nginx 服务
在原有的 docker-compose.yml 中添加 nginx 服务:
yaml
version: '3'
services:
express-app:
container_name: express-app
build:
context: .
dockerfile: Dockerfile
ports:
- 3000:3000
depends_on:
- my-postgres
- my-redis
env_file:
# 从 .env 文件中读取环境变量
- .env
networks:
- network1
my-postgres:
image: postgres:14-alpine
restart: always
container_name: my-postgres
environment:
# 从 .env 文件中读取环境变量,使用 ${变量名} 来使用它们
- POSTGRES_DB=${PSQL_DB}
- POSTGRES_USER=${PSQL_USER}
- POSTGRES_PASSWORD=${PSQL_PASS}
volumes:
- ./db-data:/var/lib/postgresql/data
networks:
- network1
my-redis:
image: redis:6.0.20-alpine
restart: always
container_name: my-redis
env_file: .env
volumes:
- ./redis-data:/data
command: redis-server --appendonly yes --requirepass ${REDIS_PASS}
networks:
- network1
nginx:
image: nginx:stable-alpine
container_name: nginx
restart: always
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./public:/usr/share/nginx/html
depends_on:
- express-app
networks:
- network1
networks:
network1:
- 在 nginx 服务中使用了两个数据卷来持久化,一个是 nginx 配置文件,一个是本地的静态文件。
- 暴露 80 端口,供用户访问
- 暴露 3000 端口,供数据调试
5.2 nginx.conf
在 server 目录下新建 nginx.conf:
nginx
# 全局配置
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
# 事件模块配置
events {
use epoll; # 多路复用
worker_connections 1024;
}
# HTTP模块配置
http {
# MIME类型配置
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 日志格式配置
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# 访问日志配置
access_log /var/log/nginx/access.log main;
# Gzip压缩配置
gzip on;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# 服务器配置
server {
listen 80;
# localhost 是主机名
server_name localhost;
# 根目录配置
root /usr/share/nginx/html;
index index.html;
# 其他路由配置
location / {
# 这里 express-app 是 node 容器名
proxy_pass <http://express-app:3000>;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
try_files $uri $uri/ /index.html;
}
# 静态文件缓存配置
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 1d;
}
}
}
- 通过设置 proxy_pass 即可设置反向代理。当用户访问 http://localhost:80 时,就会被代理到 express-app 服务上,实际上就变成了访问 http://localhost:3000,但是用户是无感知的。
- 通过设置静态文件缓存以及 gzip 压缩等,可以给网站提升性能,具体如下:
5.3 重新构建服务
移除之前的服务后,重新执行构建命令:
bash
docker-compose up -d
除了通过 docker compose ps
来查看容器运行情况,也可以直接打开 docker 去 containers 中查看:
访问 http://localhost:
填坑完毕!
6. 总结
到这里,鹊桥相会的故事就讲完了。现在再去回顾 2.3 节的编排设计,相信你会有更深刻的理解。
与 docker 比起来,docker compose 可以让我们更加快速地上线一个全栈项目。
- 数据持久化
- bridge 网络模式的 network
- 使用 docker 单容器部署
- 使用 docker compose 编排多容器服务
network 还有其他的模式,比如 host 模式、覆盖网络模式;数据库操作推荐使用 Prisma,可以更加方便地进行 CRUD。这些都可以参考我的另一个仓库:github.com/Knight174/d...
虽然这一篇我拿 express.js 举例,但是其实诸如 next.js、nest.js 等 node 框架也大同小异,数据库也是同理,但这并不是说没有工作量。
至于云端部署,大家可以自己研究一下,把本地主机看成服务器就可以了,或者来群里催更。
以上,如有谬误,还请斧正。
记得三连哦,你的鼓励是我创作的最大动力,感谢您的阅读~
👏 对了,如果你还没有我的好友,加我微信:enjoy_Mr_cat ,备注 「掘金」
,即有机会加入高质量前端交流群,在这里你将会认识更多的朋友;也欢迎关注我的公众号 见嘉 Being Dev
,并设置星标,以便第一时间收到更新。
相关文章: