Ci Cd Pipeline DevOps Github Productivity Software Development

From Zero to Hero: Setting Up CI/CD with GitHub Actions, Including Your Localhost Server

13 Mar 2025 · Rabington Chitima

Most times, we unknowingly short-change ourselves by avoiding experiences that could set us apart. But here's the thing: hands-on experience with CI/CD is a game-changer. By the end of this guide, not only will you be comfortable discussing continuous integration and deployment, but you'll also have a practical setup running on your own machine — lowering the barrier to entry into the DevOps ecosystem.

For the bigger chunk of my career, Azure DevOps was my go-to tool for pipelines. It's powerful, enterprise-ready, and widely used in corporate environments. But when I started setting up a CI/CD pipeline for my personal projects, GitHub Actions felt more streamlined and well-integrated — especially for repositories already on GitHub. And let's be honest, it's quickly becoming the default for developers working on side projects or open-source contributions.

Who Is This For?

If you've ever found yourself:

  • Copying published files manually from your computer to another machine
  • Deploying directly to a folder (e.g., wwwroot for IIS)
  • Physically carrying files to a client's environment on a USB stick
  • Manually uploading builds to cloud platforms like AWS, Azure, or DigitalOcean
  • SFTP-ing files to a remote server after every update

Then it's time to automate! In this guide, I'll walk you through how I:

  • Set up a self-hosted GitHub Actions runner on my Windows 11 machine
  • Automated builds and deployments to IIS
  • Created a streamlined CI/CD pipeline for local and remote hosting

Let's get started!

Step 1: Setting Up the Self-Hosted Runner

I have an old Windows 11 PC which now works as my dedicated Homelab for a development server and hosts personal stuff. I digress — you don't need 2 computers, this can work using the same machine you develop on.

Since my development server was also my deployment target, I needed a self-hosted runner (GitHub also provides cloud-hosted runners).

Install the GitHub Self-Hosted Runner

  1. Navigate to your GitHub repository → Settings → Actions → Runners.
  2. Click New self-hosted runner (Windows for me), and follow the instructions.
  3. Or, run the following commands in PowerShell to create and navigate to the actions-runner folder in your preferred location:
mkdir actions-runner
cd actions-runner

Download and extract runner files:

Invoke-WebRequest -Uri https://github.com/actions/runner/releases/download/v2.309.0/actions-runner-win-x64-2.309.0.zip -OutFile actions-runner-win-x64-2.309.0.zip

Expand-Archive -Path actions-runner-win-x64-2.309.0.zip -DestinationPath .

Configure the Runner — replace with your details from GitHub (Repo → Settings → Actions → Runners):

.\config.cmd --url https://github.com/YOUR_USERNAME/YOUR_REPO --token YOUR_TOKEN

Start the runner (you can also set it to run as a service so it starts on boot):

.\run.cmd

At this point, my runner was connected, and GitHub could now trigger workflows on my machine. Not necessary for this exercise, but you can also set Secrets and Variables here for things like tokens and DB connections.

Step 2: Preparing IIS for Deployment

For a .NET Core app, the quicker option is IIS:

Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebServer

Then configure IIS: open IIS Manager → Add Website, set the physical path to the folder where the app will be deployed, and assign a port (e.g. 8080).

Step 3: Writing the GitHub Actions Workflow

With the server ready, it was time to automate deployments. I created .github/workflows/deploy.yml in my repository and split the steps into two jobs, Build and Deploy:

name: CI/CD UmbracoSite to IIS

on:
  push:
    branches:
      # git branch you want to trigger the deployment
      - master

jobs:
  build: # First Job
    # Ensure this points to your self-hosted runner
    runs-on: self-hosted
    # If you have set up any Secrets and Variables this is how you can get them
    env:
      DB_CONNECTION_STRING: ${{ secrets.DB_CONNECTION_STRING }}

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

      # Add .NET to PATH for current session
      - name: Manually Add Default .NET Path to GitHub PATH
        shell: powershell
        run: |
          echo "C:\dotnet" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8

      # Install .NET SDK (if not already installed in C:\dotnet)
      - name: Install .NET SDK if not already installed
        shell: powershell
        run: |
          if (-Not (Test-Path "C:\dotnet\dotnet.exe")) {
              Write-Host ".NET SDK not found in C:\dotnet. Installing..."
              Invoke-WebRequest -Uri https://dotnet.microsoft.com/download/dotnet/scripts/v1/dotnet-install.ps1 -OutFile dotnet-install.ps1
              .\dotnet-install.ps1 -InstallDir "C:\dotnet" -Version 8.0.406
              [System.Environment]::SetEnvironmentVariable("DOTNET_ROOT", "C:\dotnet", "Machine")
              [System.Environment]::SetEnvironmentVariable("Path", "C:\dotnet;$([System.Environment]::GetEnvironmentVariable('Path', 'Machine'))", "Machine")
          } else {
              Write-Host ".NET SDK already installed in C:\dotnet"
          }

      - name: Verify .NET Installation
        shell: powershell
        run: dotnet --version

      - name: Restore dependencies
        run: dotnet restore

      # Publish the Project
      - name: Build & Publish
        run: dotnet publish -c Release -o ./publish

      - name: Upload Artifact # we will download this on deployment
        uses: actions/upload-artifact@v4
        with:
          name: umbraco-published
          path: ./publish

  deploy: # Second job
    runs-on: self-hosted # Runs on the IIS server, same as above

    steps:
      - name: Download Artifact # the uploaded files from the previous step
        uses: actions/download-artifact@v4
        with:
          name: umbraco-published
          path: C:\inetpub\wwwroot\Umbraco

      # Restart IIS to Apply Changes
      - name: Restart IIS
        shell: powershell
        run: |
          iisreset /restart
          Write-Host "IIS restarted successfully!"

You can also add your database deployments here to ensure your deployment matches any database changes.

Important Notes

  • Secrets: Make sure DB_CONNECTION_STRING is set in your repository's secrets (or the runner's environment) so it's available during the workflow run.
  • Paths: Adjust the paths (project folder names, output directories) if your solution structure differs.
  • Permissions: Ensure the relevant folders (e.g. C:\inetpub\YourProjectFolder, C:\dotnet) have the appropriate permissions for the IIS application pool or service account running your app.

Step 4: Running the Workflow and Debugging

Once I committed the file and pushed changes, GitHub Actions triggered the workflow. I ran into a few issues along the way:

  • Permissions: My self-hosted runner needed admin privileges, so I ran run.cmd in an elevated PowerShell window.
  • Port Binding Conflicts: IIS sometimes locked the port, so I used net stop was /y to reset IIS.
  • Versions of dependencies: If any dependencies (.NET, Node, Umbraco, etc.) need to be downloaded, ensure the versions match the project.

Once fixed, the pipeline successfully deployed my app after every push to master!

Conclusion

Setting up CI/CD on a Windows 11 developer server using GitHub Actions was a great learning experience. It helped me automate deployments, understand self-hosted runners, and optimize IIS configurations.

If you're also hosting a project locally, this setup will save you time and ensure your application is always up to date with every push!

← Back to blog