本项目代码已开源,具体见fullstack-blog。
数据库初始化脚本:关注公众号程序员白彬,回复关键词“博客数据库脚本”,即可获取。
为什么要使用 docker compose
也不废话了,上来直接说重点,为什么要使用 docker compose?
在初学 Docker 的时候,我们不会直接去学习 docker compose 的概念,因为直接看 docker compose 很容易陷入一个蒙圈的状态。我们会按照新手教程学习 docker 的主要概念和命令,比如:
-
熟悉 Dockerfile:了解 Dockerfile 是什么?Dockerfile 中有哪些主要的指令?
-
docker build -t xxx:根据 Dockerfile 的过程描述构建镜像。
-
docker run -it xxx:基于镜像实例化并运行容器。
但是随着项目的复杂度提升且涉及多个服务时,我们也发现了一些问题,这些命令太零碎了,如果要组织起一个复杂的项目,毫无疑问会涉及到很多条命令的执行,这个时候手输或复制粘贴命令就很容易出错了,比如有时候命令彼此间的顺序会搞错。此时要么编写流程脚本,要么就要用 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 操作流程说明。
-
开源地址:fullstack-blog