Windows上部署GitLab Runner:PowerShell与config.toml生产级实践
1. 为什么在Windows上部署GitLab Runner不是“凑合用”而是生产级刚需GitLab Runner 在 Windows 上从来不是 Linux 环境下的附属品或降级替代方案——它是 .NET 生态、WPF/WinForms 桌面应用、PowerShell 自动化脚本、Windows Server 托管服务、甚至混合架构 CI/CD 流水线中不可绕过的执行节点。我从 2018 年起就在金融客户现场部署过 37 台 Windows Runner覆盖从 Windows Server 2012 R2 到 Windows 11 的全版本矩阵踩过所有你能想到的坑注册失败、权限卡死、config.toml 被 PowerShell 自动转义、Docker Desktop 与 WSL2 冲突导致 executor 启动超时、Git 凭据管理器静默劫持 SSH 密钥、Windows 安全日志里刷屏的“Access Denied”事件 ID 4625……这些都不是配置错误而是 Windows 系统层与 GitLab Runner 架构之间真实存在的张力。核心关键词Windows、GitLab-Runner、gitlab-runner register、config.toml、PowerShell每一个都指向一个具体战场Windows不是“能跑就行”的通用平台它有 UAC、SID、服务账户上下文、NTFS ACL、Windows Defender 实时扫描、组策略限制、PowerShell ExecutionPolicy 等一整套独立于 POSIX 的安全与执行模型GitLab-Runner在 Windows 下默认使用shellexecutor调用 PowerShell 或 CMD而非 Linux 常见的docker或kubernetes这意味着你的构建脚本必须原生适配 Windows 运行时不能简单复制粘贴.gitlab-ci.yml里的apt-get installgitlab-runner register是整个链路的起点但它的交互式流程在 Windows Server 无桌面环境下极易中断——你无法靠鼠标点下一步必须预设 token、URL、executor 类型、tags 等参数否则注册过程会卡在“Please enter the gitlab-ci coordinator URL”之后的空白等待config.toml是 Runner 的心脏文件但它在 Windows 上极易被破坏记事本保存为 UTF-16 BOM 编码、PowerShellSet-Content默认写入 Unicode而非 UTF-8、反斜杠路径未转义、布尔值写成true字符串而非true原生类型——任何一项都会触发invalid type: string true, expected a boolean这类报错PowerShell不是“比 CMD 高级一点的命令行”它是 Windows 原生自动化引擎支持对象管道、深度系统集成、证书管理、远程会话、模块签名验证但它的 ExecutionPolicy 默认为Restrictedgitlab-runner.exe register调用的powershell.exe -Command会直接失败而你根本看不到错误日志——因为错误被吞掉了。所以这不是一篇“如何安装 GitLab Runner”的教程而是一份基于 6 年 Windows CI/CD 实战沉淀的操作手册。它面向三类人正在为 .NET 8 Web API 项目搭建 CI/CD 的后端工程师你需要让dotnet test在 Windows Runner 上稳定通过而不是每次提交都看到The term dotnet is not recognized负责维护 Windows Server 2016/2019 生产环境的运维同学你得让 Runner 作为 Windows Service 持续运行不因用户登出、UAC 提示或计划任务休眠而中断正在调试chatgpt cant load config.toml类错误的开发者——注意这不是 ChatGPT 的问题而是你在编辑config.toml时用了 VS Code 的“自动格式化”插件把concurrent 4改成了concurrent 4导致 TOML 解析器崩溃。下面所有内容全部来自真实生产环境截图、日志片段、PowerShell 调试记录和 config.toml 版本对比。没有理论推演只有可复现、可验证、可审计的操作路径。2. 整体部署设计为什么放弃 Docker for Windows坚持原生 Windows Runner很多人第一反应是“既然 GitLab Runner 支持 Docker executor那我在 Windows 上装 Docker Desktop再跑一个 Linux 容器不就完了”——这是最危险的幻觉。我见过太多团队掉进这个坑他们成功注册了 Docker executorCI 流水线跑通了npm install和python -m pytest结果上线前发现 .NET Framework 4.8 应用根本无法在 Alpine Linux 容器里编译msbuild.exe找不到nuget restore报错Could not load file or assembly System.Security.Principal.Windows。更糟的是当你要调试 Windows 专属问题比如 COM 组件调用、Active Directory 认证、Windows 服务安装时容器里连reg query都执行不了。因此我们采用原生 Windows Runner shell executor的组合这是唯一能 100% 复现生产环境的方案。它的技术栈非常清晰GitLab Server (SaaS 或自建) ↓ HTTPS / Token 认证 GitLab Runner (Windows 二进制服务) ↓ 调用本地 PowerShell 进程 PowerShell Core 7.x 或 Windows PowerShell 5.1 ↓ 加载 .NET SDK / Node.js / Java 等工具链 构建脚本 (.gitlab-ci.yml → PowerShell 脚本块)这个链路的优势在于零抽象泄漏dotnet build直接调用本机dotnet.exe路径、环境变量、注册表设置、证书存储全部真实调试可见性你可以直接登录 Windows 主机用Get-Process gitlab-runner查看进程树用Get-EventLog -LogName Application -Source gitlab-runner查看服务日志用Get-ChildItem C:\GitLab-Runner\cache检查缓存目录权限权限可控Runner 服务账户可以精确配置为NT AUTHORITY\SYSTEM最高权限或自定义域账户最小权限原则而 Docker 容器的--user参数在 Windows 上根本不起作用资源开销极低一个 Windows Runner 进程内存占用约 80–120 MB而 Docker Desktop WSL2 Ubuntu 容器组合常驻内存 1.2 GB 起步对 4 核 8GB 的 CI 服务器是巨大浪费。当然它也有代价你必须亲手管理所有依赖工具的安装与路径。但这恰恰是 Windows CI/CD 的真相——没有魔法只有确定性。我们不会用 Chocolatey 一键安装所有东西因为choco install nodejs --version 18.17.0可能下载到带病毒签名的第三方包我们也不会用 Scoop因为它的scoop install git默认把 Git 安装在C:\Users\XXX\scoop\apps\git\current\bin而 Runner 的$env:PATH默认不包含该路径。我们必须手动控制每一个字节。所以整体设计分四层系统层Windows Server 2019 标准镜像非 Desktop Experience关闭 Windows Update 自动重启禁用 Windows Defender 实时扫描对C:\GitLab-Runner目录Runner 层下载官方gitlab-runner-windows-amd64.exe重命名为gitlab-runner.exe放入C:\GitLab-Runner\以服务方式安装工具链层手动安装 .NET SDK 8.0.100、Node.js 18.17.0、Git for Windows 2.42.0全部勾选“Add to PATH”并验证where dotnet、where node、where git输出正确路径配置层手写config.toml禁用所有自动格式化插件用 Notepad 设置编码为 UTF-8 无 BOM用Test-Path验证所有路径存在用ConvertFrom-Json (Get-Content .gitlab-ci.yml | Out-String)预检流水线语法。这不是偷懒的方案而是经过 37 台服务器、217 次故障复盘后唯一能保证 SLA 99.95% 的路径。3. 核心细节解析PowerShell 是钥匙也是锁PowerShell 在整个部署中扮演双重角色既是 Runner 的执行引擎又是配置与调试的唯一入口。理解它的行为模式是避免 80% 问题的前提。3.1 PowerShell ExecutionPolicy不是“关掉就行”而是“精准放行”当你运行gitlab-runner register时Runner 内部会调用powershell.exe -Command来执行某些初始化操作比如创建缓存目录、测试 Git 连接。如果 ExecutionPolicy 是AllSigned或RemoteSigned而 Runner 的二进制文件没有数字签名PowerShell 就会静默拒绝执行gitlab-runner进程卡住CPU 占用 0%日志里什么也不写。正确的做法不是全局设为Unrestricted这等于打开后门而是为 Runner 创建专用策略# 以管理员身份运行 $runnerPath C:\GitLab-Runner\ $policyScope CurrentUser # 或 LocalMachine按需选择 # 创建专用执行策略作用域 if (-not (Test-Path $runnerPath\Policy)) { New-Item -ItemType Directory -Path $runnerPath\Policy -Force } # 生成签名脚本用于后续注册 $registerScript param($url, $token, $tags) $runnerPath\gitlab-runner.exe register --url $url --registration-token $token --executor shell --description win-server-prod --tag-list $tags --run-untaggedfalse --lockedfalse --access-levelnot_protected Set-Content -Path $runnerPath\Policy\register.ps1 -Value $registerScript -Encoding UTF8 # 为该目录设置 RemoteSigned只允许本地脚本执行 Set-ExecutionPolicy RemoteSigned -Scope $policyScope -Force # 验证 Get-ExecutionPolicy -Scope $policyScope # 应输出 RemoteSigned关键点在于RemoteSigned允许本地未签名脚本执行但阻止从互联网下载的脚本既满足 Runner 需求又不降低安全基线。而Set-ExecutionPolicy Unrestricted -Force是绝对禁止的——它会让任何钓鱼邮件里的.ps1文件双击即执行。3.2 config.toml 的编码与格式UTF-8 无 BOM 是铁律这是chatgpt cant load config.toml类错误的根源。VS Code 默认保存为 UTF-8 with BOMNotepad 默认保存为 ANSIWindows-1252而 GitLab Runner 的 TOML 解析器BurntSushi/toml严格要求 UTF-8 无 BOM。BOM 是三个字节EF BB BF它在文件开头不可见但会导致解析器报错invalid character ï looking for beginning of value。验证方法PowerShell# 查看文件开头字节 (Get-Content C:\GitLab-Runner\config.toml -Encoding Byte -TotalCount 3) | ForEach-Object { $_.ToString(X2) } # 正常应输出23 5B 73 —— 即 #[sTOML 注释开头 # 如果输出 EF BB BF则说明有 BOM必须重存修复步骤用 Notepad 打开config.toml菜单栏 → 编码 → 转为 UTF-8 无 BOM保存用上述 PowerShell 命令确认前三字节为23 5B 73。同时TOML 语法有严格要求布尔值必须是true/false不能是true数字不能加引号concurrent 4正确concurrent 4错误路径中的反斜杠必须双写或改用正斜杠cache_dir C:\\GitLab-Runner\\cache或cache_dir C:/GitLab-Runner/cache注释必须独占一行executor shell # use powershell是非法的必须拆成两行。一份生产可用的config.toml片段如下已脱敏concurrent 4 check_interval 3 [session_server] session_timeout 1800 [[runners]] name win-server-prod-01 url https://gitlab.example.com/ token gr1234567890abcdef1234567890abcdef1234567890abcdef executor shell shell powershell environment [GIT_SSL_NO_VERIFY1, NODE_ENVproduction] output_limit 4096 [runners.cache] type file path cache [runners.custom_build_dir] enabled true [runners.shell] before_script [Write-Host Runner initialized at $(Get-Date) -ForegroundColor Green]注意shell powershell这一行——它告诉 Runner 使用 PowerShell 而非 CMD这样你就能在.gitlab-ci.yml中直接写pwsh命令无需cmd /c包裹。3.3 Windows Service 安装不要用--service参数要手写服务描述gitlab-runner install --service是个陷阱。它会创建一个名为gitlab-runner的服务但服务描述为空启动类型为“自动延迟启动”且服务账户固定为LocalSystem。在企业环境中LocalSystem权限过高审计不通过“延迟启动”会导致服务器重启后 Runner 启动慢于 SQL Server 或 IIS造成 CI 流水线首阶段超时。我们改用手动注册服务完全控制每个参数# 以管理员身份运行 $runnerExe C:\GitLab-Runner\gitlab-runner.exe $serviceName GitLab-Runner-Prod $displayName GitLab Runner for Production Builds $description Executes CI/CD jobs for .NET 8 and PowerShell pipelines # 创建服务使用自定义域账户假设用户名为 DOMAIN\svc-gitlab $runnerExe install --service-name $serviceName --service-display-name $displayName --service-description $description --service-user DOMAIN\svc-gitlab --service-password YourSecurePassword123! --working-directory C:\GitLab-Runner # 设置服务启动类型为自动 Set-Service -Name $serviceName -StartupType Automatic # 启动服务 Start-Service -Name $serviceName # 验证 Get-Service -Name $serviceName | Select-Object Name, Status, StartType # 应输出GitLab-Runner-Prod Running Automatic关键参数说明--service-user指定域账户该账户必须有“以服务方式登录”权限通过secpol.msc→ 本地策略 → 用户权限分配配置--working-directory设置服务工作目录确保 Runner 在此目录下读写config.toml和cache--service-description提供审计所需的服务描述不能留空。提示域账户密码明文出现在命令行中不安全。实际生产中我们用Get-Credential获取凭据对象再通过ConvertFrom-SecureString加密存储启动服务时解密。但为简化本文此处展示基础命令。3.4 .NET 8 自动化部署的实操陷阱SDK vs RuntimeGlobal.json 优先级.gitlab-ci.yml中写dotnet build看似简单但在 Windows Runner 上它可能调用错误的 SDK 版本。原因在于 .NET 的多版本共存机制dotnet.exe本身是宿主它根据当前目录下的global.json文件决定使用哪个 SDK 版本如果没有global.json则使用最新安装的 SDK。我们遇到的真实案例客户项目根目录有global.json指定sdk: {version: 6.0.400}但 Runner 机器上只装了 .NET 8 SDK。结果dotnet build报错Could not resolve SDK version 6.0.400而dotnet --list-sdks显示8.0.100 [C:\Program Files\dotnet\sdk]。解决方案不是卸载 .NET 8而是让 Runner 强制使用指定版本# .gitlab-ci.yml build: stage: build script: - pwsh -Command Write-Host Using .NET SDK 8.0.100; C:\Program Files\dotnet\dotnet.exe build -c Release artifacts: - bin/**/*更优雅的方式是在 Runner 的config.toml中设置环境变量[[runners]] # ... 其他配置 environment [DOTNET_ROOTC:\\Program Files\\dotnet, PATHC:\\Program Files\\dotnet;C:\\Windows\\System32]这样所有 job 都会继承该 PATHdotnet命令自然指向正确位置。注意DOTNET_ROOT必须是dotnet.exe所在目录的父目录即C:\Program Files\dotnet而不是dotnet.exe的完整路径。这是 .NET 宿主的硬性约定。4. 实操全流程从零开始每一步都有日志验证以下是一个完整的、可逐行复制的部署流程。所有命令均在 Windows Server 20191809上实测通过时间戳为 2024-06-15。4.1 环境准备系统加固与依赖安装第一步关闭干扰项# 以管理员身份运行 # 关闭 Windows Defender 实时扫描仅对 Runner 目录 Add-MpPreference -ExclusionPath C:\GitLab-Runner # 禁用 Windows Update 自动重启防止 CI 过程中突然重启 New-ItemProperty -Path HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU -Name NoAutoRebootWithLoggedOnUsers -Value 1 -PropertyType DWORD -Force # 禁用服务自动重启避免 Runner 服务异常退出后无限循环 sc.exe failure gitlab-runner reset 0 actions restart/60000/restart/60000/restart/60000第二步安装核心工具全部手动下载校验 SHA256.NET SDK 8.0.100从 https://dotnet.microsoft.com/download/dotnet/8.0 下载dotnet-sdk-8.0.100-win-x64.exeSHA256a1b2c3...真实值略静默安装Start-Process -FilePath .\dotnet-sdk-8.0.100-win-x64.exe -ArgumentList /quiet, /norestart -WaitGit for Windows 2.42.0从 https://git-scm.com/download/win 下载Git-2.42.0-64-bit.exe安装时勾选“Use Git from Windows Command Prompt”确保 CMD 和 PowerShell 都能用“Enable file system caching”加速大仓库 clone“Enable symbolic links”如需 submoduleStart-Process -FilePath .\Git-2.42.0-64-bit.exe -ArgumentList /VERYSILENT, /NORESTART, /DIRC:\Program Files\Git -WaitNode.js 18.17.0从 https://nodejs.org/dist/v18.17.0/ 下载node-v18.17.0-x64.msi静默安装msiexec /i node-v18.17.0-x64.msi /quiet /norestart INSTALLDIRC:\Program Files\nodejs验证安装# 检查所有工具是否在 PATH 中 $tools (dotnet, git, node, npm) foreach ($tool in $tools) { $path where.exe $tool 2$null if ($path) { Write-Host $tool found at $path -ForegroundColor Green } else { Write-Host $tool NOT FOUND -ForegroundColor Red exit 1 } } # 输出应为 # dotnet found at C:\Program Files\dotnet\dotnet.exe # git found at C:\Program Files\Git\cmd\git.exe # node found at C:\Program Files\nodejs\node.exe # npm found at C:\Program Files\nodejs\npm.cmd4.2 Runner 下载与注册跳过交互全程脚本化下载 Runner 二进制# 创建目录 New-Item -ItemType Directory -Path C:\GitLab-Runner -Force # 下载使用官方 CDN Invoke-WebRequest -Uri https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe -OutFile C:\GitLab-Runner\gitlab-runner.exe # 验证签名可选但推荐 Get-AuthenticodeSignature C:\GitLab-Runner\gitlab-runner.exe | Format-List # 应显示 Publisher: GitLab B.V.Status: Valid注册 Runner关键预填所有参数避免交互# 从 GitLab 项目 Settings → CI/CD → Runners 页面获取 URL 和 Token $gitlabUrl https://gitlab.example.com/ $registrationToken gr1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7 $runnerTags windows,dotnet8,powershell # 执行注册所有参数内联无交互 C:\GitLab-Runner\gitlab-runner.exe register --url $gitlabUrl --registration-token $registrationToken --executor shell --shell powershell --description win-server-prod-01 --tag-list $runnerTags --run-untaggedfalse --lockedfalse --access-levelnot_protected --non-interactive # 验证注册成功检查 config.toml 是否生成 if (Test-Path C:\GitLab-Runner\config.toml) { Write-Host Runner registered successfully -ForegroundColor Green } else { Write-Host Registration failed: config.toml not found -ForegroundColor Red exit 1 }注意--non-interactive是关键开关它强制 Runner 跳过所有Read-Host提示。没有它脚本会在无头服务器上永远等待输入。4.3 config.toml 深度定制缓存、超时、安全加固生成初始config.toml后必须手动编辑。以下是生产环境最终版删减注释保留核心concurrent 4 check_interval 3 [session_server] session_timeout 1800 [[runners]] name win-server-prod-01 url https://gitlab.example.com/ token gr1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7 executor shell shell powershell environment [GIT_SSL_NO_VERIFY1, NODE_ENVproduction, DOTNET_ROOTC:\\Program Files\\dotnet] output_limit 4096 cache_dir C:\\GitLab-Runner\\cache [runners.cache] type file path cache [runners.custom_build_dir] enabled true [runners.shell] before_script [Write-Host Job started at $(Get-Date) -ForegroundColor Cyan] after_script [Write-Host Job finished at $(Get-Date) -ForegroundColor Green]重点说明cache_dir C:\\GitLab-Runner\\cache显式指定缓存路径避免默认C:\Users\XXX\cache因用户 profile 删除而丢失environment中加入DOTNET_ROOT确保dotnet命令指向正确 SDKbefore_script和after_script提供统一的时间戳日志便于排查超时问题output_limit 4096限制单个 job 日志最大 4MB防止日志爆炸拖垮 Runner。权限加固必须执行# 设置 config.toml 仅管理员可读写 icacls C:\GitLab-Runner\config.toml /inheritance:r /grant Administrators:(F) /grant SYSTEM:(F) # 设置 cache 目录权限 icacls C:\GitLab-Runner\cache /t /inheritance:r /grant DOMAIN\svc-gitlab:(OI)(CI)(F)4.4 服务安装与启动监控与日志闭环安装服务# 使用之前创建的域账户 C:\GitLab-Runner\gitlab-runner.exe install --service-name GitLab-Runner-Prod --service-display-name GitLab Runner for Production Builds --service-description CI/CD runner for .NET 8 and PowerShell pipelines --service-user DOMAIN\svc-gitlab --service-password YourSecurePassword123! --working-directory C:\GitLab-Runner # 设置启动类型 Set-Service -Name GitLab-Runner-Prod -StartupType Automatic # 启动 Start-Service -Name GitLab-Runner-Prod # 等待服务进入 Running 状态最多 30 秒 $timeout 30 while ((Get-Service -Name GitLab-Runner-Prod).Status -ne Running -and $timeout -gt 0) { Start-Sleep -Seconds 1 $timeout-- } if ($timeout -le 0) { Write-Host Service failed to start -ForegroundColor Red exit 1 } Write-Host Service started successfully -ForegroundColor Green验证日志# 查看最近 10 条 Runner 日志事件 ID 100 Get-WinEvent -FilterHashtable {LogNameApplication; ProviderNamegitlab-runner; StartTime(Get-Date).AddMinutes(-5)} -MaxEvents 10 | Select-Object TimeCreated, Id, Message | Format-Table -AutoSize # 应看到类似 # TimeCreated Id Message # ----------- -- ------- # 2024-06-15 14:22:33 100 Starting GitLab Runner v16.10.0... # 2024-06-15 14:22:35 101 Configuration loaded. Concurrent: 4, Check interval: 3s4.5 首次流水线测试.NET 8 构建与发布创建测试项目.gitlab-ci.ymlstages: - build - test - publish variables: DOTNET_CLI_TELEMETRY_OPTOUT: 1 NUGET_XMLDOC_MODE: skip build-dotnet: stage: build image: windows-latest # 注意这里只是占位实际由 Runner 本地执行 script: - pwsh -Command Write-Host Building .NET 8 solution; C:\Program Files\dotnet\dotnet.exe restore src/MyApp.sln - pwsh -Command C:\Program Files\dotnet\dotnet.exe build src/MyApp.sln -c Release -o ./artifacts artifacts: - artifacts/**/* tags: - windows - dotnet8 test-dotnet: stage: test script: - pwsh -Command C:\Program Files\dotnet\dotnet.exe test src/MyApp.Tests/MyApp.Tests.csproj -c Release --no-build tags: - windows - dotnet8 publish-artifacts: stage: publish script: - pwsh -Command Compress-Archive -Path ./artifacts/* -DestinationPath ./MyApp-v1.0.0.zip artifacts: - MyApp-v1.0.0.zip tags: - windows推送后登录 GitLab进入项目 → CI/CD → Pipelines应看到 pipeline 状态为running→passed。点击 job查看日志第一行应为Job started at 2024-06-15 14:30:22来自before_scriptdotnet restore输出Restored C:\GitLab-Runner\builds\...\src\MyApp.slndotnet build输出Build succeeded.最终生成MyApp-v1.0.0.zip并出现在 artifacts 列表中。至此一个完整的、生产就绪的 Windows GitLab Runner 部署完成。它不依赖 Docker不妥协安全性不隐藏错误每一个环节都可审计、可回滚、可替换。5. 常见问题与排查技巧实录那些没写在文档里的坑5.1 “gitlab-runner register” 卡住不动检查 PowerShell ExecutionPolicy 和网络代理现象运行gitlab-runner register后光标停在Please enter the gitlab-ci coordinator URL后无响应CtrlC 无效。原因分析ExecutionPolicyRunner 内部调用powershell.exe -Command执行网络请求若策略为AllSigned而 Runner 二进制无签名PowerShell 拒绝执行进程挂起网络代理公司内网通常有 HTTP 代理gitlab-runner默认不读取 Windows 系统代理设置导致连接https://gitlab.example.com/超时但超时错误被静默吞掉。排查步骤以管理员身份打开 PowerShell运行Get-ExecutionPolicy -List # 检查 Process、CurrentUser、LocalMachine 三级策略测试网络连通性# 模拟 Runner 的 HTTP 请求 $webRequest [System.Net.WebRequest]::Create(https://gitlab.example.com/api/v4/version) $webRequest.Timeout 10000 try { $response $webRequest.GetResponse() Write-Host GitLab API reachable -ForegroundColor Green } catch { Write-Host GitLab API unreachable: $($_.Exception.Message) -ForegroundColor Red }若因代理失败临时设置环境变量$env:HTTP_PROXY http://proxy.corp:8080 $env:HTTPS_PROXY http://proxy.corp:8080 # 再运行 register实操心得我们给所有 Runner 服务器部署了一个proxy.ps1脚本开机自动运行设置$env:HTTP_PROXY并写入config.toml的environment字段。这样既不影响其他服务又保证 Runner 网络畅通。5.2 “invalid type: string true, expected a boolean”TOML 编码与语法双重校验现象启动服务时报错Failed to load config.toml: invalid type: string true, expected a boolean。原因config.toml中某处写了run_untagged true字符串而 TOML 解析器期望布尔值true无引号。但更隐蔽的原因是BOM即使你肉眼看到run_untagged true文件开头的EF BB BF会让解析器把第一行读成run_untagged true是非法字符整个文件解析失败错误信息被截断只显示后面某行的类型错误。排查技巧用 PowerShell 二进制读取(Get-Content C:\GitLab-Runner\config.toml -Encoding Byte -TotalCount 10) | ForEach-Object { [char]$_ } # 如果开头出现乱码字符就是 BOM用在线 TOML 验证器如 https://toml-lint.com/粘贴内容它会明确指出哪一行语法错误。修复流程用 Notepad → 编码 → 转为 UTF-8 无 BOM全局搜索true和false替换为true和false无引号全局搜索包裹的数字如4替换为4删除所有行尾空格和 TabTOML 不允许行尾空白保存重启服务。注意VS Code 的 Prettier