• 作者:老汪软件技巧
  • 发表时间:2024-12-07 10:04
  • 浏览量:

本项目代码已开源,具体见fullstack-blog。

数据库初始化脚本:关注公众号程序员白彬,回复关键词“博客数据库脚本”,即可获取。

为什么要使用 docker compose

也不废话了,上来直接说重点,为什么要使用 docker compose?

在初学 Docker 的时候,我们不会直接去学习 docker compose 的概念,因为直接看 docker compose 很容易陷入一个蒙圈的状态。我们会按照新手教程学习 docker 的主要概念和命令,比如:

但是随着项目的复杂度提升且涉及多个服务时,我们也发现了一些问题,这些命令太零碎了,如果要组织起一个复杂的项目,毫无疑问会涉及到很多条命令的执行,这个时候手输或复制粘贴命令就很容易出错了,比如有时候命令彼此间的顺序会搞错。此时要么编写流程脚本,要么就要用 docker compose 来组织了。

那么 docker compose 到底是什么呢?它其实起到一个声明式的作用。作为前端,我们知道,DOM 操作是很繁琐的,这在复杂的UI交互场景中尤为明显,所以后来就有了 MVVM 前端框架的出现,我们基本上不需要去管理操作 DOM 的过程,框架底层会去维护状态和 DOM 的映射关系,而我们只需要声明数据和组件的绑定关系,就能得到预期的UI,这就是声明式的魅力!

docker compose 就是这样一个声明式的产物,将复杂的过程收敛到一个声明配置文件中,我们可以去声明服务、网络、数据卷等,还可以描述依赖关系。最后,只需要通过一两个命令就能把一个复杂的项目运行起来!

项目实操

回到我们这个全栈博客项目,在未使用 docker compose 之前,我们的操作过程是这样的。

来到后端工程目录下,通过 docker build 构建后端镜像。通过 docker run 把后端容器跑起来。来到前端工程目录下,通过 docker build 构建前端镜像。通过 docker run 把前端容器跑起来。

如果将数据库或者Redis等服务也考虑进来,这个工作流程就显得有点繁琐了。我们试着用 docker compose 来重新组织一下!

pnpm monorepo 下的 Dockerfile 改造

使用了 pnpm monorepo 架构后,前后端工程在同一个仓库中,因此在写 Dockerfile 时也有一些变化。pnpm 官网给出了与 docker 集成的示例,我们按照这个例子来改造即可,以下是本项目的完整 Dockerfile 配置,我们来逐步理解下这个过程。

FROM node:18-slim AS base
RUN npm i -g pnpm
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm deploy --filter=vite-vue3 /app/vite-vue3
RUN pnpm deploy --filter=express-server /app/express-server
FROM base AS vite-vue3-build
COPY --from=build /app/vite-vue3 /usr/src/fullstack-blog/app/vite-vue3
COPY tsconfig.base.json /usr/src/fullstack-blog/tsconfig.base.json
WORKDIR /usr/src/fullstack-blog/app/vite-vue3
RUN pnpm build
FROM nginx:latest AS vite-vue3-frontend
COPY --from=vite-vue3-build /usr/src/fullstack-blog/app/vite-vue3/dist/ /usr/share/nginx/html
COPY nginx/default.conf.template /etc/nginx/conf.d/default.conf.template
EXPOSE 80
FROM base AS express-backend
RUN npm i -g pm2-runtime
COPY --from=build /app/express-server /usr/src/fullstack-blog/app/express-server
WORKDIR /usr/src/fullstack-blog/app/express-server
EXPOSE 8002
CMD ["pnpm", "start-docker-prod"]

首先是全局安装了 pnpm。

FROM ... AS base 是用于多阶段构建的,多阶段构建的好处大家可以另行了解。

FROM node:18-slim AS base
RUN npm i -g pnpm

在 pnpm monorepo 中,前后端源码都在同一个仓库下,而实际上打包 Docker 镜像时,我们是要把前后端做成独立的镜像。pnpm 也考虑到了这一点,于是提供了 pnpm deploy 命令,可以将每一个 package 及相关的依赖单独部署到一个目录,变成一个独立的工程,这样就可以针对不同的 package 单独打镜像。这里我们针对 vite-vue3 的前端工程以及 express-server 的后端工程进行了 pnpm deploy 操作,分别移植到了 /app/vite-vue3 和 /app/express-server 目录下。

FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm deploy --filter=vite-vue3 /app/vite-vue3
RUN pnpm deploy --filter=express-server /app/express-server

接着来到前端专用的 /app/vite-vue3 目录下进行操作,首先是执行构建命令得到 dist 目录,也就是我们熟悉的 vite build(对应 package.json 中定义的 build 命令,所以是执行 pnpm build)。

FROM base AS vite-vue3-build
COPY --from=build /app/vite-vue3 /usr/src/fullstack-blog/app/vite-vue3
COPY tsconfig.base.json /usr/src/fullstack-blog/tsconfig.base.json
WORKDIR /usr/src/fullstack-blog/app/vite-vue3
RUN pnpm build

然后就是基于 nginx 镜像,把 dist 目录的静态资源放进 nginx 里,暴露 80 端口。

FROM nginx:latest AS vite-vue3-frontend
COPY --from=vite-vue3-build /usr/src/fullstack-blog/app/vite-vue3/dist/ /usr/share/nginx/html
COPY nginx/default.conf.template /etc/nginx/conf.d/default.conf.template
EXPOSE 80

后端 express-server 由于不涉及构建,直接一步到位启动项目。考虑到是生产环境,我们使用 pm2-runtime 来启动项目。

FROM base AS express-backend
RUN npm i -g pm2-runtime
COPY --from=build /app/express-server /usr/src/fullstack-blog/app/express-server
WORKDIR /usr/src/fullstack-blog/app/express-server
EXPOSE 8002
CMD ["pnpm", "start-docker-prod"]

这样一来,一个 Dockerfile 就改造出来了。至于数据库等服务,我们到 compose.yml 再去声明。

打镜像

你可以使用docker build命令,比如:

docker build --target vite-vue3-frontend -t fullstack-blog-vite-vue3 .
docker build --target express-backend -t fullstack-blog-express .

_全栈式运营_全栈式运维

如果已经写好 compose.yml,也可以直接运行docker compose build。

上传镜像

我使用的是阿里云镜像服务,所以我们首先登录阿里云镜像服务。

接着打镜像 tag,也就是镜像的版本号。

最后使用 docker push 推送对应的镜像版本。

以下是以我的私有镜像仓库为例说明,实际操作时,应该换成你自己的。

# 先登录
docker login --username=xxx registry.cn-hangzhou.aliyuncs.com

# 打 tag
docker tag fullstack-blog-vite-vue3 registry.cn-hangzhou.aliyuncs.com/tusi_personal/fullstack-blog-vite-vue3:3.0.0
docker tag fullstack-blog-express registry.cn-hangzhou.aliyuncs.com/tusi_personal/fullstack-blog-express:3.0.0

# 推送镜像
docker push registry.cn-hangzhou.aliyuncs.com/tusi_personal/fullstack-blog-vite-vue3:3.0.0
docker push registry.cn-hangzhou.aliyuncs.com/tusi_personal/fullstack-blog-express:3.0.0

使用 compose.yml 组装项目

在 services 下声明各个服务,比如前端,后端,数据库,Redis 等。

我们首先声明前端部分,image 来源于阿里云镜像服务,ports 声明了将宿主机的 3000 端口映射到容器的 80 端口。

services:
  vite-vue3:
    restart: always
    build:
      target: vite-vue3-frontend
    image: ${DOCKER_REGISTRY}/${DOCKER_NAMESPACE}/fullstack-blog-vite-vue3:${VITE_VUE3_VERSION:-latest}
    ports:
      - "3000:80"
    environment:
      - BACKEND_PORT=8002
    command: /bin/bash -c "envsubst '$$BACKEND_PORT' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"

由于 express 后端服务依赖了 mysql,我们先把 mysql 服务声明一下。

我这里用的是 mysql 8 的版本,由于最近国内 Docker 镜像几乎都无法使用,我这里也是通过本地网络代理将 mysql 拉到本地,然后推送到阿里云私有镜像仓库的。

mysql 默认是 3306 端口,但我们可以通过 ports 去映射其他端口,比如这里我选择了 3308 端口。

通过 environment 设置环境变量,主要是密码,数据库名称等等,还要注意时区的设置。

volumes 是用于挂载数据卷的,关键的是 mysql_data,它声明了一个由 docker 自行维护的数据卷,具体对应宿主机的哪个文件夹,其实你不用太过于关心。通过mysql_data:/var/lib/mysql就实现了 db 数据的挂载,当然一开始数据库肯定是空的,如果我们需要初始化数据,就需要用到/docker-entrypoint-initdb.d。

  mysql:
    restart: always
    image: ${DOCKER_REGISTRY}/${DOCKER_NAMESPACE}/mysql:8.4.2
    ports:
      - "3308:3306"
    environment:
      - TZ=Asia/Shanghai
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - MYSQL_DATABASE=${MYSQL_DATABASE_NAME}
      - MYSQL_CHARSET=utf8mb4
      - MYSQL_COLLATION=utf8mb4_0900_ai_ci
    volumes:
      - mysql_data:/var/lib/mysql
      - ./mysql/my.cnf:/etc/mysql/conf.d/my.cnf
      - ./mysql/init-scripts:/docker-entrypoint-initdb.d
      
// ... 其他配置
      
volumes:
  mysql_data:

有了 mysql,就可以接着声明后端服务 express-server 了。基本上就是依葫芦画瓢,抄抄改改就配置出来了,无他,唯手熟尔!

这里注意用了一个 depends_on,并且指定了 mysql 这个服务,代表 express-server 这个后端服务是依赖 mysql 服务的。

  express-server:
    restart: always
    build:
      target: express-backend
    image: ${DOCKER_REGISTRY}/${DOCKER_NAMESPACE}/fullstack-blog-express:${EXPRESS_SERVER_VERSION:-latest}
    ports:
      - "8002:8002"
    volumes:
      - ./express-server/config/env.js:/usr/src/fullstack-blog/app/express-server/src/config/env.js
    environment:
      - TZ=Asia/Shanghai
      - NODE_ENV=production
      - PORT=8002
      - NPM_CONFIG_REGISTRY=https://registry.npmmirror.com
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - MYSQL_DATABASE=${MYSQL_DATABASE_NAME}
    depends_on:
      - mysql

完成了 Dockerfile 和 compose.yml 的编写,docker 的关键操作就算做完了,剩下的就是对 docker compose 各个命令的运用了,比如 docker compose build, docker compose up 等。

更多细节可以打开源码瞧瞧,里面也写了比较详细的 docker 操作流程说明。


上一条查看详情 +【前缀和】算法思想,附两道道手撕题
下一条 查看详情 +没有了