切換菜單
切換偏好設定選單
切換個人選單
尚未登入
若您做出任何編輯,會公開您的 IP 位址。

實戰筆記:搬瓦工 Docker 部署 .NET 8 與 BWH API 自動監控流水線

出自md5.pw
這是此頁面最近一次修訂;沒有已批准修訂。

最近社區裏有不少帖子在討論各種面板、測速,或者教大家寫 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 流水線跑起來會順暢很多。