[{"data":1,"prerenderedAt":1766},["ShallowReactive",2],{"blog-/blog/bwh-docker-dotnet8-cicd-api-monitor":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"author":10,"date":11,"tags":12,"body":15,"_type":1760,"_id":1761,"_source":1762,"_file":1763,"_stem":1764,"_extension":1765},"/blog/bwh-docker-dotnet8-cicd-api-monitor","blog",false,"","实战笔记：搬瓦工 Docker 部署 .NET 8 与 BWH API 自动监控流水线","记录一次完整的部署实践：用 Docker Compose 在搬瓦工上跑 .NET 8 Worker，通过 GitHub Actions 实现自动化发布，并结合 KiwiVM API 做 Telegram 流量监控与重启控制。","VPS Hunter","2026-04-15",[13,14],"搬瓦工","VPS实战",{"type":16,"children":17,"toc":1754},"root",[18,26,31,51,58,85,98,755,761,766,771,787,852,868,989,995,1000,1021,1034,1686,1724,1729,1734,1748],{"type":19,"tag":20,"props":21,"children":22},"element","p",{},[23],{"type":24,"value":25},"text","最近社区里有不少帖子在讨论各种面板、测速，或者教大家写 Bash 脚本查 VPS 流量。",{"type":19,"tag":20,"props":27,"children":28},{},[29],{"type":24,"value":30},"其实对于开发者来说，买一台网络稳的机器（比如搬瓦工的 GIA），最大的用途还是跑自己的后端服务。但这年头，如果每次改完代码还要手动 FTP 传文件、在宿主机折腾各种运行时环境，不仅容易把系统弄脏，维护起来也挺繁琐。",{"type":19,"tag":20,"props":32,"children":33},{},[34,36,42,44,49],{"type":24,"value":35},"今天分享一套我日常在用的 DevOps 工作流：在 Linux 下用 ",{"type":19,"tag":37,"props":38,"children":39},"strong",{},[40],{"type":24,"value":41},"Docker Compose",{"type":24,"value":43}," 隔离运行 .NET 8 服务，搭配 ",{"type":19,"tag":37,"props":45,"children":46},{},[47],{"type":24,"value":48},"GitHub Actions",{"type":24,"value":50}," 做全自动 CI/CD。顺便用 C# 对接一下搬瓦工官方的 KiwiVM API，写个 Telegram 监控端。废话不多说，直接开始。",{"type":19,"tag":52,"props":53,"children":55},"h2",{"id":54},"_01-代码实现c-调用-kiwivm-api-与-tg-机器人",[56],{"type":24,"value":57},"01. 代码实现：C# 调用 KiwiVM API 与 TG 机器人",{"type":19,"tag":20,"props":59,"children":60},{},[61,63,70,72],{"type":24,"value":62},"相比传统的 Bash 脚本定时任务，用 C# 的 ",{"type":19,"tag":64,"props":65,"children":67},"code",{"className":66},[],[68],{"type":24,"value":69},"BackgroundService",{"type":24,"value":71}," 做长连接监控更稳定。另外，很多人用脚本查 API 经常会算错流量，主要是没留意官方文档里的两个细节：",{"type":19,"tag":37,"props":73,"children":74},{},[75,77,83],{"type":24,"value":76},"搬瓦工的高端机房流量是有 ",{"type":19,"tag":64,"props":78,"children":80},{"className":79},[],[81],{"type":24,"value":82},"monthly_data_multiplier",{"type":24,"value":84},"（流量乘数）的，而且 API 返回的日期是 UNIX 时间戳。",{"type":19,"tag":20,"props":86,"children":87},{},[88,90,96],{"type":24,"value":89},"我们在 .NET 8 Worker 项目中引入 ",{"type":19,"tag":64,"props":91,"children":93},{"className":92},[],[94],{"type":24,"value":95},"Telegram.Bot",{"type":24,"value":97},"，顺手把这两个容易踩坑的地方处理掉：",{"type":19,"tag":99,"props":100,"children":104},"pre",{"className":101,"code":102,"language":103,"meta":7,"style":7},"language-csharp shiki shiki-themes github-dark","using Telegram.Bot;\nusing Telegram.Bot.Types;\nusing System.Net.Http.Json;\n\npublic class BwhMonitorWorker : BackgroundService\n{\n    private readonly TelegramBotClient _botClient = new(\"你的_TG_BOT_TOKEN\");\n    private readonly HttpClient _http = new();\n    \n    private const string API_HOST = \"https://\" + \"api.64clouds.com\";\n    private const string VEID = \"你的VEID\";\n    private const string API_KEY = \"你的API_KEY\";\n    private const long ADMIN_CHAT_ID = 123456789; // 你的 TG 账号 ID，防止别人蹭用\n\n    protected override async Task ExecuteAsync(CancellationToken stoppingToken)\n    {\n        _botClient.StartReceiving(HandleUpdateAsync, HandleErrorAsync, null, stoppingToken);\n        while (!stoppingToken.IsCancellationRequested) await Task.Delay(1000, stoppingToken);\n    }\n\n    private async Task HandleUpdateAsync(ITelegramBotClient bot, Update update, CancellationToken ct)\n    {\n        if (update.Message?.Text == null || update.Message.Chat.Id != ADMIN_CHAT_ID) return;\n        \n        var text = update.Message.Text;\n        var chatId = update.Message.Chat.Id;\n\n        if (text == \"/status\") \n        {\n            var url = $\"{API_HOST}/v1/getServiceInfo?veid={VEID}&api_key={API_KEY}\";\n            var res = await _http.GetFromJsonAsync\u003CBwhInfo>(url, ct);\n            \n            if (res.error == 0)\n            {\n                // 细节 1：计算时必须带上官方规定的流量乘数 (monthly_data_multiplier)\n                double multiplier = res.monthly_data_multiplier > 0 ? res.monthly_data_multiplier : 1;\n                double usedGb = (res.data_counter * multiplier) / 1024.0 / 1024.0 / 1024.0;\n                double limitGb = (res.plan_monthly_data * multiplier) / 1024.0 / 1024.0 / 1024.0;\n                \n                // 细节 2：UNIX 时间戳转换为本地直观时间\n                DateTime resetDate = DateTimeOffset.FromUnixTimeSeconds(res.data_next_reset).LocalDateTime;\n                \n                await bot.SendTextMessageAsync(chatId, \n                    $\"🖥️ 主机名: {res.hostname}\\n\" +\n                    $\"📍 节点: {res.node_location}\\n\" +\n                    $\"📶 流量: {usedGb:F2}GB / {limitGb:F2}GB\\n\" +\n                    $\"📅 重置: {resetDate:yyyy-MM-dd HH:mm:ss}\", \n                    cancellationToken: ct);\n            }\n        }\n        else if (text == \"/reboot\") \n        {\n            var url = $\"{API_HOST}/v1/restart?veid={VEID}&api_key={API_KEY}\";\n            var res = await _http.GetFromJsonAsync\u003CBwhInfo>(url, ct);\n            if(res.error == 0) {\n                 await bot.SendTextMessageAsync(chatId, \"🔄 硬件重启指令已发送，请稍候...\", cancellationToken: ct);\n            }\n        }\n    }\n\n    private Task HandleErrorAsync(ITelegramBotClient bot, Exception ex, CancellationToken ct) => Task.CompletedTask;\n}\n\n// 对应官方文档的 JSON 映射类\npublic class BwhInfo {\n    public int error { get; set; }\n    public string hostname { get; set; }\n    public string node_location { get; set; }\n    public long data_counter { get; set; }\n    public long plan_monthly_data { get; set; }\n    public double monthly_data_multiplier { get; set; }\n    public long data_next_reset { get; set; }\n}\n","csharp",[105],{"type":19,"tag":64,"props":106,"children":107},{"__ignoreMap":7},[108,119,128,137,147,156,165,174,183,192,201,210,219,228,236,245,254,263,272,281,289,298,306,315,324,333,342,350,359,368,377,386,395,404,413,422,431,440,449,458,467,476,484,493,502,511,520,529,538,547,556,565,573,582,590,599,608,616,624,632,640,649,658,666,675,684,693,702,711,720,729,738,747],{"type":19,"tag":109,"props":110,"children":113},"span",{"class":111,"line":112},"line",1,[114],{"type":19,"tag":109,"props":115,"children":116},{},[117],{"type":24,"value":118},"using Telegram.Bot;\n",{"type":19,"tag":109,"props":120,"children":122},{"class":111,"line":121},2,[123],{"type":19,"tag":109,"props":124,"children":125},{},[126],{"type":24,"value":127},"using Telegram.Bot.Types;\n",{"type":19,"tag":109,"props":129,"children":131},{"class":111,"line":130},3,[132],{"type":19,"tag":109,"props":133,"children":134},{},[135],{"type":24,"value":136},"using System.Net.Http.Json;\n",{"type":19,"tag":109,"props":138,"children":140},{"class":111,"line":139},4,[141],{"type":19,"tag":109,"props":142,"children":144},{"emptyLinePlaceholder":143},true,[145],{"type":24,"value":146},"\n",{"type":19,"tag":109,"props":148,"children":150},{"class":111,"line":149},5,[151],{"type":19,"tag":109,"props":152,"children":153},{},[154],{"type":24,"value":155},"public class BwhMonitorWorker : BackgroundService\n",{"type":19,"tag":109,"props":157,"children":159},{"class":111,"line":158},6,[160],{"type":19,"tag":109,"props":161,"children":162},{},[163],{"type":24,"value":164},"{\n",{"type":19,"tag":109,"props":166,"children":168},{"class":111,"line":167},7,[169],{"type":19,"tag":109,"props":170,"children":171},{},[172],{"type":24,"value":173},"    private readonly TelegramBotClient _botClient = new(\"你的_TG_BOT_TOKEN\");\n",{"type":19,"tag":109,"props":175,"children":177},{"class":111,"line":176},8,[178],{"type":19,"tag":109,"props":179,"children":180},{},[181],{"type":24,"value":182},"    private readonly HttpClient _http = new();\n",{"type":19,"tag":109,"props":184,"children":186},{"class":111,"line":185},9,[187],{"type":19,"tag":109,"props":188,"children":189},{},[190],{"type":24,"value":191},"    \n",{"type":19,"tag":109,"props":193,"children":195},{"class":111,"line":194},10,[196],{"type":19,"tag":109,"props":197,"children":198},{},[199],{"type":24,"value":200},"    private const string API_HOST = \"https://\" + \"api.64clouds.com\";\n",{"type":19,"tag":109,"props":202,"children":204},{"class":111,"line":203},11,[205],{"type":19,"tag":109,"props":206,"children":207},{},[208],{"type":24,"value":209},"    private const string VEID = \"你的VEID\";\n",{"type":19,"tag":109,"props":211,"children":213},{"class":111,"line":212},12,[214],{"type":19,"tag":109,"props":215,"children":216},{},[217],{"type":24,"value":218},"    private const string API_KEY = \"你的API_KEY\";\n",{"type":19,"tag":109,"props":220,"children":222},{"class":111,"line":221},13,[223],{"type":19,"tag":109,"props":224,"children":225},{},[226],{"type":24,"value":227},"    private const long ADMIN_CHAT_ID = 123456789; // 你的 TG 账号 ID，防止别人蹭用\n",{"type":19,"tag":109,"props":229,"children":231},{"class":111,"line":230},14,[232],{"type":19,"tag":109,"props":233,"children":234},{"emptyLinePlaceholder":143},[235],{"type":24,"value":146},{"type":19,"tag":109,"props":237,"children":239},{"class":111,"line":238},15,[240],{"type":19,"tag":109,"props":241,"children":242},{},[243],{"type":24,"value":244},"    protected override async Task ExecuteAsync(CancellationToken stoppingToken)\n",{"type":19,"tag":109,"props":246,"children":248},{"class":111,"line":247},16,[249],{"type":19,"tag":109,"props":250,"children":251},{},[252],{"type":24,"value":253},"    {\n",{"type":19,"tag":109,"props":255,"children":257},{"class":111,"line":256},17,[258],{"type":19,"tag":109,"props":259,"children":260},{},[261],{"type":24,"value":262},"        _botClient.StartReceiving(HandleUpdateAsync, HandleErrorAsync, null, stoppingToken);\n",{"type":19,"tag":109,"props":264,"children":266},{"class":111,"line":265},18,[267],{"type":19,"tag":109,"props":268,"children":269},{},[270],{"type":24,"value":271},"        while (!stoppingToken.IsCancellationRequested) await Task.Delay(1000, stoppingToken);\n",{"type":19,"tag":109,"props":273,"children":275},{"class":111,"line":274},19,[276],{"type":19,"tag":109,"props":277,"children":278},{},[279],{"type":24,"value":280},"    }\n",{"type":19,"tag":109,"props":282,"children":284},{"class":111,"line":283},20,[285],{"type":19,"tag":109,"props":286,"children":287},{"emptyLinePlaceholder":143},[288],{"type":24,"value":146},{"type":19,"tag":109,"props":290,"children":292},{"class":111,"line":291},21,[293],{"type":19,"tag":109,"props":294,"children":295},{},[296],{"type":24,"value":297},"    private async Task HandleUpdateAsync(ITelegramBotClient bot, Update update, CancellationToken ct)\n",{"type":19,"tag":109,"props":299,"children":301},{"class":111,"line":300},22,[302],{"type":19,"tag":109,"props":303,"children":304},{},[305],{"type":24,"value":253},{"type":19,"tag":109,"props":307,"children":309},{"class":111,"line":308},23,[310],{"type":19,"tag":109,"props":311,"children":312},{},[313],{"type":24,"value":314},"        if (update.Message?.Text == null || update.Message.Chat.Id != ADMIN_CHAT_ID) return;\n",{"type":19,"tag":109,"props":316,"children":318},{"class":111,"line":317},24,[319],{"type":19,"tag":109,"props":320,"children":321},{},[322],{"type":24,"value":323},"        \n",{"type":19,"tag":109,"props":325,"children":327},{"class":111,"line":326},25,[328],{"type":19,"tag":109,"props":329,"children":330},{},[331],{"type":24,"value":332},"        var text = update.Message.Text;\n",{"type":19,"tag":109,"props":334,"children":336},{"class":111,"line":335},26,[337],{"type":19,"tag":109,"props":338,"children":339},{},[340],{"type":24,"value":341},"        var chatId = update.Message.Chat.Id;\n",{"type":19,"tag":109,"props":343,"children":345},{"class":111,"line":344},27,[346],{"type":19,"tag":109,"props":347,"children":348},{"emptyLinePlaceholder":143},[349],{"type":24,"value":146},{"type":19,"tag":109,"props":351,"children":353},{"class":111,"line":352},28,[354],{"type":19,"tag":109,"props":355,"children":356},{},[357],{"type":24,"value":358},"        if (text == \"/status\") \n",{"type":19,"tag":109,"props":360,"children":362},{"class":111,"line":361},29,[363],{"type":19,"tag":109,"props":364,"children":365},{},[366],{"type":24,"value":367},"        {\n",{"type":19,"tag":109,"props":369,"children":371},{"class":111,"line":370},30,[372],{"type":19,"tag":109,"props":373,"children":374},{},[375],{"type":24,"value":376},"            var url = $\"{API_HOST}/v1/getServiceInfo?veid={VEID}&api_key={API_KEY}\";\n",{"type":19,"tag":109,"props":378,"children":380},{"class":111,"line":379},31,[381],{"type":19,"tag":109,"props":382,"children":383},{},[384],{"type":24,"value":385},"            var res = await _http.GetFromJsonAsync\u003CBwhInfo>(url, ct);\n",{"type":19,"tag":109,"props":387,"children":389},{"class":111,"line":388},32,[390],{"type":19,"tag":109,"props":391,"children":392},{},[393],{"type":24,"value":394},"            \n",{"type":19,"tag":109,"props":396,"children":398},{"class":111,"line":397},33,[399],{"type":19,"tag":109,"props":400,"children":401},{},[402],{"type":24,"value":403},"            if (res.error == 0)\n",{"type":19,"tag":109,"props":405,"children":407},{"class":111,"line":406},34,[408],{"type":19,"tag":109,"props":409,"children":410},{},[411],{"type":24,"value":412},"            {\n",{"type":19,"tag":109,"props":414,"children":416},{"class":111,"line":415},35,[417],{"type":19,"tag":109,"props":418,"children":419},{},[420],{"type":24,"value":421},"                // 细节 1：计算时必须带上官方规定的流量乘数 (monthly_data_multiplier)\n",{"type":19,"tag":109,"props":423,"children":425},{"class":111,"line":424},36,[426],{"type":19,"tag":109,"props":427,"children":428},{},[429],{"type":24,"value":430},"                double multiplier = res.monthly_data_multiplier > 0 ? res.monthly_data_multiplier : 1;\n",{"type":19,"tag":109,"props":432,"children":434},{"class":111,"line":433},37,[435],{"type":19,"tag":109,"props":436,"children":437},{},[438],{"type":24,"value":439},"                double usedGb = (res.data_counter * multiplier) / 1024.0 / 1024.0 / 1024.0;\n",{"type":19,"tag":109,"props":441,"children":443},{"class":111,"line":442},38,[444],{"type":19,"tag":109,"props":445,"children":446},{},[447],{"type":24,"value":448},"                double limitGb = (res.plan_monthly_data * multiplier) / 1024.0 / 1024.0 / 1024.0;\n",{"type":19,"tag":109,"props":450,"children":452},{"class":111,"line":451},39,[453],{"type":19,"tag":109,"props":454,"children":455},{},[456],{"type":24,"value":457},"                \n",{"type":19,"tag":109,"props":459,"children":461},{"class":111,"line":460},40,[462],{"type":19,"tag":109,"props":463,"children":464},{},[465],{"type":24,"value":466},"                // 细节 2：UNIX 时间戳转换为本地直观时间\n",{"type":19,"tag":109,"props":468,"children":470},{"class":111,"line":469},41,[471],{"type":19,"tag":109,"props":472,"children":473},{},[474],{"type":24,"value":475},"                DateTime resetDate = DateTimeOffset.FromUnixTimeSeconds(res.data_next_reset).LocalDateTime;\n",{"type":19,"tag":109,"props":477,"children":479},{"class":111,"line":478},42,[480],{"type":19,"tag":109,"props":481,"children":482},{},[483],{"type":24,"value":457},{"type":19,"tag":109,"props":485,"children":487},{"class":111,"line":486},43,[488],{"type":19,"tag":109,"props":489,"children":490},{},[491],{"type":24,"value":492},"                await bot.SendTextMessageAsync(chatId, \n",{"type":19,"tag":109,"props":494,"children":496},{"class":111,"line":495},44,[497],{"type":19,"tag":109,"props":498,"children":499},{},[500],{"type":24,"value":501},"                    $\"🖥️ 主机名: {res.hostname}\\n\" +\n",{"type":19,"tag":109,"props":503,"children":505},{"class":111,"line":504},45,[506],{"type":19,"tag":109,"props":507,"children":508},{},[509],{"type":24,"value":510},"                    $\"📍 节点: {res.node_location}\\n\" +\n",{"type":19,"tag":109,"props":512,"children":514},{"class":111,"line":513},46,[515],{"type":19,"tag":109,"props":516,"children":517},{},[518],{"type":24,"value":519},"                    $\"📶 流量: {usedGb:F2}GB / {limitGb:F2}GB\\n\" +\n",{"type":19,"tag":109,"props":521,"children":523},{"class":111,"line":522},47,[524],{"type":19,"tag":109,"props":525,"children":526},{},[527],{"type":24,"value":528},"                    $\"📅 重置: {resetDate:yyyy-MM-dd HH:mm:ss}\", \n",{"type":19,"tag":109,"props":530,"children":532},{"class":111,"line":531},48,[533],{"type":19,"tag":109,"props":534,"children":535},{},[536],{"type":24,"value":537},"                    cancellationToken: ct);\n",{"type":19,"tag":109,"props":539,"children":541},{"class":111,"line":540},49,[542],{"type":19,"tag":109,"props":543,"children":544},{},[545],{"type":24,"value":546},"            }\n",{"type":19,"tag":109,"props":548,"children":550},{"class":111,"line":549},50,[551],{"type":19,"tag":109,"props":552,"children":553},{},[554],{"type":24,"value":555},"        }\n",{"type":19,"tag":109,"props":557,"children":559},{"class":111,"line":558},51,[560],{"type":19,"tag":109,"props":561,"children":562},{},[563],{"type":24,"value":564},"        else if (text == \"/reboot\") \n",{"type":19,"tag":109,"props":566,"children":568},{"class":111,"line":567},52,[569],{"type":19,"tag":109,"props":570,"children":571},{},[572],{"type":24,"value":367},{"type":19,"tag":109,"props":574,"children":576},{"class":111,"line":575},53,[577],{"type":19,"tag":109,"props":578,"children":579},{},[580],{"type":24,"value":581},"            var url = $\"{API_HOST}/v1/restart?veid={VEID}&api_key={API_KEY}\";\n",{"type":19,"tag":109,"props":583,"children":585},{"class":111,"line":584},54,[586],{"type":19,"tag":109,"props":587,"children":588},{},[589],{"type":24,"value":385},{"type":19,"tag":109,"props":591,"children":593},{"class":111,"line":592},55,[594],{"type":19,"tag":109,"props":595,"children":596},{},[597],{"type":24,"value":598},"            if(res.error == 0) {\n",{"type":19,"tag":109,"props":600,"children":602},{"class":111,"line":601},56,[603],{"type":19,"tag":109,"props":604,"children":605},{},[606],{"type":24,"value":607},"                 await bot.SendTextMessageAsync(chatId, \"🔄 硬件重启指令已发送，请稍候...\", cancellationToken: ct);\n",{"type":19,"tag":109,"props":609,"children":611},{"class":111,"line":610},57,[612],{"type":19,"tag":109,"props":613,"children":614},{},[615],{"type":24,"value":546},{"type":19,"tag":109,"props":617,"children":619},{"class":111,"line":618},58,[620],{"type":19,"tag":109,"props":621,"children":622},{},[623],{"type":24,"value":555},{"type":19,"tag":109,"props":625,"children":627},{"class":111,"line":626},59,[628],{"type":19,"tag":109,"props":629,"children":630},{},[631],{"type":24,"value":280},{"type":19,"tag":109,"props":633,"children":635},{"class":111,"line":634},60,[636],{"type":19,"tag":109,"props":637,"children":638},{"emptyLinePlaceholder":143},[639],{"type":24,"value":146},{"type":19,"tag":109,"props":641,"children":643},{"class":111,"line":642},61,[644],{"type":19,"tag":109,"props":645,"children":646},{},[647],{"type":24,"value":648},"    private Task HandleErrorAsync(ITelegramBotClient bot, Exception ex, CancellationToken ct) => Task.CompletedTask;\n",{"type":19,"tag":109,"props":650,"children":652},{"class":111,"line":651},62,[653],{"type":19,"tag":109,"props":654,"children":655},{},[656],{"type":24,"value":657},"}\n",{"type":19,"tag":109,"props":659,"children":661},{"class":111,"line":660},63,[662],{"type":19,"tag":109,"props":663,"children":664},{"emptyLinePlaceholder":143},[665],{"type":24,"value":146},{"type":19,"tag":109,"props":667,"children":669},{"class":111,"line":668},64,[670],{"type":19,"tag":109,"props":671,"children":672},{},[673],{"type":24,"value":674},"// 对应官方文档的 JSON 映射类\n",{"type":19,"tag":109,"props":676,"children":678},{"class":111,"line":677},65,[679],{"type":19,"tag":109,"props":680,"children":681},{},[682],{"type":24,"value":683},"public class BwhInfo {\n",{"type":19,"tag":109,"props":685,"children":687},{"class":111,"line":686},66,[688],{"type":19,"tag":109,"props":689,"children":690},{},[691],{"type":24,"value":692},"    public int error { get; set; }\n",{"type":19,"tag":109,"props":694,"children":696},{"class":111,"line":695},67,[697],{"type":19,"tag":109,"props":698,"children":699},{},[700],{"type":24,"value":701},"    public string hostname { get; set; }\n",{"type":19,"tag":109,"props":703,"children":705},{"class":111,"line":704},68,[706],{"type":19,"tag":109,"props":707,"children":708},{},[709],{"type":24,"value":710},"    public string node_location { get; set; }\n",{"type":19,"tag":109,"props":712,"children":714},{"class":111,"line":713},69,[715],{"type":19,"tag":109,"props":716,"children":717},{},[718],{"type":24,"value":719},"    public long data_counter { get; set; }\n",{"type":19,"tag":109,"props":721,"children":723},{"class":111,"line":722},70,[724],{"type":19,"tag":109,"props":725,"children":726},{},[727],{"type":24,"value":728},"    public long plan_monthly_data { get; set; }\n",{"type":19,"tag":109,"props":730,"children":732},{"class":111,"line":731},71,[733],{"type":19,"tag":109,"props":734,"children":735},{},[736],{"type":24,"value":737},"    public double monthly_data_multiplier { get; set; }\n",{"type":19,"tag":109,"props":739,"children":741},{"class":111,"line":740},72,[742],{"type":19,"tag":109,"props":743,"children":744},{},[745],{"type":24,"value":746},"    public long data_next_reset { get; set; }\n",{"type":19,"tag":109,"props":748,"children":750},{"class":111,"line":749},73,[751],{"type":19,"tag":109,"props":752,"children":753},{},[754],{"type":24,"value":657},{"type":19,"tag":52,"props":756,"children":758},{"id":757},"_02-docker-容器化告别环境污染",[759],{"type":24,"value":760},"02. Docker 容器化：告别环境污染",{"type":19,"tag":20,"props":762,"children":763},{},[764],{"type":24,"value":765},"我们不需要在宿主机上装任何 .NET SDK 或运行时，直接把程序打包进 Docker。这样不仅系统干净，而且自带进程守护（挂了自动拉起），省去了配置 Systemd 的麻烦。",{"type":19,"tag":20,"props":767,"children":768},{},[769],{"type":24,"value":770},"在代码根目录下新建这两个文件：",{"type":19,"tag":20,"props":772,"children":773},{},[774,785],{"type":19,"tag":37,"props":775,"children":776},{},[777,779],{"type":24,"value":778},"1. ",{"type":19,"tag":64,"props":780,"children":782},{"className":781},[],[783],{"type":24,"value":784},"Dockerfile",{"type":24,"value":786},"（极简运行时镜像）：",{"type":19,"tag":99,"props":788,"children":792},{"className":789,"code":790,"language":791,"meta":7,"style":7},"language-dockerfile shiki shiki-themes github-dark","# 使用微软官方轻量级 ASP.NET 8 运行时镜像\nFROM mcr.microsoft.com/dotnet/aspnet:8.0\nWORKDIR /app\n# 拷贝编译好的文件到容器内\nCOPY . .\n# 启动程序 (替换为你的 DLL 名字)\nENTRYPOINT [\"dotnet\", \"MyBwhBot.dll\"]\n","dockerfile",[793],{"type":19,"tag":64,"props":794,"children":795},{"__ignoreMap":7},[796,804,812,820,828,836,844],{"type":19,"tag":109,"props":797,"children":798},{"class":111,"line":112},[799],{"type":19,"tag":109,"props":800,"children":801},{},[802],{"type":24,"value":803},"# 使用微软官方轻量级 ASP.NET 8 运行时镜像\n",{"type":19,"tag":109,"props":805,"children":806},{"class":111,"line":121},[807],{"type":19,"tag":109,"props":808,"children":809},{},[810],{"type":24,"value":811},"FROM mcr.microsoft.com/dotnet/aspnet:8.0\n",{"type":19,"tag":109,"props":813,"children":814},{"class":111,"line":130},[815],{"type":19,"tag":109,"props":816,"children":817},{},[818],{"type":24,"value":819},"WORKDIR /app\n",{"type":19,"tag":109,"props":821,"children":822},{"class":111,"line":139},[823],{"type":19,"tag":109,"props":824,"children":825},{},[826],{"type":24,"value":827},"# 拷贝编译好的文件到容器内\n",{"type":19,"tag":109,"props":829,"children":830},{"class":111,"line":149},[831],{"type":19,"tag":109,"props":832,"children":833},{},[834],{"type":24,"value":835},"COPY . .\n",{"type":19,"tag":109,"props":837,"children":838},{"class":111,"line":158},[839],{"type":19,"tag":109,"props":840,"children":841},{},[842],{"type":24,"value":843},"# 启动程序 (替换为你的 DLL 名字)\n",{"type":19,"tag":109,"props":845,"children":846},{"class":111,"line":167},[847],{"type":19,"tag":109,"props":848,"children":849},{},[850],{"type":24,"value":851},"ENTRYPOINT [\"dotnet\", \"MyBwhBot.dll\"]\n",{"type":19,"tag":20,"props":853,"children":854},{},[855,866],{"type":19,"tag":37,"props":856,"children":857},{},[858,860],{"type":24,"value":859},"2. ",{"type":19,"tag":64,"props":861,"children":863},{"className":862},[],[864],{"type":24,"value":865},"docker-compose.yml",{"type":24,"value":867},"：",{"type":19,"tag":99,"props":869,"children":873},{"className":870,"code":871,"language":872,"meta":7,"style":7},"language-yaml shiki shiki-themes github-dark","services:\n  bwh-bot:\n    build: .\n    container_name: bwh-telegram-bot\n    restart: always # 容器挂了自动重启\n    environment:\n      - TZ=Asia/Shanghai\n","yaml",[874],{"type":19,"tag":64,"props":875,"children":876},{"__ignoreMap":7},[877,892,904,923,941,964,976],{"type":19,"tag":109,"props":878,"children":879},{"class":111,"line":112},[880,886],{"type":19,"tag":109,"props":881,"children":883},{"style":882},"--shiki-default:#85E89D",[884],{"type":24,"value":885},"services",{"type":19,"tag":109,"props":887,"children":889},{"style":888},"--shiki-default:#E1E4E8",[890],{"type":24,"value":891},":\n",{"type":19,"tag":109,"props":893,"children":894},{"class":111,"line":121},[895,900],{"type":19,"tag":109,"props":896,"children":897},{"style":882},[898],{"type":24,"value":899},"  bwh-bot",{"type":19,"tag":109,"props":901,"children":902},{"style":888},[903],{"type":24,"value":891},{"type":19,"tag":109,"props":905,"children":906},{"class":111,"line":130},[907,912,917],{"type":19,"tag":109,"props":908,"children":909},{"style":882},[910],{"type":24,"value":911},"    build",{"type":19,"tag":109,"props":913,"children":914},{"style":888},[915],{"type":24,"value":916},": ",{"type":19,"tag":109,"props":918,"children":920},{"style":919},"--shiki-default:#79B8FF",[921],{"type":24,"value":922},".\n",{"type":19,"tag":109,"props":924,"children":925},{"class":111,"line":139},[926,931,935],{"type":19,"tag":109,"props":927,"children":928},{"style":882},[929],{"type":24,"value":930},"    container_name",{"type":19,"tag":109,"props":932,"children":933},{"style":888},[934],{"type":24,"value":916},{"type":19,"tag":109,"props":936,"children":938},{"style":937},"--shiki-default:#9ECBFF",[939],{"type":24,"value":940},"bwh-telegram-bot\n",{"type":19,"tag":109,"props":942,"children":943},{"class":111,"line":149},[944,949,953,958],{"type":19,"tag":109,"props":945,"children":946},{"style":882},[947],{"type":24,"value":948},"    restart",{"type":19,"tag":109,"props":950,"children":951},{"style":888},[952],{"type":24,"value":916},{"type":19,"tag":109,"props":954,"children":955},{"style":937},[956],{"type":24,"value":957},"always",{"type":19,"tag":109,"props":959,"children":961},{"style":960},"--shiki-default:#6A737D",[962],{"type":24,"value":963}," # 容器挂了自动重启\n",{"type":19,"tag":109,"props":965,"children":966},{"class":111,"line":158},[967,972],{"type":19,"tag":109,"props":968,"children":969},{"style":882},[970],{"type":24,"value":971},"    environment",{"type":19,"tag":109,"props":973,"children":974},{"style":888},[975],{"type":24,"value":891},{"type":19,"tag":109,"props":977,"children":978},{"class":111,"line":167},[979,984],{"type":19,"tag":109,"props":980,"children":981},{"style":888},[982],{"type":24,"value":983},"      - ",{"type":19,"tag":109,"props":985,"children":986},{"style":937},[987],{"type":24,"value":988},"TZ=Asia/Shanghai\n",{"type":19,"tag":52,"props":990,"children":992},{"id":991},"_03-github-actions-全自动化-cicd",[993],{"type":24,"value":994},"03. GitHub Actions 全自动化 CI/CD",{"type":19,"tag":20,"props":996,"children":997},{},[998],{"type":24,"value":999},"服务和容器配置都准备好了，接下来的痛点是怎么自动化部署。这里我们直接用 GitHub Actions 搞定。",{"type":19,"tag":20,"props":1001,"children":1002},{},[1003,1005,1011,1013,1019],{"type":24,"value":1004},"只要你往 ",{"type":19,"tag":64,"props":1006,"children":1008},{"className":1007},[],[1009],{"type":24,"value":1010},"main",{"type":24,"value":1012}," 分支推代码，GitHub 就会自动走完这个流程：装 .NET SDK -> 编译代码 -> 打包连同 Docker 文件一起 SCP 传到搬瓦工 -> 最后通过 SSH 触发 ",{"type":19,"tag":64,"props":1014,"children":1016},{"className":1015},[],[1017],{"type":24,"value":1018},"docker compose up",{"type":24,"value":1020}," 构建并重启。",{"type":19,"tag":20,"props":1022,"children":1023},{},[1024,1026,1032],{"type":24,"value":1025},"在代码仓库新建 ",{"type":19,"tag":64,"props":1027,"children":1029},{"className":1028},[],[1030],{"type":24,"value":1031},".github/workflows/deploy.yml",{"type":24,"value":1033}," 文件，可以直接抄：",{"type":19,"tag":99,"props":1035,"children":1037},{"className":870,"code":1036,"language":872,"meta":7,"style":7},"name: Docker Deploy to BWH\n\non:\n  push:\n    branches: [ \"main\" ]\n\njobs:\n  build-and-deploy:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v4\n    \n    - name: 配置 .NET 8 编译环境\n      uses: actions/setup-dotnet@v4\n      with:\n        dotnet-version: '8.0.x'\n\n    - name: 编译发布\n      run: dotnet publish -c Release -o ./publish_out\n\n    - name: 准备 Docker 文件\n      run: |\n        cp Dockerfile ./publish_out/\n        cp docker-compose.yml ./publish_out/\n\n    - name: SCP 传输文件到服务器\n      uses: appleboy/scp-action@v0.1.7\n      with:\n        host: ${{ secrets.BWH_IP }}          # 你的 VPS IP\n        username: root\n        key: ${{ secrets.SSH_PRIVATE_KEY }}  # 私钥 (在 GitHub 后台配置)\n        source: \"./publish_out/*\"\n        target: \"/opt/bwh-bot\"\n        strip_components: 1\n\n    - name: SSH 触发 Docker 重新构建\n      uses: appleboy/ssh-action@v1.0.3\n      with:\n        host: ${{ secrets.BWH_IP }}\n        username: root\n        key: ${{ secrets.SSH_PRIVATE_KEY }}\n        script: |\n          cd /opt/bwh-bot\n          docker compose down\n          docker compose up -d --build\n          docker image prune -f  # 清理旧的无用镜像\n",[1038],{"type":19,"tag":64,"props":1039,"children":1040},{"__ignoreMap":7},[1041,1058,1065,1077,1089,1112,1119,1131,1143,1160,1172,1194,1201,1221,1238,1250,1267,1274,1294,1311,1318,1338,1355,1363,1371,1378,1398,1414,1425,1447,1464,1486,1503,1520,1537,1544,1564,1580,1591,1607,1622,1638,1654,1662,1670,1678],{"type":19,"tag":109,"props":1042,"children":1043},{"class":111,"line":112},[1044,1049,1053],{"type":19,"tag":109,"props":1045,"children":1046},{"style":882},[1047],{"type":24,"value":1048},"name",{"type":19,"tag":109,"props":1050,"children":1051},{"style":888},[1052],{"type":24,"value":916},{"type":19,"tag":109,"props":1054,"children":1055},{"style":937},[1056],{"type":24,"value":1057},"Docker Deploy to BWH\n",{"type":19,"tag":109,"props":1059,"children":1060},{"class":111,"line":121},[1061],{"type":19,"tag":109,"props":1062,"children":1063},{"emptyLinePlaceholder":143},[1064],{"type":24,"value":146},{"type":19,"tag":109,"props":1066,"children":1067},{"class":111,"line":130},[1068,1073],{"type":19,"tag":109,"props":1069,"children":1070},{"style":919},[1071],{"type":24,"value":1072},"on",{"type":19,"tag":109,"props":1074,"children":1075},{"style":888},[1076],{"type":24,"value":891},{"type":19,"tag":109,"props":1078,"children":1079},{"class":111,"line":139},[1080,1085],{"type":19,"tag":109,"props":1081,"children":1082},{"style":882},[1083],{"type":24,"value":1084},"  push",{"type":19,"tag":109,"props":1086,"children":1087},{"style":888},[1088],{"type":24,"value":891},{"type":19,"tag":109,"props":1090,"children":1091},{"class":111,"line":149},[1092,1097,1102,1107],{"type":19,"tag":109,"props":1093,"children":1094},{"style":882},[1095],{"type":24,"value":1096},"    branches",{"type":19,"tag":109,"props":1098,"children":1099},{"style":888},[1100],{"type":24,"value":1101},": [ ",{"type":19,"tag":109,"props":1103,"children":1104},{"style":937},[1105],{"type":24,"value":1106},"\"main\"",{"type":19,"tag":109,"props":1108,"children":1109},{"style":888},[1110],{"type":24,"value":1111}," ]\n",{"type":19,"tag":109,"props":1113,"children":1114},{"class":111,"line":158},[1115],{"type":19,"tag":109,"props":1116,"children":1117},{"emptyLinePlaceholder":143},[1118],{"type":24,"value":146},{"type":19,"tag":109,"props":1120,"children":1121},{"class":111,"line":167},[1122,1127],{"type":19,"tag":109,"props":1123,"children":1124},{"style":882},[1125],{"type":24,"value":1126},"jobs",{"type":19,"tag":109,"props":1128,"children":1129},{"style":888},[1130],{"type":24,"value":891},{"type":19,"tag":109,"props":1132,"children":1133},{"class":111,"line":176},[1134,1139],{"type":19,"tag":109,"props":1135,"children":1136},{"style":882},[1137],{"type":24,"value":1138},"  build-and-deploy",{"type":19,"tag":109,"props":1140,"children":1141},{"style":888},[1142],{"type":24,"value":891},{"type":19,"tag":109,"props":1144,"children":1145},{"class":111,"line":185},[1146,1151,1155],{"type":19,"tag":109,"props":1147,"children":1148},{"style":882},[1149],{"type":24,"value":1150},"    runs-on",{"type":19,"tag":109,"props":1152,"children":1153},{"style":888},[1154],{"type":24,"value":916},{"type":19,"tag":109,"props":1156,"children":1157},{"style":937},[1158],{"type":24,"value":1159},"ubuntu-latest\n",{"type":19,"tag":109,"props":1161,"children":1162},{"class":111,"line":194},[1163,1168],{"type":19,"tag":109,"props":1164,"children":1165},{"style":882},[1166],{"type":24,"value":1167},"    steps",{"type":19,"tag":109,"props":1169,"children":1170},{"style":888},[1171],{"type":24,"value":891},{"type":19,"tag":109,"props":1173,"children":1174},{"class":111,"line":203},[1175,1180,1185,1189],{"type":19,"tag":109,"props":1176,"children":1177},{"style":888},[1178],{"type":24,"value":1179},"    - ",{"type":19,"tag":109,"props":1181,"children":1182},{"style":882},[1183],{"type":24,"value":1184},"uses",{"type":19,"tag":109,"props":1186,"children":1187},{"style":888},[1188],{"type":24,"value":916},{"type":19,"tag":109,"props":1190,"children":1191},{"style":937},[1192],{"type":24,"value":1193},"actions/checkout@v4\n",{"type":19,"tag":109,"props":1195,"children":1196},{"class":111,"line":212},[1197],{"type":19,"tag":109,"props":1198,"children":1199},{"style":888},[1200],{"type":24,"value":191},{"type":19,"tag":109,"props":1202,"children":1203},{"class":111,"line":221},[1204,1208,1212,1216],{"type":19,"tag":109,"props":1205,"children":1206},{"style":888},[1207],{"type":24,"value":1179},{"type":19,"tag":109,"props":1209,"children":1210},{"style":882},[1211],{"type":24,"value":1048},{"type":19,"tag":109,"props":1213,"children":1214},{"style":888},[1215],{"type":24,"value":916},{"type":19,"tag":109,"props":1217,"children":1218},{"style":937},[1219],{"type":24,"value":1220},"配置 .NET 8 编译环境\n",{"type":19,"tag":109,"props":1222,"children":1223},{"class":111,"line":230},[1224,1229,1233],{"type":19,"tag":109,"props":1225,"children":1226},{"style":882},[1227],{"type":24,"value":1228},"      uses",{"type":19,"tag":109,"props":1230,"children":1231},{"style":888},[1232],{"type":24,"value":916},{"type":19,"tag":109,"props":1234,"children":1235},{"style":937},[1236],{"type":24,"value":1237},"actions/setup-dotnet@v4\n",{"type":19,"tag":109,"props":1239,"children":1240},{"class":111,"line":238},[1241,1246],{"type":19,"tag":109,"props":1242,"children":1243},{"style":882},[1244],{"type":24,"value":1245},"      with",{"type":19,"tag":109,"props":1247,"children":1248},{"style":888},[1249],{"type":24,"value":891},{"type":19,"tag":109,"props":1251,"children":1252},{"class":111,"line":247},[1253,1258,1262],{"type":19,"tag":109,"props":1254,"children":1255},{"style":882},[1256],{"type":24,"value":1257},"        dotnet-version",{"type":19,"tag":109,"props":1259,"children":1260},{"style":888},[1261],{"type":24,"value":916},{"type":19,"tag":109,"props":1263,"children":1264},{"style":937},[1265],{"type":24,"value":1266},"'8.0.x'\n",{"type":19,"tag":109,"props":1268,"children":1269},{"class":111,"line":256},[1270],{"type":19,"tag":109,"props":1271,"children":1272},{"emptyLinePlaceholder":143},[1273],{"type":24,"value":146},{"type":19,"tag":109,"props":1275,"children":1276},{"class":111,"line":265},[1277,1281,1285,1289],{"type":19,"tag":109,"props":1278,"children":1279},{"style":888},[1280],{"type":24,"value":1179},{"type":19,"tag":109,"props":1282,"children":1283},{"style":882},[1284],{"type":24,"value":1048},{"type":19,"tag":109,"props":1286,"children":1287},{"style":888},[1288],{"type":24,"value":916},{"type":19,"tag":109,"props":1290,"children":1291},{"style":937},[1292],{"type":24,"value":1293},"编译发布\n",{"type":19,"tag":109,"props":1295,"children":1296},{"class":111,"line":274},[1297,1302,1306],{"type":19,"tag":109,"props":1298,"children":1299},{"style":882},[1300],{"type":24,"value":1301},"      run",{"type":19,"tag":109,"props":1303,"children":1304},{"style":888},[1305],{"type":24,"value":916},{"type":19,"tag":109,"props":1307,"children":1308},{"style":937},[1309],{"type":24,"value":1310},"dotnet publish -c Release -o ./publish_out\n",{"type":19,"tag":109,"props":1312,"children":1313},{"class":111,"line":283},[1314],{"type":19,"tag":109,"props":1315,"children":1316},{"emptyLinePlaceholder":143},[1317],{"type":24,"value":146},{"type":19,"tag":109,"props":1319,"children":1320},{"class":111,"line":291},[1321,1325,1329,1333],{"type":19,"tag":109,"props":1322,"children":1323},{"style":888},[1324],{"type":24,"value":1179},{"type":19,"tag":109,"props":1326,"children":1327},{"style":882},[1328],{"type":24,"value":1048},{"type":19,"tag":109,"props":1330,"children":1331},{"style":888},[1332],{"type":24,"value":916},{"type":19,"tag":109,"props":1334,"children":1335},{"style":937},[1336],{"type":24,"value":1337},"准备 Docker 文件\n",{"type":19,"tag":109,"props":1339,"children":1340},{"class":111,"line":300},[1341,1345,1349],{"type":19,"tag":109,"props":1342,"children":1343},{"style":882},[1344],{"type":24,"value":1301},{"type":19,"tag":109,"props":1346,"children":1347},{"style":888},[1348],{"type":24,"value":916},{"type":19,"tag":109,"props":1350,"children":1352},{"style":1351},"--shiki-default:#F97583",[1353],{"type":24,"value":1354},"|\n",{"type":19,"tag":109,"props":1356,"children":1357},{"class":111,"line":308},[1358],{"type":19,"tag":109,"props":1359,"children":1360},{"style":937},[1361],{"type":24,"value":1362},"        cp Dockerfile ./publish_out/\n",{"type":19,"tag":109,"props":1364,"children":1365},{"class":111,"line":317},[1366],{"type":19,"tag":109,"props":1367,"children":1368},{"style":937},[1369],{"type":24,"value":1370},"        cp docker-compose.yml ./publish_out/\n",{"type":19,"tag":109,"props":1372,"children":1373},{"class":111,"line":326},[1374],{"type":19,"tag":109,"props":1375,"children":1376},{"emptyLinePlaceholder":143},[1377],{"type":24,"value":146},{"type":19,"tag":109,"props":1379,"children":1380},{"class":111,"line":335},[1381,1385,1389,1393],{"type":19,"tag":109,"props":1382,"children":1383},{"style":888},[1384],{"type":24,"value":1179},{"type":19,"tag":109,"props":1386,"children":1387},{"style":882},[1388],{"type":24,"value":1048},{"type":19,"tag":109,"props":1390,"children":1391},{"style":888},[1392],{"type":24,"value":916},{"type":19,"tag":109,"props":1394,"children":1395},{"style":937},[1396],{"type":24,"value":1397},"SCP 传输文件到服务器\n",{"type":19,"tag":109,"props":1399,"children":1400},{"class":111,"line":344},[1401,1405,1409],{"type":19,"tag":109,"props":1402,"children":1403},{"style":882},[1404],{"type":24,"value":1228},{"type":19,"tag":109,"props":1406,"children":1407},{"style":888},[1408],{"type":24,"value":916},{"type":19,"tag":109,"props":1410,"children":1411},{"style":937},[1412],{"type":24,"value":1413},"appleboy/scp-action@v0.1.7\n",{"type":19,"tag":109,"props":1415,"children":1416},{"class":111,"line":352},[1417,1421],{"type":19,"tag":109,"props":1418,"children":1419},{"style":882},[1420],{"type":24,"value":1245},{"type":19,"tag":109,"props":1422,"children":1423},{"style":888},[1424],{"type":24,"value":891},{"type":19,"tag":109,"props":1426,"children":1427},{"class":111,"line":361},[1428,1433,1437,1442],{"type":19,"tag":109,"props":1429,"children":1430},{"style":882},[1431],{"type":24,"value":1432},"        host",{"type":19,"tag":109,"props":1434,"children":1435},{"style":888},[1436],{"type":24,"value":916},{"type":19,"tag":109,"props":1438,"children":1439},{"style":937},[1440],{"type":24,"value":1441},"${{ secrets.BWH_IP }}",{"type":19,"tag":109,"props":1443,"children":1444},{"style":960},[1445],{"type":24,"value":1446},"          # 你的 VPS IP\n",{"type":19,"tag":109,"props":1448,"children":1449},{"class":111,"line":370},[1450,1455,1459],{"type":19,"tag":109,"props":1451,"children":1452},{"style":882},[1453],{"type":24,"value":1454},"        username",{"type":19,"tag":109,"props":1456,"children":1457},{"style":888},[1458],{"type":24,"value":916},{"type":19,"tag":109,"props":1460,"children":1461},{"style":937},[1462],{"type":24,"value":1463},"root\n",{"type":19,"tag":109,"props":1465,"children":1466},{"class":111,"line":379},[1467,1472,1476,1481],{"type":19,"tag":109,"props":1468,"children":1469},{"style":882},[1470],{"type":24,"value":1471},"        key",{"type":19,"tag":109,"props":1473,"children":1474},{"style":888},[1475],{"type":24,"value":916},{"type":19,"tag":109,"props":1477,"children":1478},{"style":937},[1479],{"type":24,"value":1480},"${{ secrets.SSH_PRIVATE_KEY }}",{"type":19,"tag":109,"props":1482,"children":1483},{"style":960},[1484],{"type":24,"value":1485},"  # 私钥 (在 GitHub 后台配置)\n",{"type":19,"tag":109,"props":1487,"children":1488},{"class":111,"line":388},[1489,1494,1498],{"type":19,"tag":109,"props":1490,"children":1491},{"style":882},[1492],{"type":24,"value":1493},"        source",{"type":19,"tag":109,"props":1495,"children":1496},{"style":888},[1497],{"type":24,"value":916},{"type":19,"tag":109,"props":1499,"children":1500},{"style":937},[1501],{"type":24,"value":1502},"\"./publish_out/*\"\n",{"type":19,"tag":109,"props":1504,"children":1505},{"class":111,"line":397},[1506,1511,1515],{"type":19,"tag":109,"props":1507,"children":1508},{"style":882},[1509],{"type":24,"value":1510},"        target",{"type":19,"tag":109,"props":1512,"children":1513},{"style":888},[1514],{"type":24,"value":916},{"type":19,"tag":109,"props":1516,"children":1517},{"style":937},[1518],{"type":24,"value":1519},"\"/opt/bwh-bot\"\n",{"type":19,"tag":109,"props":1521,"children":1522},{"class":111,"line":406},[1523,1528,1532],{"type":19,"tag":109,"props":1524,"children":1525},{"style":882},[1526],{"type":24,"value":1527},"        strip_components",{"type":19,"tag":109,"props":1529,"children":1530},{"style":888},[1531],{"type":24,"value":916},{"type":19,"tag":109,"props":1533,"children":1534},{"style":919},[1535],{"type":24,"value":1536},"1\n",{"type":19,"tag":109,"props":1538,"children":1539},{"class":111,"line":415},[1540],{"type":19,"tag":109,"props":1541,"children":1542},{"emptyLinePlaceholder":143},[1543],{"type":24,"value":146},{"type":19,"tag":109,"props":1545,"children":1546},{"class":111,"line":424},[1547,1551,1555,1559],{"type":19,"tag":109,"props":1548,"children":1549},{"style":888},[1550],{"type":24,"value":1179},{"type":19,"tag":109,"props":1552,"children":1553},{"style":882},[1554],{"type":24,"value":1048},{"type":19,"tag":109,"props":1556,"children":1557},{"style":888},[1558],{"type":24,"value":916},{"type":19,"tag":109,"props":1560,"children":1561},{"style":937},[1562],{"type":24,"value":1563},"SSH 触发 Docker 重新构建\n",{"type":19,"tag":109,"props":1565,"children":1566},{"class":111,"line":433},[1567,1571,1575],{"type":19,"tag":109,"props":1568,"children":1569},{"style":882},[1570],{"type":24,"value":1228},{"type":19,"tag":109,"props":1572,"children":1573},{"style":888},[1574],{"type":24,"value":916},{"type":19,"tag":109,"props":1576,"children":1577},{"style":937},[1578],{"type":24,"value":1579},"appleboy/ssh-action@v1.0.3\n",{"type":19,"tag":109,"props":1581,"children":1582},{"class":111,"line":442},[1583,1587],{"type":19,"tag":109,"props":1584,"children":1585},{"style":882},[1586],{"type":24,"value":1245},{"type":19,"tag":109,"props":1588,"children":1589},{"style":888},[1590],{"type":24,"value":891},{"type":19,"tag":109,"props":1592,"children":1593},{"class":111,"line":451},[1594,1598,1602],{"type":19,"tag":109,"props":1595,"children":1596},{"style":882},[1597],{"type":24,"value":1432},{"type":19,"tag":109,"props":1599,"children":1600},{"style":888},[1601],{"type":24,"value":916},{"type":19,"tag":109,"props":1603,"children":1604},{"style":937},[1605],{"type":24,"value":1606},"${{ secrets.BWH_IP }}\n",{"type":19,"tag":109,"props":1608,"children":1609},{"class":111,"line":460},[1610,1614,1618],{"type":19,"tag":109,"props":1611,"children":1612},{"style":882},[1613],{"type":24,"value":1454},{"type":19,"tag":109,"props":1615,"children":1616},{"style":888},[1617],{"type":24,"value":916},{"type":19,"tag":109,"props":1619,"children":1620},{"style":937},[1621],{"type":24,"value":1463},{"type":19,"tag":109,"props":1623,"children":1624},{"class":111,"line":469},[1625,1629,1633],{"type":19,"tag":109,"props":1626,"children":1627},{"style":882},[1628],{"type":24,"value":1471},{"type":19,"tag":109,"props":1630,"children":1631},{"style":888},[1632],{"type":24,"value":916},{"type":19,"tag":109,"props":1634,"children":1635},{"style":937},[1636],{"type":24,"value":1637},"${{ secrets.SSH_PRIVATE_KEY }}\n",{"type":19,"tag":109,"props":1639,"children":1640},{"class":111,"line":478},[1641,1646,1650],{"type":19,"tag":109,"props":1642,"children":1643},{"style":882},[1644],{"type":24,"value":1645},"        script",{"type":19,"tag":109,"props":1647,"children":1648},{"style":888},[1649],{"type":24,"value":916},{"type":19,"tag":109,"props":1651,"children":1652},{"style":1351},[1653],{"type":24,"value":1354},{"type":19,"tag":109,"props":1655,"children":1656},{"class":111,"line":486},[1657],{"type":19,"tag":109,"props":1658,"children":1659},{"style":937},[1660],{"type":24,"value":1661},"          cd /opt/bwh-bot\n",{"type":19,"tag":109,"props":1663,"children":1664},{"class":111,"line":495},[1665],{"type":19,"tag":109,"props":1666,"children":1667},{"style":937},[1668],{"type":24,"value":1669},"          docker compose down\n",{"type":19,"tag":109,"props":1671,"children":1672},{"class":111,"line":504},[1673],{"type":19,"tag":109,"props":1674,"children":1675},{"style":937},[1676],{"type":24,"value":1677},"          docker compose up -d --build\n",{"type":19,"tag":109,"props":1679,"children":1680},{"class":111,"line":513},[1681],{"type":19,"tag":109,"props":1682,"children":1683},{"style":937},[1684],{"type":24,"value":1685},"          docker image prune -f  # 清理旧的无用镜像\n",{"type":19,"tag":1687,"props":1688,"children":1689},"blockquote",{},[1690],{"type":19,"tag":20,"props":1691,"children":1692},{},[1693,1698,1700,1706,1708,1714,1716,1722],{"type":19,"tag":37,"props":1694,"children":1695},{},[1696],{"type":24,"value":1697},"提示",{"type":24,"value":1699},"：记得在 GitHub 仓库的 ",{"type":19,"tag":64,"props":1701,"children":1703},{"className":1702},[],[1704],{"type":24,"value":1705},"Settings -> Secrets and variables -> Actions",{"type":24,"value":1707}," 里把 ",{"type":19,"tag":64,"props":1709,"children":1711},{"className":1710},[],[1712],{"type":24,"value":1713},"BWH_IP",{"type":24,"value":1715}," 和 ",{"type":19,"tag":64,"props":1717,"children":1719},{"className":1718},[],[1720],{"type":24,"value":1721},"SSH_PRIVATE_KEY",{"type":24,"value":1723}," 填好。",{"type":19,"tag":52,"props":1725,"children":1727},{"id":1726},"总结",[1728],{"type":24,"value":1726},{"type":19,"tag":20,"props":1730,"children":1731},{},[1732],{"type":24,"value":1733},"配置好之后，以后每次在本地更新完代码只要 push 一下，云端的服务就会自动完成重建和替换。",{"type":19,"tag":20,"props":1735,"children":1736},{},[1737,1739,1746],{"type":24,"value":1738},"这套流程其实对 VPS 的网络连通性有一定要求。如果服务器网络比较差，GitHub Action 在 SCP 传文件或者 SSH 连接时偶尔会超时报错。平时部署后端服务，建议尽量选网络稳一点的机房（类似 ",{"type":19,"tag":1740,"props":1741,"children":1743},"a",{"href":1742},"/go/bwh-87",[1744],{"type":24,"value":1745},"搬瓦工 CN2 GIA 高端线",{"type":24,"value":1747}," 这种），文件传输基本秒达，CI/CD 流水线跑起来会顺畅很多。",{"type":19,"tag":1749,"props":1750,"children":1751},"style",{},[1752],{"type":24,"value":1753},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":7,"searchDepth":121,"depth":121,"links":1755},[1756,1757,1758,1759],{"id":54,"depth":121,"text":57},{"id":757,"depth":121,"text":760},{"id":991,"depth":121,"text":994},{"id":1726,"depth":121,"text":1726},"markdown","content:blog:bwh-docker-dotnet8-cicd-api-monitor.md","content","blog/bwh-docker-dotnet8-cicd-api-monitor.md","blog/bwh-docker-dotnet8-cicd-api-monitor","md",1789526575180]