引言
最近把像素厂(PixeledPicPro)的前端又翻新了一遍,也把 Nest应用部署到了云服务器,下面分享一下使用 docker-compose 的部署流程,保证你可以按步骤完成 Nest 项目(小水管服务器的)部署工作。
配置文件
本地和服务器是两个环境,所以难免会涉及配置文件问题,这里一并讲了。然后按本地模拟部署和服务器两部分演示
本地部署
首先保证你的 Nest 服务在本地 dev 环境下是正常可用的。有些同学不要一上来就拿不知道哪年的项目开始练手,结果项目本身就有问题。
因为本地和线上是两个不同环境,mysql、redis等很多需要区分环境的属性值都需要统一配置。Nest提供了@nest/config
这个包,可以用来读取环境变量文件,并注入一个 service 到所有 module 里使用。
环境变量文件的类型有很多,可以是.env 文件,也可以是.yaml,也可以是.js, .ts。这里我使用的是.env,下面就以此为例。
.env 文件放在根目录下,如果你是有多个微服务,这个 .env文件默认读取的也是根目录。
在目录中位置如下:
在app.module.ts 中配置 ConfigModule,同样的,在这个文件中把 TypeOrmModule 的配置改成 env里的配置项
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath:
process.env.NODE_ENVIRONMENT === 'production'
? path.join(process.cwd(), '.prod.env')
: path.join(process.cwd(), '.dev.env'),
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory(configService: ConfigService) {
return {
type: 'mysql',
host: configService.get('mysql_server_host'),
port: configService.get('mysql_server_port'),
username: configService.get('mysql_server_username'),
password: configService.get('mysql_server_password'),
database: configService.get('mysql_server_database'),
# 可以同步表结构,先开着
synchronize: true,
logging: true,
entities: [
你的 entities
],
poolSize: 10,
connectorPackage: 'mysql2',
extra: {
authPlugin: 'sha256_password',
},
};
},
inject: [ConfigService],
}),
]
})
redis配置同理,也把之前写死的属性值改成 env 文件里的配置项
redis.module.ts
@Module({
controllers: [RedisController],
providers: [
RedisService,
{
provide: 'REDIS_CLIENT',
async useFactory(configService: ConfigService) {
console.log(`redis 打印`, configService.get('redis_server_port'));
const client = createClient({
socket: {
host: configService.get('redis_server_host'),
port: configService.get('redis_server_port'),
},
database: configService.get('redis_server_db'),
});
await client.connect();
return client;
},
inject: [ConfigService],
},
],
imports: [],
exports: [RedisService],
})
更改完成后,先启动一下本地服务,看看能不能正常访问,如果可以再进行下一步!这一步卡住也没事,慢慢 Google 一下,基本都是常见问题。出现问题再解决才能变成自己的东西。
本地开发的时候,我是用Docker Desktop单独跑了两个容器:mysql、redis。他们的 data 也挂载到了我本地目录上。而上线的时候我需要用 docker-compose 编排 nest、mysql、redis 他们三个,并且 nest 依赖于 mysql 和 redis,下面直接列一下我本地部署时的 docker-compose.yml。我直接把注释写在里面,免去上下滑动对比着看了
有些名称、端口我故意没有起成一样的,方便形成对比
version: '1.0'
services:
# 我的nest服务名称
nest-master:
# 指定了容器名, 这个容器名会用于容器间的通信, 比如你本地 mysql 的 host 是 localhost,那线上就用 zz_master 访问。 不声明的话就是‘nest-master’
container_name: 'zz_master'
# nest项目打包时,用的是他自己目录下的 Dockerfile
build:
context: ./
dockerfile: ./apps/master/Dockerfile
# 依赖于另外两个 service,另外两个 service 会先被构建
depends_on:
- zz-mysql-7
- zz-redis-7
# 端口映射,左边是宿主机上的端口 7008,也就是你打开浏览器http://localhost:7008 可以访问到 nest 的接口,而在容器内部他是运行在 7007 上
ports:
- '7008:7007'
# 失败时重启,有时候 mysql 没启动起来,nest 已经完事了,就会连不上 mysql,所以一直重启,知道 mysql 启动成功
# 不过你的项目如果有bug,他就会无限重启,所以要自己注意了
restart: on-failure
# 声明他们在zzstudio-server 这个网络中,可以用container_name进行访问
# 不声明的话,也会在同一个网络中,名称默认是 项目_default, 比如我这个项目叫 zz-nest, 默认的网络名称就是 zz-nest_default
networks:
- zzstudio-server
# 我的 mysql 服务的名称
zz-mysql-7:
# 我指定的容器名,当 nest服务(也就是上边的 nest-master)要访问 mysql 时,mysql的 host 就配置为 zz_mysql
container_name: 'zz_mysql'
image: mysql
# 端口映射,当你从外部访问 3307 时会被映射到容器内部的 3306 上。
# 因为这里我们三个服务在同一个网络下,所以我们prod.env 使用的应该是 3306
ports:
- '3307:3306'
# 挂载的本地目录
volumes:
- /我的本地目录/mysql:/var/lib/mysql
# 可以用于初始化时执行一些 sql,我查阅的野文里,有的用来解决数据库没有被创建,或用来表结构初始化,有需要的自行尝试
# - ./init.sql/:/docker-entrypoint-initdb.d/init.sql
# 相关的环境变量,密码应该是必须要设置的,忘了咋回事了
environment:
# 如果你的 mysql 是 8.x 不要指定 MYSQL_USER=root,会报错
# 在指定了MYSQL_DATABASE后,会自动创建这个数据库!
MYSQL_DATABASE: zzstudio
MYSQL_ROOT_PASSWORD: 123456
# 同样,显式的声明在一个网络下
networks:
- zzstudio-server
# 我的redis服务名称
zz-redis-7:
# 我指定的容器名称,当 nest 应用要连接 redis 时,redis server host 直接写这个‘zz_redis’ 即可
container_name: 'zz_redis'
image: redis
# 端口映射,道理和 mysql 一样。可以看下上边的描述
ports:
- '6378:6379'
volumes:
- /我的本地目录/redis:/data
networks:
- zzstudio-server
# 声明网络,和上边所有的 server 下边的 networks 相对应
networks:
zzstudio-server:
driver: bridge
然后再对比看一下本地和线上的环境配置,我放在了一起演示,可以自己体会一下
redis_server_host=localhost #dev
redis_server_host=zz_redis #prod 对应 docker-compose.yml 里的 redis 的 container_name
redis_server_port=6379 #dev
redis_server_port=6379 #prod ports 配的是 6378:6379,这里用的是 6379,因为他们已经在同一个网络里
redis_server_db=1
mysql_server_host=localhost # dev
mysql_server_host=zz_mysql # prod 对应 docker-compose.yml 里的 msyql 的 container_name
mysql_server_port=3307# dev 我本地 docker单独启用 mysql 时, 使用的 ports 是 3307:3306,所以我 nest 访问 mysql是用 3307 访问,从外部访问。同理 navicat 这种软件访问也需要从 3307
mysql_server_port=3306# prod 线上ports 配的也是 3307:3306,这里用的是 3306,因为他们已经在同一个网络里
#mysql_server_username=用户名
#mysql_server_password=密码
#mysql_server_database=数据库名称
nest 应用的 Dockerfile 如下,要注意的是我单独把 prod.env
复制进去了,不然不会扔进去。你也可以先自己试试,看看报错信息,思考一下是哪里出了问题,再回过头来按我这个流程排查。这样也能加深记忆。
这个 Dockfile 我也是参考了很多野文,这方面不再赘述了,能跑起来就行!
要注意:master 是我的服务的文件名,要记得换成自己的
FROM node:18.18.2-alpine3.18 as build-stage
WORKDIR /app
RUN npm install -g pnpm
COPY package.json .
RUN pnpm install
COPY . .
RUN pnpm build:master
FROM node:18.18.2-alpine3.18 as production-stage
COPY --from=build-stage /app/dist/apps /app/apps
COPY --from=build-stage /app/.prod.env /app/.prod.env
COPY --from=build-stage /app/package.json /app/package.json
WORKDIR /app
RUN npm install -g pnpm
ENV NODE_ENVIRONMENT=production
RUN pnpm install --production
CMD [ "node", "./apps/master/main.js" ]
EXPOSE 7007
写好了Dockerfile、 docker-compose.yml 和本地 env,我们先把本地当线上跑一下,跑之前先看看自己本地的docker 运行情况,不要冲突了。
docker-compose up
跑完后,出现下图,即为成功
如果出现了类似以下报错,可能是因为没建成数据库,也可能是你挂载的本地目录/你的目录/mysql
不为空,导致初始化失败。
也可能会出现 178.x.x.x:端口号 访问不到的报错,一般是你配置文件里 port
配错了,应该配成容器的 network
的 bridge
的 Gateway
地址
可以往上滑一下日志,看看具体报错信息,如有不知名报错,也可以在公众号:早早集市 找到我,我再尝试复现一下。
zz_master | Error: getaddrinfo EAI_AGAIN zz_mysql
然后我们拿浏览器测试一下 Get接口即可,能返回数据,终端里有输出日志就行啦
然后开始部署到云服务器!
服务器部署
往服务器部署前还需要改一下 docker-compose.yml
mysql、redis的 volumes
改为服务器上的地址,没有的话先新建一下
然后代码传到服务器上,可以用各种方式,如 git、ftp、jenkins等。
然后我们通过 ssh 连一下云服务器,然后进入到项目的根目录下, 运行
docker-compose up
如果你的服务器上只安装了 docker 没有 docker-compose,可以使用以下命令先安装一下
sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
在经过几次 retry之后,可以看到服务成功启动!
zz_master | [Nest] 1 - 12/24/2023, 12:32:59 PM ERROR [TypeOrmModule] Unable to connect to the database. Retrying (4)...
使用 docker-compose down
停止,然后使用docker-compose up -d
,在后台启动即可
然后使用docker ps
,查看已经在运行的容器~
docker-compose down
运行后也可以看到,启动的是三个 Container,一个 Network,Network 就是上边指定的网络
[+] Running 4/4
⠿ Container zz_master Removed 10.4s
⠿ Container zz_redis Removed 0.4s
⠿ Container zz_mysql Removed 1.1s
⠿ Network server_zzstudio-server Removed
ok,服务已经启动,接下来我用 Ngxin 做代理,把接口代理到 nest 服务上(容器的对外端口上)
nginx 也是一个 docker 单独启动的容器,目前用来挂载一个前端应用的目录,靠Termius
的 SFTP
部署前端,因为Jenkins 太吃内存,后面我再调研一下有没有平替的方案。运维方面也不着急优化。
Nginx 转发到 Nest
一开始我是这样配置的
location ^~ /api/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://localhost:7007/;
}
在服务器里使用 curl
是可以调通Nest的接口的,但是用 Apifox
调不同,会报 502,查看错误日志
2023/12/24 08:04:24 [warn] 24#24: *4 upstream server temporarily disabled while connecting to upstream, client: xxxx, server: zzstudio.cn, request: "GET /api/user/aaa HTTP/1.1", upstream: "http://127.0.0.1:7007/user/aaa", host: "zzstudio.cn"
2023/12/24 08:04:25 [error] 24#24: *4 no live upstreams while connecting to upstream, client: xxx, server: zzstudio.cn, request: "GET /api/user/aaa HTTP/1.1", upstream: "http://localhost/user/aaa", host: "zzstudio.cn"
然后把配置里的 localhost 改成自己服务器的公网 ip,发现还是不行
然后又查看了一下 nginx 容器里的 ip
# 我的nginx容器名称叫nginx
docker inspect nginx
拉到最后,看到 Networks
里,bridge
的 Gateway
地址,把上边 location
里的 proxy_pass
改成 http://Gateway 地址: 端口号/
, 再去 Apifox 尝试下自己的接口
location ^~ /api/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://Gateway 地址:7007/;
}
成功了👏
小结
以上就是我使用 docker-compose 部署 Nest 应用的全部过程,如果对你有帮助的话,还望点个关注、点赞支持一下~
也欢迎关注公众号:早早集市,第一时间围观我继续产出其他产品和文章~
感谢你的阅读,我是枣把儿~