打开/关闭菜单
打开/关闭外观设置菜单
打开/关闭个人菜单
未登录
未登录用户的IP地址会在进行任意编辑后公开展示。

实战笔记:搬瓦工 Docker 部署 .NET 8 与 BWH API 自动监控流水线

来自md5.pw
Musazu留言 | 贡献2026年4月15日 (三) 05:25的版本 (创建页面,内容为“最近社区里有不少帖子在讨论各种面板、测速,或者教大家写 Bash 脚本查 VPS 流量。 其实对于开发者来说,买一台网络稳的机器(比如搬瓦工的 GIA),最大的用途还是跑自己的后端服务。但这年头,如果每次改完代码还要手动 FTP 传文件、在宿主机折腾各种运行时环境,不仅容易把系统弄脏,维护起来也挺繁琐。 今天分享一套我日常在用的 DevOps 工…”)
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)

最近社区里有不少帖子在讨论各种面板、测速,或者教大家写 Bash 脚本查 VPS 流量。

其实对于开发者来说,买一台网络稳的机器(比如搬瓦工的 GIA),最大的用途还是跑自己的后端服务。但这年头,如果每次改完代码还要手动 FTP 传文件、在宿主机折腾各种运行时环境,不仅容易把系统弄脏,维护起来也挺繁琐。

今天分享一套我日常在用的 DevOps 工作流:在 Linux 下用 **Docker Compose** 隔离运行 .NET 8 服务,搭配 **GitHub Actions** 做全自动 CI/CD。顺便用 C# 对接一下搬瓦工官方的 KiwiVM API,写个 Telegram 监控端。废话不多说,直接开始。

## 01. 代码实现:C# 调用 KiwiVM API 与 TG 机器人

相比传统的 Bash 脚本定时任务,用 C# 的 `BackgroundService` 做长连接监控更稳定。另外,很多人用脚本查 API 经常会算错流量,主要是没留意官方文档里的两个细节:**搬瓦工的高端机房流量是有 `monthly_data_multiplier`(流量乘数)的,而且 API 返回的日期是 UNIX 时间戳。**

我们在 .NET 8 Worker 项目中引入 `Telegram.Bot`,顺手把这两个容易踩坑的地方处理掉:

```csharp

using Telegram.Bot;

using Telegram.Bot.Types;

using System.Net.Http.Json;

public class BwhMonitorWorker : BackgroundService

{

    private readonly TelegramBotClient _botClient = new("你的_TG_BOT_TOKEN");

    private readonly HttpClient _http = new();

   

    private const string API_HOST = "https://" + "api.64clouds.com";

    private const string VEID = "你的VEID";

    private const string API_KEY = "你的API_KEY";

    private const long ADMIN_CHAT_ID = 123456789; // 你的 TG 账号 ID,防止别人蹭用

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)

    {

        _botClient.StartReceiving(HandleUpdateAsync, HandleErrorAsync, null, stoppingToken);

        while (!stoppingToken.IsCancellationRequested) await Task.Delay(1000, stoppingToken);

    }

    private async Task HandleUpdateAsync(ITelegramBotClient bot, Update update, CancellationToken ct)

    {

        if (update.Message?.Text == null || update.Message.Chat.Id != ADMIN_CHAT_ID) return;

       

        var text = update.Message.Text;

        var chatId = update.Message.Chat.Id;

        if (text == "/status")

        {

            var url = $"{API_HOST}/v1/getServiceInfo?veid={VEID}&api_key={API_KEY}";

            var res = await _http.GetFromJsonAsync<BwhInfo>(url, ct);

           

            if (res.error == 0)

            {

                // 细节 1:计算时必须带上官方规定的流量乘数 (monthly_data_multiplier)

                double multiplier = res.monthly_data_multiplier > 0 ? res.monthly_data_multiplier : 1;

                double usedGb = (res.data_counter * multiplier) / 1024.0 / 1024.0 / 1024.0;

                double limitGb = (res.plan_monthly_data * multiplier) / 1024.0 / 1024.0 / 1024.0;

               

                // 细节 2:UNIX 时间戳转换为本地直观时间

                DateTime resetDate = DateTimeOffset.FromUnixTimeSeconds(res.data_next_reset).LocalDateTime;

               

                await bot.SendTextMessageAsync(chatId,

                    $"🖥️ 主机名: {res.hostname}\n" +

                    $"📍 节点: {res.node_location}\n" +

                    $"📶 流量: {usedGb:F2}GB / {limitGb:F2}GB\n" +

                    $"📅 重置: {resetDate:yyyy-MM-dd HH:mm:ss}",

                    cancellationToken: ct);

            }

        }

        else if (text == "/reboot")

        {

            var url = $"{API_HOST}/v1/restart?veid={VEID}&api_key={API_KEY}";

            var res = await _http.GetFromJsonAsync<BwhInfo>(url, ct);

            if(res.error == 0) {

                 await bot.SendTextMessageAsync(chatId, "🔄 硬件重启指令已发送,请稍候...", cancellationToken: ct);

            }

        }

    }

    private Task HandleErrorAsync(ITelegramBotClient bot, Exception ex, CancellationToken ct) => Task.CompletedTask;

}

// 对应官方文档的 JSON 映射类

public class BwhInfo {

    public int error { get; set; }

    public string hostname { get; set; }

    public string node_location { get; set; }

    public long data_counter { get; set; }

    public long plan_monthly_data { get; set; }

    public double monthly_data_multiplier { get; set; }

    public long data_next_reset { get; set; }

}

```

## 02. Docker 容器化:告别环境污染

我们不需要在宿主机上装任何 .NET SDK 或运行时,直接把程序打包进 Docker。这样不仅系统干净,而且自带进程守护(挂了自动拉起),省去了配置 Systemd 的麻烦。

在代码根目录下新建这两个文件:

**1. `Dockerfile`**(极简运行时镜像):

```dockerfile

# 使用微软官方轻量级 ASP.NET 8 运行时镜像

FROM mcr.microsoft.com/dotnet/aspnet:8.0

WORKDIR /app

# 拷贝编译好的文件到容器内

COPY . .

# 启动程序 (替换为你的 DLL 名字)

ENTRYPOINT ["dotnet", "MyBwhBot.dll"]

```

**2. `docker-compose.yml`**

```yaml

services:

  bwh-bot:

    build: .

    container_name: bwh-telegram-bot

    restart: always # 容器挂了自动重启

    environment:

      - TZ=Asia/Shanghai

```

## 03. GitHub Actions 全自动化 CI/CD

服务和容器配置都准备好了,接下来的痛点是怎么自动化部署。这里我们直接用 GitHub Actions 搞定。

只要你往 `main` 分支推代码,GitHub 就会自动走完这个流程:装 .NET SDK -> 编译代码 -> 打包连同 Docker 文件一起 SCP 传到搬瓦工 -> 最后通过 SSH 触发 `docker compose up` 构建并重启。

在代码仓库新建 `.github/workflows/deploy.yml` 文件,可以直接抄:

```yaml

name: Docker Deploy to BWH

on:

  push:

    branches: [ "main" ]

jobs:

  build-and-deploy:

    runs-on: ubuntu-latest

    steps:

    - uses: actions/checkout@v4

   

    - name: 配置 .NET 8 编译环境

      uses: actions/setup-dotnet@v4

      with:

        dotnet-version: '8.0.x'

    - name: 编译发布

      run: dotnet publish -c Release -o ./publish_out

    - name: 准备 Docker 文件

      run: |

        cp Dockerfile ./publish_out/

        cp docker-compose.yml ./publish_out/

    - name: SCP 传输文件到服务器

      uses: appleboy/scp-action@v0.1.7

      with:

        host: ${{ secrets.BWH_IP }}          # 你的 VPS IP

        username: root

        key: ${{ secrets.SSH_PRIVATE_KEY }}  # 私钥 (在 GitHub 后台配置)

        source: "./publish_out/*"

        target: "/opt/bwh-bot"

        strip_components: 1

    - name: SSH 触发 Docker 重新构建

      uses: appleboy/ssh-action@v1.0.3

      with:

        host: ${{ secrets.BWH_IP }}

        username: root

        key: ${{ secrets.SSH_PRIVATE_KEY }}

        script: |

          cd /opt/bwh-bot

          docker compose down

          docker compose up -d --build

          docker image prune -f  # 清理旧的无用镜像

```

> **提示**:记得在 GitHub 仓库的 `Settings -> Secrets and variables -> Actions` 里把 `BWH_IP` 和 `SSH_PRIVATE_KEY` 填好。

## 总结

配置好之后,以后每次在本地更新完代码只要 push 一下,云端的服务就会自动完成重建和替换。

这套流程其实对 VPS 的网络连通性有一定要求。如果服务器网络比较差,GitHub Action 在 SCP 传文件或者 SSH 连接时偶尔会超时报错。平时部署后端服务,建议尽量选网络稳一点的机房(类似 搬瓦工 CN2 GIA 高端线这种),文件传输基本秒达,CI/CD 流水线跑起来会顺畅很多。