前言
本篇文章将介绍我本人从 0 到 1 实现一个 nestjs
服务自动化部署的过程。
起步
我们做以下几项准备:
- 创建一个
nestjs
工程 - 集成
redis
和mysql
- 安装
Docker Desktop
以及mysql
和redis
- 制作镜像、容器互联、以及使用
Docker Compose
快速编排 - 手动部署工程到服务器
有经验的可以直接跳过这一步~
准备创建一个 nestjs
的新工程:
nest new nest-demo
cd nest-test
注意,我这因为 3000 端口在使用,所以修改为了 4396,正常默认是 3000 端口
随后访问: http://localhost:4396
正常的服务 mysql
和 redis
是必不可少的,我们这里使用 docker
去安装创建
为什么要使用 Docker 部署?
- 可移植性:我们在部署服务的时候很多情况下需要考虑宿主机器环境差异,而
Docker
通过容器化技术,允许开发者将应用程序及其依赖环境打包在一起,形成一个轻量级、独立的容器,这就消除了因环境差异导致的问题 - 环境隔离:也就是说每一个容器都是独立的,可以独立部署,并且可以单独升级或删除,相当于是每个应用程序都自己住在独立的公寓互不影响,还会带来安全性升级
- 简化部署过程:
Docker
容器化技术可以轻松实现应用的打包、分发、部署,简化了应用的部署过程,可以减少部署出错的概率,可以在Docker
环境下快速打开、运行 - 可扩展性:
Docker
容器可以快速复制、水平扩展 - 版本控制:
Docker
容器每一个镜像都有自己的版本号,可以轻松实现回滚、发版 - 强大的生态系统支持:
Docker Hub
社区内有成千上万的镜像,和丰富的资源可以快速使用
安装 Docker Desktop
这一步直接去 Docker
官网下载桌面端即可,桌面端可以帮助我们更简单的管理容器、管理镜像。www.docker.com/
安装 redis & mysql
打开安装好的 Docker
桌面端,搜索安装 redis
和 mysql
,后并运行
工程集成 redis 和 mysql
使用 npm 安装 redis 和 mysql以及相关依赖包,然后在工程中集成进行测试
npm install @nestjs/typeorm mysql2 typeorm redis
在 app.module.ts
集成 redis
和 typeorm
模块,如下:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { createClient } from 'redis';
import { TestEntity } from './test.entity';
import { EntityManager } from 'typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: <your password>,
database: 'actions-test',
synchronize: true,
logging: true,
autoLoadEntities: true,
connectorPackage: 'mysql2',
entities: [TestEntity],
poolSize: 10,
}),
],
controllers: [AppController],
providers: [
{
provide: 'REDIS_CLIENT',
useFactory: async () => {
const client = createClient({
socket: {
host: 'localhost',
port: 6379,
},
});
await client.connect();
return client;
},
},
AppService,
],
})
export class AppModule {
constructor(entity: EntityManager) {
// 初始化时插入一条数据
entity.save(TestEntity, {
name: '小明' + Date.now(),
});
}
}
在工程下创建一个mysql
实体,test.entity.ts
,如下:
import { Column, Entity,PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class TestEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({
type: 'varchar',
length: 255,
})
name: string;
}
- 初始化链接
mysql
和redis
- 初始化时给
mysql
插入一条数据,用于后续的测试
修改app.controller.ts和 app.service.ts,如下:
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('/redis')
redis() {
return this.appService.getRedisKey();
}
@Get('/mysql')
mysql() {
return this.appService.getMysqlData();
}
}
app.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import { RedisClientType } from 'redis';
import { TestEntity } from './test.entity';
import { EntityManager } from 'typeorm';
@Injectable()
export class AppService {
@Inject('REDIS_CLIENT')
private readonly redisClient: RedisClientType;
@InjectEntityManager()
private readonly testEntity: EntityManager;
async getRedisKey() {
await this.redisClient.set('key', 'redis测试');
return await this.redisClient.get('key');
}
async getMysqlData() {
const test = await this.testEntity.getRepository(TestEntity);
return test.find();
}
}
运行项目访问: http://localhost:4396/redis http://localhost:4396/mysql
后续可以把代码提交到 github
上,方便后续部署。
创建 Docker 镜像和容器
在根目录下创建 Docerfile
文件,如下:
# 使用node镜像
FROM node:latest
# 工作目录
WORKDIR /app
# 复制package.json文件到当前目录,方便我们安装依赖包
COPY package.json .
# 设置npm镜像
RUN npm config set registry https://registry.npmmirror.com/
# 安装依赖包
RUN npm install
# 复制整个项目
COPY . .
# 构建项目
RUN npm run build
# 暴露端口
EXPOSE 4396
CMD [ "node", "./dist/main.js" ]
然后我们打包我们的镜像
docker build . -t actions-test:0.1
这个时候会自动根据我们目录下的 Dockerfile
去构建出我们的镜像,一般我们安装依赖时最好设置下 npm
国内镜像,这样构建速度会更快
最终我们可以在 Docker
桌面端中查看到我们的镜像:
我们直接点击运行按钮运行我们的服务:
😱额,果然出意外了!!!
这是因为我们的服务运行在了一个独立的容器内,改容器内没有 redis
和 mysql
,这个时候需要用到 Docker network
帮助我们实现容器互联
使用 docker network 容器互联
# 创建网络 actions-test-net
docker network create -d bridge actions-test-net
随后我们需要在 Docker
桌面端中把在运行中的 redis
和 mysql
删除,因为他们没有加入我们创建的这个网络
随后我们通过命令行去运行 redis
和 mysql
容器,并把他们加入网络
# 运行 redis 容器
docker run -d -it --name redis --network actions-test-net redis
# 运行 mysql 容器
docker run -d -e MYSQL_ROOT_PASSWORD=<your password> -e MYSQL_DATABASE=actions-test -it --name mysql --network actions-test-net mysql
修改app.module.ts
中 redis
和 mysql
的 host
为容器的名称
mysql
redis
重新打包镜像
docker build . -t actions-test:0.2
docker run -d -p 4396:4396 --name actions-test --network actions-test-net actions-test:0.2
我们再去访问这两个地址就会发现已经正常了 http://localhost:4396/mysql http://localhost:4396/redis
每次都这样进行容器互联太麻烦了,而且真实情况我们每次都要在服务器上去这样运行。有没有一种更好的方式呢🤔🤔?欢迎Docker Compose
登场!!!👏👏
Docker Compose 容器编排
Docker Compose
是一个用于定义和运行多容器 Docker 应用程序的工具。通过 docker-compose.yml
文件,我们可以定义一个应用程序的所有服务,并指定每个服务的容器镜像、环境变量、端口映射等配置信息。
它可以帮助我们解决以下问题:
- 多容器互联
- 多个容器之间编排
我们创建一个 dokcer-compose.yml
文件,如下:
version: '3.8'
services:
action-test:
build:
context: ./
dockerfile: ./Dockerfile
# 依赖项
depends_on:
- mysql
- redis
ports:
- '4396:4396'
# 容器重启策略
restart: always
networks:
- common-network
mysql:
image: mysql
ports:
- '3306:3306'
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci # 设置默认字符集
environment:
MYSQL_DATABASE: actions-test
MYSQL_ROOT_PASSWORD: <your password>
restart: always
networks:
- common-network
redis:
image: redis
ports:
- '6379:6379'
restart: always
networks:
- common-network
networks:
common-network:
driver: bridge
我们把之前的容器都给删除一下,然后重新运行 docker-compose.yml
文件
docker-compose up -d
这样就是容器运行成功了,我们同样按照上面那样去访问测试下
手动部署服务器
使用 ssh
登入服务器:
ssh <your user>@<your server ip>
mkdir /var/www/service && cd /var/www/service
git clone <your github repo>
cd <your project name>
docker-compose up -d
这样直接在服务器上使用 docker compose
编排容器就可以了
这样就运行成功了,访问
ip:port
就行了,我这边是
http://39.106.130.29:4396/mysql
http://39.106.130.29:4396/redis
# 销毁容器
docker-compose down
# 查看容器状态
docker-compose ps
# 查看容器日志
docker-compose logs
集成自动化部署
上面我们已经实现了手动部署,大致流程也已经了解了,那么结合Github Actions
,我们就可以实现自动化部署了,大致流程如下:
- 修改代码并
push
到Github
- 触发
Github Actions
的工作流程- 构建镜像
- 推送到镜像仓库
ssh
登录到服务器,更新镜像重启容器
配置 Github Actions
我们在项目根目录下创建 .github/workflows/nest-test.yml
文件,如下:
注意:我这里是该仓库有多个工程,所以指定工程名称,分别在
docker/build-push-action@v5
中指定on push
处指定触发条件
.github/workflows/nest-test.yml
name: 服务部署
on:
push:
branches:
- master
# 设置那些文件发生改变才触发,我这里是因为有多个仓库,所以需要设置
paths:
- "nest-test/**"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Get Metadata
uses: docker/metadata-action@v5
id: metadata
with:
images: thankslyh/nest-test
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{secrets.DOCKER_USER}}
password: ${{secrets.DOCKER_PASSWORD}}
- name: Build and push to Docker Hub
uses: docker/build-push-action@v5
with:
# 注意:我这里是该仓库有多个工程,所以指定工程名称
context: ./nest-test
# 构建出来的镜像的 tag,因为我们 on push 处指定了 master,所以这里指定 master
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
push: true
- name: Update
uses: appleboy/ssh-action@v1.0.3
with:
password: ${{ secrets.ESC_PASSWORD }}
# 部署目标主机
host: ${{ secrets.ESC_HOST }}
# 登录用户
username: ${{ secrets.ESC_USER }}
script: docker login --username=${{secrets.DOCKER_USER}} --password=${{secrets.DOCKER_PASSWORD}} && cd /var/www/actions-test/nest-test && git pull && docker-compose down && docker-compose pull && docker-compose up -d
接下来我们需要在 github
上创建以下这些 secrets
:
- DOCKER_USER -
Docker Hub
用户名 - DOCKER_PASSWORD -
Docker Hub
密码 - ESC_HOST - 部署目标主机
- ESC_PASSWORD - 部署目标主机密码
注意事项:
- 使用
secrets
来保护敏感信息,比如密码,token
等。这样不会在构建的时候被log
出来同时在配置里也看不见- 因为我们上面使用的
metadata-action
这个包,它会从Git Refrence
和Docker
的事件中提取信息,然后我们指定的是提取出来的信息中的tag
作为我们镜像的tag
,所以这里的tag
是master
设置 ESC
服务器DockerHub
镜像加速 Docker 国内镜像加速
同时需要修改docker-compose.yml
文件,nest-test
容器的 image
改为 我们自己的镜像名称,如下:
可以登录 Docker Hub 进行查看自己的镜像,我这里是
thankslyh/nest-test
这样我们修改下工程代码,在 app.controler.ts
里增加一个路由,用来测试部署结果
...
@Get('/deploy')
deploy() {
return 'deploy success';
}
...
然后 push
到 Github
,然后就可以看到 Github Actions
的工作流执行完成
访问 http://39.106.130.29:4396/deploy
到这里我们完成了自动化部署,但是还有问题,每次部署都太慢了要好几分钟,有什么办法加速下?
优化自动化部署
我们要优化构建,首先我们从流程节点中去分析慢的节点都有哪些,从上面流程我们大概能知道完整的流程节点如下:
Checkout
检出代码- 获取元信息
- 登录到
Docker Hub
- 打包镜像并上传
Docker Hub
- 历时 2m51s
ssh
登录服务器,更新代码、更新镜像、重新启动- 50s
但是从上图其中可以看出来,主要耗时在下面两个阶段:
- 构建镜像并上传,耗时 2 分 51 秒
ssh
登录服务器,更新代码、更新镜像、重新启动 50s
所以主要的优化就针对于这两个大的阶段了。
构建优化
构建优化主要针对于打包镜像的优化,该阶段影响部署速度主要分为以下三个方面:
- 构建镜像的耗时
- 镜像上传到镜像仓库的耗时
- 镜像大小
- 带宽(无法控制,可以氪金)
现状镜像大小 1.5G,构建时长 200s 左右,主要是安装依赖阶段
我们可以通过以下方式优化构建镜像的耗时:
- 使用
alpine
的基础镜像,减少镜像体积 - 多阶段构建,降低复杂性、减少依赖、减小文件大小、节约构建时间
- 使用使用
.dockerignore
忽略文件,减少不必要的文件打包到镜像里,减少镜像体积
创建 .dockeringore
文件:
*.md
!README.md
node_modules/
[a-c].txt
.git/
.DS_Store
.vscode/
.dockerignore
.eslintignore
.eslintrc
.prettierrc
.prettierignore
我们修改 Dockerfile
如下:
# 使用alpine基础镜像
FROM node:alpine AS builder
WORKDIR /app
COPY package.json .
RUN npm config set registry https://registry.npmmirror.com/
RUN npm install
COPY . .
RUN npm run build
FROM node:alpine AS runner
COPY --from=builder /app/dist /app
COPY --from=builder /app/package.json /app/package.json
WORKDIR /app
RUN npm config set registry https://registry.npmmirror.com/
RUN npm install --omit=dev
EXPOSE 4396
CMD [ "node", "./dist/main.js" ]
构建镜像后,镜像体积减小了 1.5G -> 500M以下,构建时长 200s -> 150s 左右
我们修改业务代码部署下看看效果如何:
时间缩短到了1 分 55s 和 27s 左右,比之前快了 50%左右,但是比较奇怪的是依赖安装速度还是比较慢,两次阶段的依赖安装时间分别是 80s 和 15s,很奇怪
去掉 npm 的镜像
大胆假设小心推理,既然依赖安装的过程是在 Github Actions
的过程中(理论上本身就是在外网),那是不是就不需要设置 npm
镜像了?理论上是可以的,试试看看
我们把 Dockerfile
中的镜像配置去掉,修改 app.controller.ts
,修改返回结果为"deploy success2"
部署代码看看,奈斯!现在构建时间已经优化到不到 2 分钟了
镜像上传阿里云
因为 Docker Hub
的地址是在国外,虽然我们增加了 Docker Hub
的镜像加速,但还是可能存在不稳定情况,我们修改为使用阿里云的镜像服务,直接把镜像部署到阿里云
阿里云镜像服务 创建命名空间
获取访问凭证
随后我们修改 docker-compose.yml
文件,把镜像地址改为阿里云的镜像地址,如下:
version: '3.8'
services:
action-test:
# 这里是${镜像地址}/${命名空间}/${镜像名称}:${镜像标签}
image: registry.cn-beijing.aliyuncs.com/thankslyh/nest-test:master
...
同时我们修改 .github/workflows/nest-test.yml
如下:
name: 服务部署
...
jobs:
build:
runs-on: ubuntu-latest
steps:
...
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
# 设置登录的仓库
registry: registry.cn-beijing.aliyuncs.com
username: ${{secrets.ALI_DOCKER_USER}}
password: ${{secrets.ALI_DOCKER_PASS}}
...
- name: Update
uses: appleboy/ssh-action@v1.0.3
with:
password: ${{ secrets.ESC_PASSWORD }}
# 部署目标主机
host: ${{ secrets.ESC_HOST }}
# 登录用户
username: ${{ secrets.ESC_USER }}
script: docker login --username=${{secrets.ALI_DOCKER_USER}} --password=${{secrets.ALI_DOCKER_PASS}} registry.cn-beijing.aliyuncs.com && cd /var/www/actions-test/nest-test && docker-compose down && docker-compose pull && docker-compose up -d
注意修改项:
- 增加了登录仓库地址
registry.cn-beijing.aliyuncs.com
,需要改成你自己的- 在 appleboy/ssh-action 里的
script
中增加了指定登录镜像地址的命令 docker login --username=${{secrets.ALI_DOCKER_USER}} --password=${{secrets.ALI_DOCKER_PASS}} registry.cn-beijing.aliyuncs.com
同时我们这里因为新增了两个secrets
变量,所以需要到 github
的设置里添加两个变量:
ALI_DOCKER_USER - 阿里云登录账号 ALI_DOCKER_PASS - 阿里云登录密码
修改工程代码 push master 试试,成功了
虽然速度慢了点,但是好处是稳定,到这里我们整体的流程就完成啦
完整版
Dockerfile
FROM node:alpine AS builder
WORKDIR /app
COPY package.json .
# RUN npm config set registry https://registry.npmmirror.com/
# 需要打包所以需要完整的依赖
RUN npm install
COPY . .
RUN npm run build
FROM node:alpine AS runner
COPY --from=builder /app/dist /app
COPY --from=builder /app/package.json /app/package.json
# RUN npm config set registry https://registry.npmmirror.com/
WORKDIR /app
# 移除开发依赖
RUN npm install --omit=dev
EXPOSE 4396
CMD [ "node", "/app/main.js" ]
.github/workflows/nest-test.yml
name: 服务部署
on:
push:
branches:
- master
# 设置那些文件发生改变才触发,我这里是因为有多个仓库,所以需要设置
paths:
- "nest-test/src/**"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Get Metadata
uses: docker/metadata-action@v5
id: metadata
with:
images: registry.cn-beijing.aliyuncs.com/thankslyh/nest-test
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
# 设置登录的仓库
registry: registry.cn-beijing.aliyuncs.com
username: ${{secrets.ALI_DOCKER_USER}}
password: ${{secrets.ALI_DOCKER_PASS}}
- name: Build and push
uses: docker/build-push-action@v5
with:
# 注意:我这里是该仓库下有多个工程,所以需要指定路径
context: ./nest-test
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
push: true
- name: Update
uses: appleboy/ssh-action@v1.0.3
with:
password: ${{ secrets.ESC_PASSWORD }}
# 部署目标主机
host: ${{ secrets.ESC_HOST }}
# 登录用户
username: ${{ secrets.ESC_USER }}
script: docker login --username=${{secrets.ALI_DOCKER_USER}} --password=${{secrets.ALI_DOCKER_PASS}} registry.cn-beijing.aliyuncs.com && cd /var/www/actions-test/nest-test && docker-compose down && docker-compose pull && docker-compose up -d
docker-compose.yml
version: '3.8'
services:
action-test:
image: registry.cn-beijing.aliyuncs.com/thankslyh/nest-test:master
depends_on:
- mysql
- redis
ports:
- '4396:4396'
restart: always
networks:
- common-network
mysql:
image: mysql
ports:
- '3306:3306'
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci # 设置默认字符集
environment:
MYSQL_DATABASE: actions-test
MYSQL_ROOT_PASSWORD: <your password>
restart: always
networks:
- common-network
redis:
image: redis
ports:
- '6379:6379'
restart: always
networks:
- common-network
networks:
common-network:
driver: bridge
总结
- 我们通过
Github Actions
实现了Docker
镜像的自动打包、上传和部署 - 我们通过分析构建部署过程优化了构建镜像的耗时和上传镜像的耗时
- 使用
alpine
的基础镜像,减少镜像体积 - 多阶段构建,降低复杂性、减少依赖、减小文件大小、节约构建时间
- 使用使用
.dockerignore
忽略文件,减少不必要的文件打包到镜像里,减少镜像体积 - 去掉
npm
的镜像,加快依赖安装速度
- 使用
- 我们把镜像部署到阿里云,提升了部署的稳定性
套个盾:非专业人员,文中有错误的地方欢迎指出交流,谢谢~~ 看到这里了如果觉得有帮助,麻烦点个赞~~
参考
yeasy.gitbook.io/docker_prac… github.com/docker/meta… github.com/docker/buil… github.com/appleboy/ss…