How to Configure a GitHub Actions Self-Hosted Runner on Windows Server 2025

GitHub Actions self-hosted runners let you execute workflow jobs on your own infrastructure instead of GitHub’s cloud-hosted runners. For teams building Windows applications, running integration tests against local databases, or working in air-gapped environments, a self-hosted runner on Windows Server 2025 is the natural choice. This guide walks through registering the runner with GitHub, installing it as a Windows service, writing workflows that target the runner, and keeping it maintained over time.

Prerequisites

  • Windows Server 2025 with administrator privileges
  • PowerShell 5.1 or later (PowerShell 7 recommended)
  • Outbound HTTPS access to github.com and api.github.com (port 443)
  • A GitHub repository or organization with Admin access
  • At least 2 GB RAM and 10 GB free disk space for the runner and workspace
  • The required build tools for your project (e.g., Visual Studio Build Tools, .NET SDK, Node.js)

Step 1: Create the Self-Hosted Runner in GitHub

Self-hosted runners can be scoped to a repository, an organization, or an enterprise. For a repository-level runner:

  1. Navigate to your repository on github.com
  2. Click SettingsActionsRunners
  3. Click New self-hosted runner
  4. Select Windows as the operating system and x64 as the architecture

GitHub displays a download URL and a time-limited registration token. Keep this page open — you will need both values in the next step.

Step 2: Download and Extract the Runner Application

Copy the commands shown on the GitHub runner setup page. They look similar to the following — always use the exact URL and token GitHub generates for you, as tokens expire after one hour:

# Create a dedicated directory for the runner
New-Item -ItemType Directory -Path "C:actions-runner" -Force
Set-Location "C:actions-runner"

# Download the runner package (replace with the URL shown on GitHub)
$runnerVersion = "2.323.0"
$downloadUrl = "https://github.com/actions/runner/releases/download/v$runnerVersion/actions-runner-win-x64-$runnerVersion.zip"
Invoke-WebRequest -Uri $downloadUrl -OutFile "actions-runner-win-x64.zip"

# Extract the archive
Expand-Archive -Path "actions-runner-win-x64.zip" -DestinationPath "." -Force

# Remove the archive
Remove-Item "actions-runner-win-x64.zip"

Step 3: Register the Runner with Your Repository

The config.cmd script registers this machine with GitHub. Run it from an elevated PowerShell prompt, substituting the URL and token from the GitHub UI:

# Register the runner (interactive — press Enter to accept defaults)
.config.cmd --url https://github.com/YourOrg/YourRepo --token AXXXXXXXXXXXXXXXXXXXXXXXXX

# Unattended registration with all options specified
.config.cmd `
    --url https://github.com/YourOrg/YourRepo `
    --token AXXXXXXXXXXXXXXXXXXXXXXXXX `
    --name "win2025-runner-01" `
    --labels "self-hosted,windows,x64,dotnet,powershell" `
    --work "_work" `
    --runasservice `
    --windowslogonaccount "NT AUTHORITYNETWORK SERVICE" `
    --unattended

The --labels flag is important: it allows workflows to target this specific runner by capability. The built-in labels self-hosted and windows are always added automatically alongside any custom labels you specify.

Step 4: Install and Start the Runner as a Windows Service

GitHub provides svc.cmd to manage the runner Windows service lifecycle:

# Install the runner as a Windows service
.svc.cmd install

# Start the service
.svc.cmd start

# Verify it is running
.svc.cmd status

# Check via PowerShell
Get-Service -Name "actions.runner.*"

By default the service runs as NT AUTHORITYNETWORK SERVICE. For builds that need elevated privileges or domain credentials, configure a specific service account:

# Stop and uninstall current service
.svc.cmd stop
.svc.cmd uninstall

# Re-install with a specific service account
.svc.cmd install --username "DOMAINsvc-ghrunner" --password "RunnerP@ss2025!"

.svc.cmd start

Step 5: Write a GitHub Actions Workflow Targeting the Windows Runner

Create a workflow file at .github/workflows/windows-build.yml in your repository. Use runs-on: self-hosted to target any self-hosted runner, or use a label array to be more selective:

name: Windows Build and Test

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    # Target any self-hosted Windows x64 runner with the dotnet label
    runs-on: [self-hosted, windows, x64, dotnet]

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up .NET SDK
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'

      - name: Restore dependencies
        shell: powershell
        run: dotnet restore MyApp.sln

      - name: Build
        shell: powershell
        run: dotnet build MyApp.sln --configuration Release --no-restore

      - name: Run tests
        shell: powershell
        run: |
          dotnet test MyApp.TestsMyApp.Tests.csproj `
            --configuration Release `
            --no-build `
            --verbosity normal `
            --logger "trx;LogFileName=test-results.trx" `
            --results-directory "$env:GITHUB_WORKSPACETestResults"

      - name: Upload test results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results
          path: TestResults/*.trx
          retention-days: 14

      - name: Publish
        shell: powershell
        run: |
          dotnet publish MyAppMyApp.csproj `
            --configuration Release `
            --output "$env:GITHUB_WORKSPACEpublish"

      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: windows-build-${{ github.run_number }}
          path: publish

Step 6: Using PowerShell Steps in Workflows

Self-hosted Windows runners support both cmd and powershell shells. Specify shell: powershell (Windows PowerShell 5.1) or shell: pwsh (PowerShell 7+) per step:

      - name: System information (PowerShell 7)
        shell: pwsh
        run: |
          $PSVersionTable
          Get-ComputerInfo | Select-Object WindowsProductName, TotalPhysicalMemory
          Get-PSDrive C | Select-Object Used, Free

      - name: Install NuGet packages using legacy PowerShell
        shell: powershell
        run: |
          Set-ExecutionPolicy RemoteSigned -Scope Process -Force
          nuget restore MyApp.sln -PackagesDirectory packages

      - name: Run custom deployment script
        shell: pwsh
        env:
          DEPLOY_TARGET: ${{ vars.DEPLOY_SERVER }}
          DEPLOY_SECRET: ${{ secrets.DEPLOY_KEY }}
        run: |
          .scriptsDeploy-ToStaging.ps1 `
            -Server $env:DEPLOY_TARGET `
            -ApiKey $env:DEPLOY_SECRET

Step 7: Managing Runner Groups

At the organization level, runner groups let you control which repositories can use which runners. Navigate to Organization → Settings → Actions → Runner groups to create groups and assign runners. For example, create a group named Windows Production restricted to specific repositories that have access to production deployment credentials.

Step 8: Configuring Auto-Update

The runner agent auto-updates by default when GitHub releases a new version. The update process downloads the new version, stops the service, swaps the binary, and restarts — usually completing in under two minutes. To disable auto-update (useful in locked-down environments):

# Edit .runner file to disable auto-update
$runnerConfig = Get-Content "C:actions-runner.runner" | ConvertFrom-Json
$runnerConfig | Add-Member -NotePropertyName "disableUpdate" -NotePropertyValue $true -Force
$runnerConfig | ConvertTo-Json | Set-Content "C:actions-runner.runner"

Step 9: Removing the Runner

When decommissioning a runner, always unregister it from GitHub before deprovisioning the server to avoid stale runners in your runner list:

# Stop the service
.svc.cmd stop

# Uninstall the service
.svc.cmd uninstall

# Unregister from GitHub (generates a new one-time token from GitHub UI or API)
.config.cmd remove --token AXXXXXXXXXXXXXXXXXXXXXXXXX

Conclusion

A self-hosted GitHub Actions runner on Windows Server 2025 brings the full power of the Windows platform — including PowerShell, the Windows SDK, COM interop, and enterprise network access — to your GitHub-managed CI/CD workflows. By running the agent as a Windows service under a dedicated account, labeling runners precisely, and writing workflow files that target those labels, you maintain clean separation between different build environments while keeping operational overhead low. As your pipeline matures, consider grouping runners by environment tier, adding ephemeral runners for sensitive workloads, and integrating with GitHub’s Dependabot to keep runner-installed tool versions current.