{"id":1442,"date":"2026-11-03T15:00:25","date_gmt":"2026-11-03T06:00:25","guid":{"rendered":"https:\/\/www.winserver.net\/blog\/?p=1442"},"modified":"2026-11-04T10:42:26","modified_gmt":"2026-11-04T01:42:26","slug":"github-actions-self-hosted-runner-windows-vps","status":"publish","type":"post","link":"https:\/\/www.winserver.net\/blog\/github-actions-self-hosted-runner-windows-vps\/","title":{"rendered":"How to Set Up a GitHub Actions Self-Hosted Runner on a Windows VPS (Step by Step)"},"content":{"rendered":"<p><em>Last updated: 2026\/10\/30<\/em><\/p>\n<p>If you build Windows software with GitHub Actions, such as .NET apps, desktop tools or Windows installers, you need a Windows machine to run your builds. GitHub&#8217;s own Windows runners are charged by the minute and start from scratch every time. A self-hosted runner on a Windows VPS gives you an always-on build machine with your tools already installed, for a flat monthly cost.<\/p>\n<div style=\"border: 2px solid #045FA4; border-radius: 8px; background: #F4F8FC; padding: 16px 20px; margin: 0 0 24px;\">\n<p style=\"margin: 0 0 8px; font-weight: bold; color: #394f6d;\">Key takeaways<\/p>\n<ul style=\"margin: 0; padding-left: 20px;\">\n<li><strong>A self-hosted runner is a build machine you control.<\/strong> GitHub sends jobs to it, and it keeps your tools and caches between builds.<\/li>\n<li><strong>Setup takes about 15 minutes:<\/strong> download the runner, connect it to your repository, and install it as a Windows service.<\/li>\n<li><strong>Runs as a service<\/strong>, so it starts automatically after reboots with no one signed in.<\/li>\n<li><strong>Use it with private repositories only.<\/strong> GitHub warns that public repositories can let others run code on your machine.<\/li>\n<\/ul>\n<\/div>\n<h2>Why use a self-hosted runner on a Windows VPS?<\/h2>\n<p>GitHub offers two kinds of runners: GitHub-hosted (GitHub provides the machine) and self-hosted (you provide it). For Windows builds, a self-hosted runner on a VPS has some clear advantages:<\/p>\n<ul>\n<li><strong>Flat monthly cost.<\/strong> GitHub-hosted Windows runners cost $0.010 per minute from January 2026, beyond your plan&#8217;s included minutes. If you build often, a VPS at a fixed monthly price can work out cheaper.<\/li>\n<li><strong>Tools stay installed.<\/strong> GitHub-hosted runners are wiped after every job. On your own VPS, SDKs, build tools and dependency caches stay in place, so builds often start faster.<\/li>\n<li><strong>Full control.<\/strong> Install exactly the software versions you need, including licensed or older tools that GitHub&#8217;s images don&#8217;t include.<\/li>\n<li><strong>A fixed IP address.<\/strong> Useful when builds need to reach servers, databases or APIs that only allow approved IP addresses.<\/li>\n<\/ul>\n<p><strong>A note on pricing:<\/strong> in December 2025, GitHub announced a per-minute fee for self-hosted runners in private repositories, then postponed it the next day after community feedback. Check <strong><span style=\"color: #0189ff;\"><a href=\"https:\/\/docs.github.com\/en\/billing\" target=\"_blank\" rel=\"noopener\" style=\"color: #0189ff;\">GitHub&#8217;s current billing documentation<\/a> <\/span><\/strong>for the latest before you decide.<\/p>\n<h3>When GitHub-hosted runners are the better choice<\/h3>\n<ul>\n<li>You only build occasionally, and your plan&#8217;s included minutes cover it.<\/li>\n<li>Your repository is <strong>public<\/strong>. GitHub recommends self-hosted runners for private repositories only (see <a href=\"#security\">Security<\/a>).<\/li>\n<li>You need a fresh, clean machine for every job.<\/li>\n<\/ul>\n<h2>What you need<\/h2>\n<table>\n<thead>\n<tr>\n<th>Item<\/th>\n<th>Requirement<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><span style=\"color: #000000;\"><a href=\"https:\/\/winserver.net\/\" style=\"color: #000000;\">Windows VPS<\/a><\/span><\/td>\n<td>Windows Server 2019, 2022 or 2025<\/td>\n<\/tr>\n<tr>\n<td>Memory<\/td>\n<td>4 GB for light builds. 8 GB or more for .NET, C++ or larger projects.<\/td>\n<\/tr>\n<tr>\n<td>Storage<\/td>\n<td>Enough for your tools, source code and build output. Build folders grow over time.<\/td>\n<\/tr>\n<tr>\n<td>GitHub access<\/td>\n<td>Owner of the repository (personal account), or admin access to it (organization)<\/td>\n<\/tr>\n<tr>\n<td>Repository<\/td>\n<td>A private repository<\/td>\n<\/tr>\n<tr>\n<td>Network<\/td>\n<td>Outbound HTTPS to GitHub. No inbound ports need to be opened.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>Step 1: Install your build tools<\/h2>\n<p>Connect to your VPS with Remote Desktop and install what your builds need, for example Git, the .NET SDK, Visual Studio Build Tools or Node.js. At minimum, install Git. Open <strong>PowerShell as Administrator<\/strong>:<\/p>\n<pre><code class=\"language-powershell\">$ProgressPreference = 'SilentlyContinue'\r\n$rel = Invoke-RestMethod https:\/\/api.github.com\/repos\/git-for-windows\/git\/releases\/latest\r\n$url = ($rel.assets | Where-Object name -match '^Git-.*-64-bit\\.exe$').browser_download_url\r\nInvoke-WebRequest $url -OutFile \"$env:TEMP\\git.exe\" -UseBasicParsing\r\nStart-Process \"$env:TEMP\\git.exe\" -ArgumentList '\/VERYSILENT \/NORESTART' -Wait<\/code><\/pre>\n<p>On Windows Server 2025, you can use <code>winget install --id Git.Git -e --source winget<\/code> instead.<\/p>\n<h2>Step 2: Create the runner folder<\/h2>\n<p>Keep the runner in its own folder near the root of the drive:<\/p>\n<pre><code class=\"language-powershell\">New-Item -ItemType Directory -Force C:\\actions-runner\r\nSet-Location C:\\actions-runner<\/code><\/pre>\n<h2>Step 3: Get the setup commands from GitHub<\/h2>\n<ol>\n<li>Open your repository on GitHub and click <strong>Settings<\/strong>.<\/li>\n<li>In the left sidebar, click <strong>Actions \u2192 Runners<\/strong>.<\/li>\n<li>Click <strong>New self-hosted runner<\/strong>.<\/li>\n<li>Choose <strong>Windows<\/strong> and <strong>x64<\/strong>.<\/li>\n<\/ol>\n<p>GitHub shows the exact commands for the current runner version, including a <strong>registration token<\/strong>. The token expires after one hour, so do the next steps straight away.<\/p>\n<h2>Step 4: Download the runner<\/h2>\n<p>Copy the <strong>Download<\/strong> commands from the GitHub page and run them in the same PowerShell window. They look like this, with the current version number in place of <code>X.Y.Z<\/code>:<\/p>\n<pre><code class=\"language-powershell\">Invoke-WebRequest -Uri https:\/\/github.com\/actions\/runner\/releases\/download\/vX.Y.Z\/actions-runner-win-x64-X.Y.Z.zip -OutFile actions-runner-win-x64-X.Y.Z.zip\r\nAdd-Type -AssemblyName System.IO.Compression.FileSystem\r\n[System.IO.Compression.ZipFile]::ExtractToDirectory(\"$PWD\\actions-runner-win-x64-X.Y.Z.zip\", \"$PWD\")<\/code><\/pre>\n<p>Use the commands GitHub shows you rather than these, so you get the right version (GitHub&#8217;s page also includes a checksum check).<\/p>\n<h2>Step 5: Connect the runner and install it as a service<\/h2>\n<p>Run the <strong>Configure<\/strong> command from the GitHub page. It looks like this:<\/p>\n<pre><code class=\"language-powershell\">.\\config.cmd --url https:\/\/github.com\/YOUR-ACCOUNT\/YOUR-REPO --token YOUR-TOKEN<\/code><\/pre>\n<p>It then asks a few questions. Press Enter to accept a default:<\/p>\n<table>\n<thead>\n<tr>\n<th>Question<\/th>\n<th>Suggested answer<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Runner group<\/td>\n<td>Press Enter (default)<\/td>\n<\/tr>\n<tr>\n<td>Runner name<\/td>\n<td>Something clear, e.g. <code>windows-vps-01<\/code><\/td>\n<\/tr>\n<tr>\n<td>Additional labels<\/td>\n<td><code>windows-vps<\/code> (used to send jobs to this runner)<\/td>\n<\/tr>\n<tr>\n<td>Work folder<\/td>\n<td>Press Enter (<code>_work<\/code>)<\/td>\n<\/tr>\n<tr>\n<td><strong>Run as service?<\/strong><\/td>\n<td><strong>Y<\/strong>, so it starts automatically after reboots<\/td>\n<\/tr>\n<tr>\n<td>User account for the service<\/td>\n<td>Press Enter for the default, or enter a dedicated account (see <a href=\"#security\">Security<\/a>)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Choosing <strong>Y<\/strong> for the service matters. According to GitHub&#8217;s documentation, if you configure the runner without the service option, you have to remove it and configure it again to add it later.<\/p>\n<h2>Step 6: Check the runner is online<\/h2>\n<p>On the VPS:<\/p>\n<pre><code class=\"language-powershell\">Get-Service \"actions.runner.*\"<\/code><\/pre>\n<p>The status should be <strong>Running<\/strong>. On GitHub, refresh <strong>Settings \u2192 Actions \u2192 Runners<\/strong>. Your runner should appear with the status <strong>Idle<\/strong>, meaning it&#8217;s ready for jobs.<\/p>\n<h2>Step 7: Send a workflow to your runner<\/h2>\n<p>Create <code>.github\/workflows\/windows-build.yml<\/code> in your repository:<\/p>\n<pre><code class=\"language-yaml\">name: Build on Windows VPS\r\n\r\non:\r\n  push:\r\n    branches: [ main ]\r\n  workflow_dispatch:\r\n\r\njobs:\r\n  build:\r\n    runs-on: [self-hosted, windows, windows-vps]\r\n    steps:\r\n      - uses: actions\/checkout@v4\r\n\r\n      - name: Check the build machine\r\n        shell: powershell\r\n        run: |\r\n          git --version\r\n          $PSVersionTable.PSVersion\r\n          Get-ComputerInfo -Property OsName, CsTotalPhysicalMemory<\/code><\/pre>\n<p>The <code>runs-on<\/code> line sends the job to runners with all three labels, which means your VPS. Push the file, then open the <strong>Actions<\/strong> tab to watch it run. Replace the last step with your real build commands, such as <code>dotnet build<\/code> or <code>msbuild<\/code>.<\/p>\n<p><strong>Tip:<\/strong> use <code>shell: powershell<\/code> (Windows PowerShell, built in). <code>shell: pwsh<\/code> only works if you&#8217;ve installed PowerShell 7 on the VPS.<\/p>\n<h2>Keeping the runner running<\/h2>\n<ul>\n<li><strong>Reboots:<\/strong> the service starts automatically. Unlike desktop apps, a runner service doesn&#8217;t need anyone signed in.<\/li>\n<li><strong>Runner updates:<\/strong> the runner updates itself automatically when GitHub releases new versions.<\/li>\n<li><strong>Windows Update:<\/strong> keep the VPS patched, and schedule restarts for a quiet time so they don&#8217;t interrupt builds.<\/li>\n<li><strong>Disk space:<\/strong> build folders under <code>C:\\actions-runner\\_work<\/code> grow over time. Clean them up regularly, or add a cleanup step to your workflows.<\/li>\n<li><strong>Start, stop and check the service:<\/strong><\/li>\n<\/ul>\n<pre><code class=\"language-powershell\">Start-Service \"actions.runner.*\"\r\nStop-Service \"actions.runner.*\"\r\nGet-Service \"actions.runner.*\"<\/code><\/pre>\n<h2 id=\"security\">Security<\/h2>\n<ul>\n<li><strong>Private repositories only.<\/strong> GitHub recommends using self-hosted runners only with private repositories. In a public repository, anyone could open a pull request that runs their own code on your runner.<\/li>\n<li><strong>Use a dedicated machine.<\/strong> Don&#8217;t run the runner on a server that also hosts your live website or database. A separate VPS keeps build jobs away from production.<\/li>\n<li><strong>Use a limited service account.<\/strong> The default service account has limited rights. If your builds need more access, create a dedicated Windows user with only the permissions required, rather than using an administrator account.<\/li>\n<li><strong>Keep secrets in GitHub.<\/strong> Store passwords and API keys as GitHub Actions secrets, not in files on the VPS.<\/li>\n<li><strong>Lock down Remote Desktop.<\/strong> Use a strong password and, if possible, allow Remote Desktop only from your own IP address. The runner itself only needs outgoing connections to GitHub.<\/li>\n<\/ul>\n<h2>Troubleshooting<\/h2>\n<table>\n<thead>\n<tr>\n<th>Problem<\/th>\n<th>Likely cause<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Job stuck on &#8220;Waiting for a runner to pick up this job&#8221;<\/td>\n<td>The labels in <code>runs-on<\/code> don&#8217;t match the runner&#8217;s labels, or the runner is offline<\/td>\n<td>Compare the labels in Settings \u2192 Actions \u2192 Runners with your workflow. Check the service is running.<\/td>\n<\/tr>\n<tr>\n<td>Runner shows as Offline<\/td>\n<td>The service has stopped, or the VPS restarted and the service failed to start<\/td>\n<td>Run <code>Start-Service \"actions.runner.*\"<\/code> and check its status.<\/td>\n<\/tr>\n<tr>\n<td>&#8220;Token expired&#8221; during setup<\/td>\n<td>The registration token is only valid for one hour<\/td>\n<td>Open New self-hosted runner again to get a fresh token.<\/td>\n<\/tr>\n<tr>\n<td>A tool isn&#8217;t found in the build, but works when you&#8217;re signed in<\/td>\n<td>The service started before the tool was installed, or the tool was installed only for your user<\/td>\n<td>Restart the service after installing tools, and install them for all users.<\/td>\n<\/tr>\n<tr>\n<td>&#8220;Access denied&#8221; errors in a build<\/td>\n<td>The service account can&#8217;t reach a folder or resource<\/td>\n<td>Give the service account access to that folder, or reconfigure the runner with a dedicated account.<\/td>\n<\/tr>\n<tr>\n<td>&#8220;pwsh is not recognized&#8221;<\/td>\n<td>PowerShell 7 isn&#8217;t installed<\/td>\n<td>Use <code>shell: powershell<\/code>, or install PowerShell 7.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>Removing the runner<\/h2>\n<p>On GitHub, open the runner under <strong>Settings \u2192 Actions \u2192 Runners<\/strong> and click <strong>Remove<\/strong> to get a removal command with its own token. Then on the VPS:<\/p>\n<pre><code class=\"language-powershell\">Set-Location C:\\actions-runner\r\nStop-Service \"actions.runner.*\"\r\n.\\config.cmd remove --token YOUR-REMOVAL-TOKEN<\/code><\/pre>\n<h2>Choosing a VPS for your runner<\/h2>\n<p>For build machines, memory, CPU and disk speed matter more than location, since the runner only talks to GitHub. Choose a plan that fits your builds: <span style=\"color: #000000;\"><a href=\"https:\/\/www.winserver.net\/development\" style=\"color: #000000;\">4 GB for light work, 8 GB or more for larger .NET or C++ projects<\/a><\/span>. If your team or the servers your builds connect to are in Japan or East Asia, a VPS in the same region keeps those connections fast.<\/p>\n<h2>FAQ<\/h2>\n<h3>Is a self-hosted runner free?<\/h3>\n<p>GitHub announced a per-minute fee for self-hosted runners in December 2025, then postponed it. Check GitHub&#8217;s billing documentation for the current position. You pay for the VPS either way.<\/p>\n<h3>Can I use a self-hosted runner with a public repository?<\/h3>\n<p>You can, but GitHub recommends against it, because anyone could run code on your machine through a pull request. Use GitHub-hosted runners for public repositories.<\/p>\n<h3>Can I run more than one runner on one VPS?<\/h3>\n<p>Yes. Configure each runner in its own folder (for example <code>C:\\actions-runner-2<\/code>). Make sure the VPS has enough memory and CPU for builds running at the same time.<\/p>\n<h3>Do I need Remote Desktop after setup?<\/h3>\n<p>Only for maintenance, such as installing new tools. Once the runner is installed as a service, it works without anyone signed in.<\/p>\n<h3>Windows Server or Windows 11?<\/h3>\n<p>Windows Server is the standard choice for a VPS and works well for most builds. If you specifically need to test on Windows 11, that&#8217;s a separate setup.<\/p>\n<h3>How is this different from running Claude Code on a VPS?<\/h3>\n<p>Both use an always-on Windows VPS, but a runner works as a background service and needs no one signed in, while interactive tools need a signed-in session. See our guide to <span style=\"color: #0189ff;\"><strong><a href=\"https:\/\/www.winserver.net\/blog\/claude-code-vps\/\" style=\"color: #0189ff;\">running Claude Code 24\/7 on a Windows VPS<\/a>.<\/strong><\/span><\/p>\n<section class=\"winserver-cta-section\">\n<h2>Need a Windows VPS for your builds?<\/h2>\n<p>Winserver offers Windows Server VPS plans from our data center in Osaka, Japan, with full administrator access and plans from 2 GB up. Compare plans to find the right size for your build machine.<\/p>\n<div class=\"winserver-cta-button-wrapper\"><a href=\"https:\/\/www.winserver.net\/#pricing\" class=\"winserver-cta-button\" target=\"_blank\" rel=\"noopener\" style=\"color: #ffffff;\">Compare Windows VPS Plans<\/a><\/div>\n<\/section>\n<h2>Sources<\/h2>\n<ul>\n<li><a href=\"https:\/\/docs.github.com\/en\/actions\/hosting-your-own-runners\/adding-self-hosted-runners\" target=\"_blank\" rel=\"noopener\">GitHub Docs: Adding self-hosted runners<\/a><\/li>\n<li><a href=\"https:\/\/docs.github.com\/en\/actions\/how-tos\/manage-runners\/self-hosted-runners\/configure-the-application\" target=\"_blank\" rel=\"noopener\">GitHub Docs: Configuring the self-hosted runner application as a service<\/a><\/li>\n<li><a href=\"https:\/\/www.crosstecch.com\/blog\/self-hosted-github-actions-runners-real-cost\" target=\"_blank\" rel=\"noopener\">CrossTecch: GitHub reversed its self-hosted runner fee (pricing history)<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Last updated: 2026\/10\/30 If you build Windows software with GitHub Actions, such as .NET apps, desktop tools or Windows installers, you need a Windows machine to run your builds. GitHub&#8217;s own Windows runners are charged by the minute and start from scratch every time. A self-hosted runner on a Windows VPS gives you an always-on [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1445,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"swell_btn_cv_data":"","footnotes":""},"categories":[372],"tags":[386,387,385,377,8,38],"_links":{"self":[{"href":"https:\/\/www.winserver.net\/blog\/wp-json\/wp\/v2\/posts\/1442"}],"collection":[{"href":"https:\/\/www.winserver.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.winserver.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.winserver.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.winserver.net\/blog\/wp-json\/wp\/v2\/comments?post=1442"}],"version-history":[{"count":1,"href":"https:\/\/www.winserver.net\/blog\/wp-json\/wp\/v2\/posts\/1442\/revisions"}],"predecessor-version":[{"id":1443,"href":"https:\/\/www.winserver.net\/blog\/wp-json\/wp\/v2\/posts\/1442\/revisions\/1443"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.winserver.net\/blog\/wp-json\/wp\/v2\/media\/1445"}],"wp:attachment":[{"href":"https:\/\/www.winserver.net\/blog\/wp-json\/wp\/v2\/media?parent=1442"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.winserver.net\/blog\/wp-json\/wp\/v2\/categories?post=1442"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.winserver.net\/blog\/wp-json\/wp\/v2\/tags?post=1442"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}