如何使用Docker將node項目部署到服務(wù)器并用pm2做負(fù)載均衡?
我們的后臺管理系統(tǒng)基本功能大致已經(jīng)完成了,接下來我們要做的就是把它部署到服務(wù)器上,讓用戶可以訪問到我們的后臺管理系統(tǒng)。本篇文章將以我們開發(fā)好的后臺管理系統(tǒng)為例,介紹如何使用 docker 部署我們系統(tǒng)的后端Node服務(wù)到服務(wù)器上。
docker 在前面的文章中已經(jīng)簡單介紹過了,在 windows 系統(tǒng)上我們可以通過docker desktop
來安裝和使用管理docker
,非常方便。
Dockerfile
首先我們來了解一下什么是Dockerfile
。
簡單來說
Dockerfile
是一個文本文件,包含了一系列指令,用于自動化創(chuàng)建 Docker 鏡像的過程。它定義了鏡像的基礎(chǔ)環(huán)境、所需的依賴、應(yīng)用程序的代碼以及如何運行該應(yīng)用程序等等。比如一個簡單的前端項目Dockerfile
如下:
# 使用官方的 Node.js 鏡像作為基礎(chǔ)鏡像
FROM node:14-alpine
# 設(shè)置工作目錄
WORKDIR /app
# 復(fù)制 package.json 和 package-lock.json 到工作目錄
COPY package*.json .
# 安裝項目依賴
RUN npm install
# 復(fù)制項目代碼到工作目錄
COPY . .
# 構(gòu)建生產(chǎn)環(huán)境的應(yīng)用程序
RUN npm run build
# 暴露應(yīng)用程序的端口
EXPOSE3000
# 啟動應(yīng)用程序
CMD ["node", "dist/main.js"]
然后我們就可以通過docker build -t <鏡像名稱>:<標(biāo)簽> .
將我們的項目打包成一個鏡像,就可以通過docker run [OPTIONS] <鏡像名稱>:<標(biāo)簽>
這個鏡像在任何地方運行我們的項目了。
docker-compose
除了Dockerfile
之外,我們還可以使用docker-compose
來管理我們的項目。我們的后臺管理系統(tǒng)的后端服務(wù)有很多依賴,比如數(shù)據(jù)庫、redis 等等,我們需要一個個拉取鏡像然后一個個的去構(gòu)建啟動容器,有點麻煩。所以我們可以使用docker-compose
來定義和管理多個Docker
容器,我們可以在一個docker-compose.yml
文件中定義多個容器的配置,然后通過docker-compose up
來啟動所有的容器。比如一個簡單的docker-compose.yml
文件如下:
version: "1.0"# 指定 Docker Compose 文件的版本
services:# 定義服務(wù)部分
nest-app:# 服務(wù)名稱,表示 Nest.js 應(yīng)用
build:# 構(gòu)建配置
context:./# 構(gòu)建上下文,指定 Dockerfile 所在的目錄
dockerfile:./Dockerfile# 指定 Dockerfile 的路徑
depends_on:# 指定依賴關(guān)系,確保在啟動此服務(wù)之前啟動依賴的服務(wù)
-mysql-container# 依賴 MySQL 容器
-redis-container# 依賴 Redis 容器
ports:# 端口映射
-3000:3000# 將宿主機的 3000 端口映射到容器的 3000 端口
networks:# 指定服務(wù)連接的網(wǎng)絡(luò)
-common-network# 連接到名為 common-network 的網(wǎng)絡(luò)
mysql-container:# 服務(wù)名稱,表示 MySQL 數(shù)據(jù)庫
image:mysql# 使用官方 MySQL 鏡像
volumes:# 數(shù)據(jù)卷配置,用于持久化數(shù)據(jù)
-E:/mysqlData:/var/lib/mysql# 將宿主機的 E:/mysqlData 目錄掛載到容器的 /var/lib/mysql 目錄
environment:# 環(huán)境變量配置
MYSQL_DATABASE:fs_admin# 創(chuàng)建的數(shù)據(jù)庫名稱
MYSQL_ROOT_PASSWORD:123456# MySQL 根用戶的密碼
networks:# 指定服務(wù)連接的網(wǎng)絡(luò)
-common-network# 連接到名為 common-network 的網(wǎng)絡(luò)
ports:# 端口映射
-3307:3306# 將宿主機的 3307 端口映射到容器的 3306 端口
redis-container:# 服務(wù)名稱,表示 Redis 數(shù)據(jù)庫
image:redis# 使用官方 Redis 鏡像
networks:# 指定服務(wù)連接的網(wǎng)絡(luò)
-common-network# 連接到名為 common-network 的網(wǎng)絡(luò)
ports:# 端口映射
-6379:6379# 將宿主機的 6379 端口映射到容器的 6379 端口
networks:# 定義網(wǎng)絡(luò)部分
common-network:# 自定義網(wǎng)絡(luò)名稱
driver:bridge# 使用橋接網(wǎng)絡(luò)驅(qū)動
然后我們就可以通過docker-compose up
來啟動所有的容器了,之后將我們的項目部署到服務(wù)器上也可以直接執(zhí)行docker-compose up
就把所有的容器都啟動部署了。
編寫后端項目 Dockerfile
接下來看一下我們的后臺管理系統(tǒng)的后端服務(wù)的Dockerfile
如何寫:
- 要拉取
node
鏡像,因為我們的后端服務(wù)是基于node
的。 - 我們需要設(shè)置在 docker 中的工作目錄。(/app)
- 將項目的
package.json
復(fù)制到工作目錄中。 - 執(zhí)行
npm install
安裝項目的依賴。 - 將項目的代碼復(fù)制到工作目錄中。
- 執(zhí)行
npm run build
構(gòu)建生產(chǎn)環(huán)境的應(yīng)用程序。 - 暴露應(yīng)用程序的端口。
- 啟動應(yīng)用程序。
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"]
但是這樣做會有一個問題,就是我們把項目的代碼都復(fù)制到 docker 的工作目錄中,這樣會導(dǎo)致構(gòu)建鏡像的大小變大,其實構(gòu)建 docker 鏡像中只需要打包后的dist
文件以及一些生產(chǎn)環(huán)境的依賴就行了,這時候我們該怎么做呢?這里就要用到Docker
的多階段構(gòu)建了。
我們將前面一部分構(gòu)建作為第一階段命名為build-stage
,第一階段構(gòu)建完后我們將其dist
、package.json
、以及我們生產(chǎn)環(huán)境所需的.env.prod
復(fù)制到第二階段production-stage
中,然后設(shè)置工作目錄、環(huán)境變量、安裝依賴、暴露端口等即可。
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
# 環(huán)境變量
ENV NODE_ENV production
RUN npm config set registry https://registry.npmmirror.com/
RUN npm install
EXPOSE3000
CMD ["npm", "run","prod"]
這樣我們就完成了我們的后端服務(wù)的Dockerfile
的編寫了。
docker-compose 配置
項目的鏡像構(gòu)建完成后,我們還需要配置docker-compose.yml
文件來管理我們的項目。在docker-compose.yml
我們可以通過${環(huán)境變量}
來獲取環(huán)境變量的值,默認(rèn)取的是.env
文件中的值,生產(chǎn)環(huán)境中我們可以通過--env-file
來指定環(huán)境變量的文件,比如我們的項目的環(huán)境變量文件是.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
的數(shù)據(jù)、redis
的數(shù)據(jù)、nest
上傳的靜態(tài)文件等等。這些變量我們可以在.env.prod
文件中配置。
# 數(shù)據(jù)庫配置 # 數(shù)據(jù)庫地址 DB_HOST=mysql-container # 數(shù)據(jù)庫端口 DB_PORT=3306 # 數(shù)據(jù)庫登錄名 DB_USER=root # 數(shù)據(jù)庫名稱 DB_DATABASE=fs_admin # 數(shù)據(jù)庫登錄密碼 DB_PASSWD=xxx # 數(shù)據(jù)庫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
我們會發(fā)現(xiàn)我們的數(shù)據(jù)庫 DB_HOST 和 redis 的 RD_HOST 都是容器的名稱,這是因為我們的容器是通過docker-compose.yml
文件來管理的,我們將其配置在了同一個橋接網(wǎng)絡(luò),所以我們可以通過容器的名稱來訪問容器的服務(wù)。
注意:如果我們的項目是開源項目,就比如我這個項目,我們是不能吧.env.prod 上傳到 git 上的,不然你的數(shù)據(jù)庫密碼等私密信息就會被別人獲取到,所以這個文件我會在服務(wù)器上進行配置。
負(fù)載均衡
我們都知道 nodejs 是單線程的,所以我們需要使用負(fù)載均衡的工具來提高我們的服務(wù)的并發(fā)能力。這里我們選擇pm2
為我們的服務(wù)開啟多個進程。pm2
用法其實很簡單,首先在項目中安裝pm2
,當(dāng)然你也可以全局安裝。然后再記住它的幾個命令即可。
這里我們使用配置文件的形式來配置pm2
。我們直接使用命令pm2 init simple
即可生成一個簡單的配置文件ecosystem.config.js
,然后我們配置一下。
module.exports = {
apps: [
{
name: "Nest_APP", //應(yīng)用名稱
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", //要啟動實例的數(shù)量即負(fù)載數(shù)量,max表示根據(jù)cpu的進程數(shù)來設(shè)置
},
],
};
然后我們在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 需要一個阻塞控制臺的進程,才可以持續(xù)運行,否則會關(guān)閉,所以執(zhí)行的時候加一條輸出日志的命令用于阻塞控制臺進程,docker 容器才不會關(guān)閉。
最后修改一下Dockerfile
,將 pm2 配置文件也復(fù)制到容器。
此時我們就完成了 pm2 負(fù)載均衡的配置了。
本地部署
在部署服務(wù)器之前,我們先在本地部署測試一下有沒有問題。我們直接在當(dāng)前項目下執(zhí)行docker compose --env-file .env.prod up
看一下。
可以看到本地正常啟動了,然后隨便訪問一個接口,可以看到數(shù)據(jù)正常返回。
說明我們的后端服務(wù)已經(jīng)使用 docker 在本地部署成功了。注意這時候數(shù)據(jù)庫中的表是不會自動創(chuàng)建的,需要我們手動將開發(fā)環(huán)境的表結(jié)構(gòu)同步到部署環(huán)境。
同時控制臺執(zhí)行docker ps
可以查看到我們的三個容器正在運行。
或者直接在docker desktop
中也可以看到。
如果你構(gòu)建過程出現(xiàn)了問題,需要修改再構(gòu)建的話最好先執(zhí)行docker compose down --rmi all
來停止并刪除Docker Compose
項目中的所有服務(wù),然后再執(zhí)行docker compose --env-file.env.pr up
來重新構(gòu)建。
服務(wù)器部署
想要部署到服務(wù)器首先要有一臺自己的服務(wù)器,這里以阿里云服務(wù)器為例我購買一個便宜的輕量應(yīng)用服務(wù)器。
應(yīng)用鏡像選 docker,這樣就不用再在服務(wù)器上手動安裝 doker 了。系統(tǒng)鏡像選擇你喜歡的就行,這里我選擇第一個阿里云的 Linux 系統(tǒng)。
下單購買之后我們就擁有了一臺自己的服務(wù)器了。進入控制臺就能看到我們的服務(wù)器了。點擊進去就可以進行遠(yuǎn)程連接登錄我們的服務(wù)器了。
連接成功就進入了服務(wù)器的終端界面。
接下來就是將我們的項目部署到服務(wù)器上了。我們新建一個文件夾fsAdmin
來存放我們的項目mkdir fsAdmin
,然后進入文件夾cd fsAdmin
。用 git 將項目克隆到這個目錄下git clone 你的項目倉庫地址
。
clone 完成之后進入我們的后臺項目cd fs-admin/admin_nest
,我們前面提到過生產(chǎn)環(huán)境的配置文件.env.prod
是不在 git 上的,因此在服務(wù)器上我們需要手動創(chuàng)建這個文件,并且配置好環(huán)境變量。所以我們需要在 Linux 上安裝一個編輯器,這里我選擇的是nano
,執(zhí)行sudo yum install nano
(根據(jù)你的 Linux 發(fā)行版本不同可能安裝方式有所差異)即可完成安裝。
安裝完成之后我們就可以使用nano .env.prod
來創(chuàng)建或打開這個文件了,然后將生產(chǎn)環(huán)境的配置復(fù)制到這個文件中即可。
到這里我們的前置操作就完成了。接下來我們就可以直接使用sudo docker compose --env-file.env.prod up
來啟動我們的項目了。不出意外的話應(yīng)該是可以啟動成功的。
此時我們的后端項目就已經(jīng)部署到服務(wù)器了,但是此時我們還是訪問不到我們的接口的,因為我們的服務(wù)器是在阿里云的,我們需要在阿里云的控制臺中配置一下安全組。因為我們用的是輕量應(yīng)用服務(wù)器,所以這里我們配置一下防火墻就行,我們需要在防火墻中添加一條規(guī)則,允許我們的服務(wù)器的 3000 端口和數(shù)據(jù)庫 3007 端口訪問。直接點擊實例 id,然后點擊防火墻將 3000 端口和 3007 端口添加進去即可。
這里解釋一下為什么是 3007 而不是 3006? 因為我們的 mysql 在 docker 中的端口是 3006,而它映射到宿主機也就是我們服務(wù)器上的端口是 3007。這是在docker-compose.yml
中配置的。
此時我們就應(yīng)該可以直接使用服務(wù)器公網(wǎng) ip+3000端口
訪問我們的接口了并且可以使用3007
來連接數(shù)據(jù)庫了。
最后我們需要將數(shù)據(jù)庫的數(shù)據(jù)表及基本的數(shù)據(jù)也同步到服務(wù)器上,因為為了安全起見,線上數(shù)據(jù)表不會像開發(fā)環(huán)境中自動創(chuàng)建。
我們可以使用數(shù)據(jù)庫連接工具根據(jù)線上數(shù)據(jù)庫域名端口及密碼連接到線上數(shù)據(jù)庫,然后將數(shù)據(jù)表及基礎(chǔ)數(shù)據(jù)導(dǎo)入。sql
我已經(jīng)放在github
上的項目中了fs_admin.sql
,直接導(dǎo)入即可。管理員賬戶及密碼為admin 123456