部署
记录从零一步步将 Nest.js 项目部署到 Linux 服务器的过程。
配置 ssh 免密登录服务器
- 如果本机没有生成过 sshkey,那么就新生成一个,执行以下指令,然后一直 Enter
ssh-keygen
- 执行指令,将密钥添加到远程服务器的授权配置中:
ssh-copy-id user@host
可参考 手动上传密钥
- 测试密钥登录:
ssh user@host
给 github action 配置服务器密钥访问
因为需要服务器上直接从 github 上拉取代码,本地编译并启动服务器,所以需要有服务器有访问代码仓库的权限。
- ssh 连接服务器,执行
ssh-keygen
生成密钥,并将公钥配置到 github 的私人密钥中
本机公钥默认地址:~/.ssh/id_rsa.pub
, github 配置密钥入口:个人/Settings/SSH and GPG keys
- 将刚才生成的私钥配置到仓库的
secrets
中。取名为SSH_PRIVATE_KEY
,这个变量定义将在 github action 中用到
本机私钥默认地址:~/.ssh/id_rsa
, github 项目仓库配置secrets
入口:仓库Settings/Secrets/New repository secret
- 将远程服务器的地址也配置到仓库的
Secrets
中,取名为SERVER_HOST
,github action 将会用到
服务器代码编译
- 安装 git
linux 默认不安装 git,需要手动安装:
yum install git
- 复制 github 上项目的 ssh 访问地址,在服务器中拉取代码:
# 创建项目存放的文件夹
mkdir ~/data/server
cd ~/data/server
git clone git@github.com:daichangxin/xxx.git
配置 node 环境
使用n
来管理 node 环境。
- 安装
n
# 创建一个放安装包的文件夹
mkdir ~/data/installs
curl -L https://raw.githubusercontent.com/tj/n/master/bin/n -o n
bash n lts
这样 node 环境就安装好了,为了方便以后全局使用n
,再执行下全局安装n
:
npm i n -g
- 安装
pm2
来管理服务器进程
npm i pm2 -g
github action 配置
在仓库目录下创建 ci 配置文件:.github/workflows/ci.yml
,内容:
name: Deploy Server
on:
push:
branches:
- master
jobs:
deploy:
if: "!contains(github.event.head_commit.message, 'skip ci')"
runs-on: ubuntu-latest
steps:
- run: |
eval $(ssh-agent -s)
echo "${{secrets.SSH_PRIVATE_KEY}}" > ssh.key
mkdir -p ~/.ssh
chmod 0600 ssh.key
ssh-add ssh.key
echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
ssh -p ${{secrets.SERVER_PORT}} root@${{secrets.SERVER_HOST}} "cd ~/data/server/xxx/ && npm run server:update"
从上到下解释:
- 仅在 master 分支上 push 代码时触发。
- 如果提交信息中包含了
skip ci
字样,则略过 ci 编译 - 将
Secrets
中配置的服务器私钥,配置到 ci 当前的机器上,让当前机器拥有密钥访问远程服务器的权限 - 在 ci 机器上密钥连接服务器并执行指令:进入仓库目录,并执行服务更新脚本
服务器脚本
仓库 package.json 中涉及到服务器操作的脚本:
"build": "rimraf dist && nest build",
"server:start": "npm ci && npm run build && pm2 start dist/main.js -i max --name server --env production -f",
"server:reload": "pm2 reload server",
"server:update": "git pull && npm ci && npm run build && npm run server:reload",
从上到下解释:
- 编译项目
- 首次拉取仓库时,创建
server
服务,仅首次执行一次即可 - 重启服务脚本,一般不需要手动执行,都是走后面的
server:update
- 在 ci 中会调用到
server:update
,拉取最新代码,安装类库,重新编译项目,执行服务重启
配置开机自动启动服务
- 生成 pm2-root 的启动脚本,且自动将 pm2-root 设为服务
pm2 startup
输出:
[PM2] Init System found: systemd
Platform systemd
Template
[Unit]
Description=PM2 process manager
Documentation=https://pm2.keymetrics.io/
After=network.target
[Service]
Type=forking
User=root
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
Environment=PM2_HOME=/root/.pm2
PIDFile=/root/.pm2/pm2.pid
Restart=on-failure
ExecStart=/usr/local/lib/node_modules/pm2/bin/pm2 resurrect
ExecReload=/usr/local/lib/node_modules/pm2/bin/pm2 reload all
ExecStop=/usr/local/lib/node_modules/pm2/bin/pm2 kill
[Install]
WantedBy=multi-user.target
Target path
/etc/systemd/system/pm2-root.service
Command list
[ 'systemctl enable pm2-root' ]
[PM2] Writing init configuration in /etc/systemd/system/pm2-root.service
[PM2] Making script booting at startup...
[PM2] [-] Executing: systemctl enable pm2-root...
Created symlink from /etc/systemd/system/multi-user.target.wants/pm2-root.service to /etc/systemd/system/pm2-root.service.
[PM2] [v] Command successfully executed.
+---------------------------------------+
[PM2] Freeze a process list on reboot via:
$ pm2 save
[PM2] Remove init script via:
$ pm2 unstartup systemd
- 将当前 pm2 所运行的应用保存在 /root/.pm2/dump.pm2 下,当开机重启时,运行 pm2-root 服务脚本,并且到 /root/.pm2/dump.pm2 下读取应用并启动
pm2 save
输出:
[PM2] Saving current process list...
[PM2] Successfully saved in /root/.pm2/dump.pm2
配置 Nginx
为啥需要 Nginx?因为需要获取用户的真实 IP、设置跨域等。这里只做简单的转发,设置获取 ip 的 header 头。
-
安装 略
-
在配置目录
conf.d
下,新建server.conf
文件,必须以conf
结尾,nginx 默认会加载该目录下的所有配置。内容如下:
server {
listen 80;
location /trade/ {
proxy_http_version 1.1;
proxy_pass http://127.0.0.1:3000/;
proxy_set_header Connection "keep-alive";
proxy_set_header X-Real-IP $remote_addr;
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Headers "x-token, Origin, X-Requested-With, Content-Type, Accept, Authorization" always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
if ($request_method = 'OPTIONS') {
return 204;
}
}
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
表示访问服务器的xxx.com/trade/
会自动转发到http://127.0.0.1:3000/
。
注意:
-
add_header 配置后要加
always
,否则部分服务器的错误码会被 nginx 拦截并提示 header 跨域错误。 -
如果
server.conf
配置不生效,查看是不是被default.conf
拦截了,可以删除default.conf
,或者去掉default.conf
中对 location/
的处理。 -
如果提示跨域 Header 头重复,查看下是不是服务器代码中是不是也设置了跨域,有 Nginx 的话服务器中的跨域不需要设置了。