我们的后台管理系统基本功能大致已经完成了,接下来我们要做的就是把它部署到服务器上,让用户可以访问到我们的后台管理系统。本篇文章将以我们开发好的后台管理系统为例,介绍如何使用 docker 部署我们系统的后端Node服务到服务器上。
docker 在前面的文章中已经简单介绍过了,在 windows 系统上我们可以通过docker desktop
来安装和使用管理docker
,非常方便。
Dockerfile
首先我们来了解一下什么是Dockerfile
。
简单来说
Dockerfile
是一个文本文件,包含了一系列指令,用于自动化创建 Docker 镜像的过程。它定义了镜像的基础环境、所需的依赖、应用程序的代码以及如何运行该应用程序等等。比如一个简单的前端项目Dockerfile
如下:
# 使用官方的 Node.js 镜像作为基础镜像
FROM node:14-alpine
# 设置工作目录
WORKDIR /app
# 复制 package.json 和 package-lock.json 到工作目录
COPY package*.json .
# 安装项目依赖
RUN npm install
# 复制项目代码到工作目录
COPY . .
# 构建生产环境的应用程序
RUN npm run build
# 暴露应用程序的端口
EXPOSE3000
# 启动应用程序
CMD ["node", "dist/main.js"]
然后我们就可以通过docker build -t <镜像名称>:<标签> .
将我们的项目打包成一个镜像,就可以通过docker run [OPTIONS] <镜像名称>:<标签>
这个镜像在任何地方运行我们的项目了。
docker-compose
除了Dockerfile
之外,我们还可以使用docker-compose
来管理我们的项目。我们的后台管理系统的后端服务有很多依赖,比如数据库、redis 等等,我们需要一个个拉取镜像然后一个个的去构建启动容器,有点麻烦。所以我们可以使用docker-compose
来定义和管理多个Docker
容器,我们可以在一个docker-compose.yml
文件中定义多个容器的配置,然后通过docker-compose up
来启动所有的容器。比如一个简单的docker-compose.yml
文件如下:
version: "1.0"# 指定 Docker Compose 文件的版本
services:# 定义服务部分
nest-app:# 服务名称,表示 Nest.js 应用
build:# 构建配置
context:./# 构建上下文,指定 Dockerfile 所在的目录
dockerfile:./Dockerfile# 指定 Dockerfile 的路径
depends_on:# 指定依赖关系,确保在启动此服务之前启动依赖的服务
-mysql-container# 依赖 MySQL 容器
-redis-container# 依赖 Redis 容器
ports:# 端口映射
-3000:3000# 将宿主机的 3000 端口映射到容器的 3000 端口
networks:# 指定服务连接的网络
-common-network# 连接到名为 common-network 的网络
mysql-container:# 服务名称,表示 MySQL 数据库
image:mysql# 使用官方 MySQL 镜像
volumes:# 数据卷配置,用于持久化数据
-E:/mysqlData:/var/lib/mysql# 将宿主机的 E:/mysqlData 目录挂载到容器的 /var/lib/mysql 目录
environment:# 环境变量配置
MYSQL_DATABASE:fs_admin# 创建的数据库名称
MYSQL_ROOT_PASSWORD:123456# MySQL 根用户的密码
networks:# 指定服务连接的网络
-common-network# 连接到名为 common-network 的网络
ports:# 端口映射
-3307:3306# 将宿主机的 3307 端口映射到容器的 3306 端口
redis-container:# 服务名称,表示 Redis 数据库
image:redis# 使用官方 Redis 镜像
networks:# 指定服务连接的网络
-common-network# 连接到名为 common-network 的网络
ports:# 端口映射
-6379:6379# 将宿主机的 6379 端口映射到容器的 6379 端口
networks:# 定义网络部分
common-network:# 自定义网络名称
driver:bridge# 使用桥接网络驱动
然后我们就可以通过docker-compose up
来启动所有的容器了,之后将我们的项目部署到服务器上也可以直接执行docker-compose up
就把所有的容器都启动部署了。
编写后端项目 Dockerfile
接下来看一下我们的后台管理系统的后端服务的Dockerfile
如何写:
- 要拉取
node
镜像,因为我们的后端服务是基于node
的。 - 我们需要设置在 docker 中的工作目录。(/app)
- 将项目的
package.json
复制到工作目录中。 - 执行
npm install
安装项目的依赖。 - 将项目的代码复制到工作目录中。
- 执行
npm run build
构建生产环境的应用程序。 - 暴露应用程序的端口。
- 启动应用程序。
FROM node:18.0-alpine3.14
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "run","prod"]
但是这样做会有一个问题,就是我们把项目的代码都复制到 docker 的工作目录中,这样会导致构建镜像的大小变大,其实构建 docker 镜像中只需要打包后的dist
文件以及一些生产环境的依赖就行了,这时候我们该怎么做呢?这里就要用到Docker
的多阶段构建了。
我们将前面一部分构建作为第一阶段命名为build-stage
,第一阶段构建完后我们将其dist
、package.json
、以及我们生产环境所需的.env.prod
复制到第二阶段production-stage
中,然后设置工作目录、环境变量、安装依赖、暴露端口等即可。
FROM node:18.0-alpine3.14 as build-stage
WORKDIR /app
COPY package.json .
RUN npm config set registry https://registry.npmmirror.com/
RUN npm install
COPY . .
RUN npm run build
# production stage
FROM node:18.0-alpine3.14 as production-stage
COPY --from=build-stage /app/dist /app/dist
COPY --from=build-stage /app/package.json /app/package.json
COPY --from=build-stage /app/.env.prod /app/.env.prod
WORKDIR /app
# 环境变量
ENV NODE_ENV production
RUN npm config set registry https://registry.npmmirror.com/
RUN npm install
EXPOSE3000
CMD ["npm", "run","prod"]
这样我们就完成了我们的后端服务的Dockerfile
的编写了。
docker-compose 配置
项目的镜像构建完成后,我们还需要配置docker-compose.yml
文件来管理我们的项目。在docker-compose.yml
我们可以通过${环境变量}
来获取环境变量的值,默认取的是.env
文件中的值,生产环境中我们可以通过--env-file
来指定环境变量的文件,比如我们的项目的环境变量文件是.env.prod
,我们就可以通过docker compose --env-file .env.prod up
来启动我们的项目。
version: "1.0"
services:
nest-app:
build:
context:./
dockerfile:./Dockerfile
depends_on:
-mysql-container
-redis-container
ports:
-3000:3000
networks:
-common-network
volumes:
-${APP_PATH}:/app/static
mysql-container:
image:mysql
volumes:
-${MYSQL_DATA_PATH}:/var/lib/mysql
environment:
MYSQL_DATABASE:${DB_DATABASE}
MYSQL_ROOT_PASSWORD:${DB_PASSWD}
networks:
-common-network
ports:
-3307:${DB_PORT}# 显式映射 MySQL 端口
redis-container:
image:redis
volumes:
-${REDIS_DATA_PATH}:/data
networks:
-common-network
ports:
-6379:${RD_PORT}# 显式映射 Redis 端口
networks:
common-network:
driver:bridge
其中volumes
是配置挂载的目录,可以将宿主机的目录挂载到容器的目录中,这样容器重新启动后,容器中的目录中的文件不会丢失。比如mysql
的数据、redis
的数据、nest
上传的静态文件等等。这些变量我们可以在.env.prod
文件中配置。
# 数据库配置 # 数据库地址 DB_HOST=mysql-container # 数据库端口 DB_PORT=3306 # 数据库登录名 DB_USER=root # 数据库名称 DB_DATABASE=fs_admin # 数据库登录密码 DB_PASSWD=xxx # 数据库Data保存路径 MYSQL_DATA_PATH=/fsAdmin/mysqlData # redis配置 RD_HOST=redis-container # redis端口 RD_PORT=6379 # redis Data保存路径 REDIS_DATA_PATH=/fsAdmin/redisData # JWT配置 JWT_SECRET=xxxx # JWT过期时间 JWT_EXP=2h # 上传文件域名配置 FILESAVEURL=http://xxx xxx:3000/ # 上传文件保存路径 APP_PATH=/fsAdmin/files
我们会发现我们的数据库 DB_HOST 和 redis 的 RD_HOST 都是容器的名称,这是因为我们的容器是通过docker-compose.yml
文件来管理的,我们将其配置在了同一个桥接网络,所以我们可以通过容器的名称来访问容器的服务。
注意:如果我们的项目是开源项目,就比如我这个项目,我们是不能吧.env.prod 上传到 git 上的,不然你的数据库密码等私密信息就会被别人获取到,所以这个文件我会在服务器上进行配置。
负载均衡
我们都知道 nodejs 是单线程的,所以我们需要使用负载均衡的工具来提高我们的服务的并发能力。这里我们选择pm2
为我们的服务开启多个进程。pm2
用法其实很简单,首先在项目中安装pm2
,当然你也可以全局安装。然后再记住它的几个命令即可。
这里我们使用配置文件的形式来配置pm2
。我们直接使用命令pm2 init simple
即可生成一个简单的配置文件ecosystem.config.js
,然后我们配置一下。
module.exports = {
apps: [
{
name: "Nest_APP", //应用名称
log_date_format: "YYYY-MM-DD HH:mm:ss", //日志格式
script: "dist/main.js", //启动文件
out_file: "./log/file.log", //日志文件
error_file: "./log/file_error.log", //错误日志文件
autorestart: true, //是否自动重启
instances: "max", //要启动实例的数量即负载数量,max表示根据cpu的进程数来设置
},
],
};
然后我们在package.json
中修改一下npm run prod
的脚本,同时添加pm2
的几个命令用于测试使用。
"scripts": {
"prod": "pm2 start && pm2 logs",
"pm2:delete": "pm2 delete all",
"pm2:stop": "pm2 stop all",
"pm2:restart": "pm2 restart all",
},
注意启动时加了一个命令 pm2 logs 是因为:docker 部署 pm2 启动的程序时不能直接让 pm2 后台运行,因为 docker 需要一个阻塞控制台的进程,才可以持续运行,否则会关闭,所以执行的时候加一条输出日志的命令用于阻塞控制台进程,docker 容器才不会关闭。
最后修改一下Dockerfile
,将 pm2 配置文件也复制到容器。
此时我们就完成了 pm2 负载均衡的配置了。
本地部署
在部署服务器之前,我们先在本地部署测试一下有没有问题。我们直接在当前项目下执行docker compose --env-file .env.prod up
看一下。
可以看到本地正常启动了,然后随便访问一个接口,可以看到数据正常返回。
说明我们的后端服务已经使用 docker 在本地部署成功了。注意这时候数据库中的表是不会自动创建的,需要我们手动将开发环境的表结构同步到部署环境。
同时控制台执行docker ps
可以查看到我们的三个容器正在运行。
或者直接在docker desktop
中也可以看到。
如果你构建过程出现了问题,需要修改再构建的话最好先执行docker compose down --rmi all
来停止并删除Docker Compose
项目中的所有服务,然后再执行docker compose --env-file.env.pr up
来重新构建。
服务器部署
想要部署到服务器首先要有一台自己的服务器,这里以阿里云服务器为例我购买一个便宜的轻量应用服务器。
应用镜像选 docker,这样就不用再在服务器上手动安装 doker 了。系统镜像选择你喜欢的就行,这里我选择第一个阿里云的 Linux 系统。
下单购买之后我们就拥有了一台自己的服务器了。进入控制台就能看到我们的服务器了。点击进去就可以进行远程连接登录我们的服务器了。
连接成功就进入了服务器的终端界面。
接下来就是将我们的项目部署到服务器上了。我们新建一个文件夹fsAdmin
来存放我们的项目mkdir fsAdmin
,然后进入文件夹cd fsAdmin
。用 git 将项目克隆到这个目录下git clone 你的项目仓库地址
。
clone 完成之后进入我们的后台项目cd fs-admin/admin_nest
,我们前面提到过生产环境的配置文件.env.prod
是不在 git 上的,因此在服务器上我们需要手动创建这个文件,并且配置好环境变量。所以我们需要在 Linux 上安装一个编辑器,这里我选择的是nano
,执行sudo yum install nano
(根据你的 Linux 发行版本不同可能安装方式有所差异)即可完成安装。
安装完成之后我们就可以使用nano .env.prod
来创建或打开这个文件了,然后将生产环境的配置复制到这个文件中即可。
到这里我们的前置操作就完成了。接下来我们就可以直接使用sudo docker compose --env-file.env.prod up
来启动我们的项目了。不出意外的话应该是可以启动成功的。
此时我们的后端项目就已经部署到服务器了,但是此时我们还是访问不到我们的接口的,因为我们的服务器是在阿里云的,我们需要在阿里云的控制台中配置一下安全组。因为我们用的是轻量应用服务器,所以这里我们配置一下防火墙就行,我们需要在防火墙中添加一条规则,允许我们的服务器的 3000 端口和数据库 3007 端口访问。直接点击实例 id,然后点击防火墙将 3000 端口和 3007 端口添加进去即可。
这里解释一下为什么是 3007 而不是 3006? 因为我们的 mysql 在 docker 中的端口是 3006,而它映射到宿主机也就是我们服务器上的端口是 3007。这是在docker-compose.yml
中配置的。
此时我们就应该可以直接使用服务器公网 ip+3000端口
访问我们的接口了并且可以使用3007
来连接数据库了。
最后我们需要将数据库的数据表及基本的数据也同步到服务器上,因为为了安全起见,线上数据表不会像开发环境中自动创建。
我们可以使用数据库连接工具根据线上数据库域名端口及密码连接到线上数据库,然后将数据表及基础数据导入。sql
我已经放在github
上的项目中了fs_admin.sql
,直接导入即可。管理员账户及密码为admin 123456