使用 ssh-action 部署到群晖

注意:本篇没有过多的介绍 Github Actions、ssh key、内网穿透等知识,如果你对这些不熟悉,建议先自行了解一下。

最近写了一些私有的 node.js 小项目,部署在群晖上。然而每次开发、修改完之后,都需要 ssh 到群晖上,然后执行一连串的 cd、git pull、pnpm install、pnpm build、pm2 restart 等命令,非常麻烦,于是想到了使用 Github Actions 来自动化部署。

当然,如果在本地写一个部署脚本,通过 ssh 远程命令的方式来实现也是可以的。但是这样的话,每次修改完代码之后,还需要手动执行一次脚本,这样终归没有直接 push 完之后自动部署来的方便,毕竟 push 这个操作是不可避免的。

由于之前用过 Github 的 self-hosted runner,所以觉得在群晖上操作起来也很容易。结果发现在 ./config.sh 阶段报错了,缺少一些依赖,无奈只能放弃。最后想到可以使用 ssh-action 来部署。

也有想到在群晖的虚拟机套件上安装一个完整的 Ubuntu,这样可以直接使用 self-hosted runner,但我的 Docker 程序都是直接运行在群晖上,感觉为了几个简单的 node.js 项目,另外折腾一个系统有点麻烦了。

前期准备

如果你的目标服务器是在公网上,可以跳过这一步。

由于群晖一般是在家庭局域网中,而 ssh-action 是在 Github 的服务器上执行的,所以需要先将群晖的 ssh 端口映射到公网上,这样 Github 才能访问到群晖。

要实现这个,可以有如下几个选择:

  1. 如果你的家庭宽带有公网 IP,那么直接在路由器上配置端口转发即可。
  2. 如果没有公网 IP,可以使用一些内网穿透工具,比如:npsfrp 等。

我是使用了 nps,需要借助一台公网服务器,在 nps 上开启一个 tcp 隧道,如此就可以用下面的方式来访问群晖了:

ssh -p 18888 username@your_public_ip

其中 18888 是 nps 服务端开启的端口,username 是群晖的用户名,your_public_ip 是公网服务器的 IP。

然而这里遇到了一个问题,使用 ssh 通过密钥登录时,这个密钥对到底是用公网服务器的还是群晖的呢?经过测试,发现是用的群晖的,也就是说,18888 这个端口仅仅是一个转发,用户名和密钥对都是群晖的,等于是给群晖换了一个端口和 IP 而已。

配置 ssh-action

我的项目用到了 express.jstypescriptpnpmpm2 等,所以部署过程涉及到了依赖安装、编译、重启服务等步骤。

假设项目在群晖的路径为 /var/services/homes/ovnrain/node-project/my-app,我们可以这样配置:

name: deploy to synology

on:
  push:
    branches:
      - deploy
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: deploy to synology
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.PRIVATE_KEY }}
          port: ${{ secrets.PORT }}
          script: |
            cd /var/services/homes/ovnrain/node-project/my-app

            git fetch
            git checkout deploy
            git reset --hard origin/deploy
            pnpm i --ignore-scripts --frozen-lockfile
            pnpm build
            pm2 startOrRestart ecosystem.config.json

运行之后发现遇到了一个错误,提示 git、pnpm 等命令不存在,经过研究发现,是因为执行远程命令时,会使用非交互式的 shell,所以 PATH 环境变量不全,需要手动添加(ssh-action 有配置 envs 的选项,但我觉得在 script 中写会更加灵活一些)。

script: |
  export PATH=/var/services/homes/ovnrain/.nvm/versions/node/v18.17.1/bin:/usr/local/bin:$PATH

  cd /var/services/homes/ovnrain/node-project/my-app

  # ...

由于我使用了 nvm 来管理 node.js 版本,所以添加了 /var/services/homes/ovnrain/.nvm/versions/node/v18.17.1/bin 使得在当前 node 版本中安装的 npm、pnpm 等命令可以被找到。/usr/local/bin 是为了可以找到 git 命令。

由于每个人的环境不一样,所以这里的 PATH 可能需要根据自己的情况来修改。你可以通过 which gitwhich pnpm 等命令来查看命令的路径。

至此,ssh-action 部署到群晖的配置就可用了,以后每次 push 到 deploy 分支,就会自动部署到群晖上。

然而用了一会儿发现,假如以后升级了 node.js,比如使用了 19、20 版本,那么这里的 PATH 就需要修改,非常不方便。经过一番研究发现,可以直接把 nvm 官方的初始化脚本放到 script 中,这样无论以后使用哪个版本,都不需要再修改 PATH 了。

script: |
  export PATH=/usr/local/bin:$PATH
  export NVM_DIR="$HOME/.nvm"
  [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

  cd /var/services/homes/ovnrain/node-project/my-app

  # ...

或者更进一步,一般安装了 nvm 之后,.bashrc 已经添加了初始化脚本,所以可以直接使用 source ~/.bashrc,这样连添加 git PATH 都不需要了。

script: |
  source ~/.bashrc

  cd /var/services/homes/ovnrain/node-project/my-app

  # ...

至此,完整的配置如下:

name: deploy to synology

on:
  push:
    branches:
      - deploy
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: deploy to synology
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.PRIVATE_KEY }}
          port: ${{ secrets.PORT }}
          script: |
            source ~/.bashrc

            cd /var/services/homes/ovnrain/node-project/my-app

            git fetch
            git checkout deploy
            git reset --hard origin/deploy
            pnpm i --ignore-scripts --frozen-lockfile
            pnpm build
            pm2 startOrRestart ecosystem.config.json

体验之后发现,使用 ssh-action 相比于 Github 的 self-hosted runner,有以下几个优点:

  1. 不需要在群晖上安装任何依赖,只需要开启 ssh 服务即可,不会污染群晖的环境。
  2. 你可以自由决定使用哪个程序、哪个版本。
  3. 项目路径可以放置在一个固定的地方。
  4. 可以使用 .env 文件来管理敏感信息,否则的话需要在 Github 上配置 secret,编辑删除等都不方便。

如果你的程序是一些简单的静态服务,实际运行不涉及 node_modules.env 等,或者你的机器配置很低,比如 1 CPU、512M 内存,难以支撑在自己的服务器上运行编译、构建等任务,还可以使用 appleboy/scp-actionburnett01/rsync-deployments 等,在 Github 的服务器上安装依赖、编译、打包,然后将最终的输出文件传输到自己的服务器上。

希望这篇文章能对你有所帮助。