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