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

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

来自md5.pw
Liam留言 | 贡献2026年4月15日 (三) 07:01的版本
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)

最近社区里有不少帖子在讨论各种面板、测速,或者教大家写 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,顺手把这两个容易踩坑的地方处理掉:

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(极简运行时镜像):

# 使用微软官方轻量级 ASP.NET 8 运行时镜像
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app

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

# 启动程序 (替换为你的 DLL 名字)
ENTRYPOINT ["dotnet", "MyBwhBot.dll"]

2. docker-compose.yml

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 文件:

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_IPSSH_PRIVATE_KEY 填好。

总结

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

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