前端工程师也应该了解的 docker compose

海面上,一艘满载集装箱的货轮正在驶离港口......

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 版本的代码即可:

github.com/Knight174/d...

后端接口 /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 几乎是无缝衔接的。

访问 http://localhost:3000

没问题!

另外,通过指令 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,并设置星标,以便第一时间收到更新。

相关文章:

相关推荐
jonssonyan43 分钟前
稳了,搭建Docker国内源图文教程
运维·docker·容器
[听得时光枕水眠]3 小时前
【Docker】Docker上安装MySql8和Redis
运维·docker·容器
神秘的土鸡4 小时前
Linux中Docker容器构建MariaDB数据库教程
linux·运维·服务器·数据库·docker·mariadb
攸攸太上6 小时前
Docker学习
java·网络·学习·docker·容器
Sylvan Ding6 小时前
Docker+PyCharm远程调试&环境隔离解决方案
docker·容器·pycharm
_道隐_9 小时前
如何在Windows上安装Docker
windows·docker
孙强_05259 小时前
使用docker创建zabbix服务器
服务器·docker·zabbix
shelby_loo13 小时前
通过 Docker 部署 MySQL 服务器
服务器·mysql·docker
prcyang16 小时前
Docker Compose
运维·docker·容器
蜗牛^^O^16 小时前
Docker和K8S
java·docker·kubernetes