Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions .env

This file was deleted.

25 changes: 25 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copy to .env and fill in. .env is git-ignored.

# --- Runner registration ---------------------------------------------------
# Org: https://github.com/<org>
# Repo: https://github.com/<org>/<repo>
RUNNER_URL=

# Exactly one of the two. PAT is long-lived (needs "Self-hosted runners: read/write");
# TOKEN is the short-lived registration token from Settings > Actions > Runners.
PAT=
# TOKEN=

# Number of runners registered from this VM
RUNNERS=2

# Optional overrides of the image defaults
# GITHUB_RUNNER_VERSION=2.338.0
# GITHUB_RUNNER_NAME=windows_x64_vagrant
# GITHUB_RUNNER_LABELS=windows,win_x64,windows_x64,windows_vagrant_action

# --- VM sizing -------------------------------------------------------------
# MEMORY in MiB, DISK_SIZE in GiB
MEMORY=8000
CPU=4
DISK_SIZE=100
16 changes: 16 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
groups:
github-actions:
patterns:
- "*"

# Tracks the base image tag in the Dockerfile
- package-ecosystem: docker
directory: /
schedule:
interval: weekly
7 changes: 4 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ on:
release:
types: [published]

env:
TAG_NAME: ${{ github.ref_name }}
permissions:
contents: read
packages: write

jobs:
publish-image:
Expand All @@ -18,7 +19,7 @@ jobs:
sudo rm -rf "$AGENT_TOOLSDIRECTORY"

- name: Checkout current repository
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Docker meta
id: meta
Expand Down
28 changes: 28 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Lint

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
lint:
runs-on: ubuntu-latest
name: lint
steps:
- name: Checkout current repository
uses: actions/checkout@v7

- name: Dockerfile (hadolint)
uses: hadolint/hadolint-action@v3.5.0
with:
dockerfile: Dockerfile

- name: Vagrantfile (Ruby syntax)
run: ruby -c Vagrantfile

- name: Workflows (actionlint)
uses: raven-actions/actionlint@v2
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Local configuration (contains PAT/TOKEN)
.env

# Vagrant state
.vagrant/
26 changes: 12 additions & 14 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
# syntax=docker/dockerfile:1
FROM ghcr.io/vaggeliskls/windows-in-docker-container:latest
FROM ghcr.io/vaggeliskls/windows-in-docker-container:1.0.1

# Github action settings
LABEL org.opencontainers.image.source="https://github.com/vaggeliskls/windows-github-custom-runner" \
org.opencontainers.image.description="Self-hosted GitHub Actions runners on a Windows VM (Vagrant + libvirt) inside a Linux container"

# GitHub Actions runner defaults. All of these can be overridden at runtime via .env;
# the download URL is derived from GITHUB_RUNNER_VERSION inside the Vagrantfile.
ENV GITHUB_RUNNER_VERSION=2.338.0
ENV GITHUB_RUNNER_NAME=windows_x64_vagrant
ENV GITHUB_RUNNER_VERSION=2.334.0
ENV GITHUB_RUNNER_FILE=actions-runner-win-x64-${GITHUB_RUNNER_VERSION}.zip
ENV GITHUB_RUNNER_URL=https://github.com/actions/runner/releases/download/v${GITHUB_RUNNER_VERSION}/${GITHUB_RUNNER_FILE}
ENV GITHUB_RUNNER_LABELS=windows,win_x64,windows_x64,windows_vagrant_action

# Provision elevated and in an interactive session so runners can drive GUI tooling
ENV PRIVILEGED=true
ENV INTERACTIVE=true
ENV DOLLAR=$

RUN rm -rf /Vagrantfile /Vagrantfile.tmp /startup.sh

COPY Vagrantfile /Vagrantfile.tmp
COPY startup.sh /
RUN chmod +x startup.sh

ENTRYPOINT ["/startup.sh"]
CMD ["/bin/bash"]
# Replace the base image's generic Vagrantfile with the runner one.
# The base image's /app/startup.sh (libvirtd startup, KVM/QEMU fallback, graceful halt) is reused as-is.
COPY Vagrantfile /app/Vagrantfile
166 changes: 97 additions & 69 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,96 +1,124 @@
# 🏃 Windows Github Custom Runner
# 🏃 Windows Github Custom Runner

Explore an innovative, efficient, and cost-effective approach to deploying a custom GitHub Runner that runs in a containerized Windows OS (x64) environment on a Linux system. This project leverages the robust capabilities of Vagrant VM, libvirt, and docker-compose which allows for seamless management of a Windows instance just like any Docker container. The added value here lies in the creation of a plug-and-play solution, significantly enhancing convenience, optimizing resource allocation, and integrating flawlessly with existing workflows. This strategy enriches CI/CD pipeline experiences in various dev-ops environments, providing a smooth and comprehensive approach that does not require prior knowledge of VM creation.
Explore an innovative, efficient, and cost-effective approach to deploying a custom GitHub Runner that runs in a containerized Windows OS (x64) environment on a Linux system. This project leverages the robust capabilities of Vagrant VM, libvirt, and Docker Compose, which allows for seamless management of a Windows instance just like any Docker container. The added value here lies in the creation of a plug-and-play solution, significantly enhancing convenience, optimizing resource allocation, and integrating flawlessly with existing workflows. This strategy enriches CI/CD pipeline experiences in various dev-ops environments, providing a smooth and comprehensive approach that does not require prior knowledge of VM creation.

⭐ **Don't forget to star the project if it helped you!**

# 📋 Prerequisites

- [docker](https://www.docker.com/) version 24 or higher.
- [docker-compose](https://www.docker.com/) version 1.18 or higher.
- **Linux host** with hardware virtualization enabled and `/dev/kvm` present.
Check with `lscpu | grep -i Virtualization` (`VT-x` or `AMD-V`). Without KVM the VM falls back to plain QEMU and is several times slower.
- [Docker](https://www.docker.com/) 20 or higher with Compose v2 (`docker compose`).
- `privileged: true` and `cgroup: host` in the compose file — libvirt needs both.

# 🚥 Authentication for Self-Hosted Runners
For the purpose of authenticating your custom self-hosted runners, we offer two viable authentication methods:

1. Personal Access Token (`PAT`) - The Personal Access Token is a static, manually created token that provides secure access to GitHub. This offers a long-lived method of authentication (The PAT token needs Read and Write access to organization self-hosted runners).
Two authentication methods are supported. Set **one** of them in `.env`:

2. Registration Token (`TOKEN`) - The Registration Token is a dynamic, short-lived token generated automatically by GitHub during the creation of a new self-hosted runner. This provides a temporary but immediate method of authentication.

> **Note:** Only one of these authentication methods is necessary. Choose the method that best fits your
1. Personal Access Token (`PAT`) — a long-lived token you create on GitHub. It needs the *Self-hosted runners: Read and write* permission for organization runners (or admin access to the repository for repository runners). The runner only uses it to fetch a registration token; it is not stored in the VM.
2. Registration Token (`TOKEN`) — the short-lived token GitHub shows under *Settings → Actions → Runners → New self-hosted runner*. It expires after one hour, so it must be fresh when the container starts.

# 🚀 Deployment Guide

1. Create/Update the environmental file `.env`
- `PAT`: Personal access token from GitHub
- `TOKEN`: Short lived Github token
- `RUNNER_URL`: The URL of the GitHub that the runner connects to
- `RUNNERS`: Number of runners
- `MEMORY`: Amount of memory for the Vagrant image (in MB)
- `CPU`: Number of CPUs for the Vagrant image
- `DISK_SIZE`: Disk size for the Vagrant image (in GB)

### Example with PAT
```env
# Runner settings
PAT=<Your Personal access token>
RUNNER_URL=<runner url>
RUNNERS=1
# Vagrant image settings
MEMORY=8000 # 8GB
CPU=4
DISK_SIZE=100
```
### Example with TOKEN
```env
# Runner settings
TOKEN=<Your short lived acess token>
RUNNER_URL=<runner url>
RUNNERS=1
# Vagrant image settings
MEMORY=8000 # 8GB
CPU=4
DISK_SIZE=100
```
2. Create `docker-compose.yml`
```yaml
version: "3.9"

services:
windows-github-runner-vm:
image: docker.io/vaggeliskls/windows-github-custom-runner:latest
env_file: .env
stdin_open: true
tty: true
privileged: true
ports:
- 3389:3389
```
3. Run: `docker-compose up -d`
1. Create `.env` from the template and fill it in:

```bash
cp .env.example .env
```

| Variable | Required | Description |
|---|---|---|
| `RUNNER_URL` | yes | Org (`https://github.com/<org>`) or repo (`https://github.com/<org>/<repo>`) the runners register with |
| `PAT` / `TOKEN` | one of | See [Authentication](#-authentication-for-self-hosted-runners) |
| `RUNNERS` | no (`1`) | Number of runners started in the VM |
| `MEMORY` | no (`8000`) | VM memory in MiB |
| `CPU` | no (`4`) | VM vCPUs |
| `DISK_SIZE` | no (`100`) | VM disk in GiB |
| `GITHUB_RUNNER_VERSION` | no | Override the [actions/runner](https://github.com/actions/runner/releases) version baked into the image — no rebuild needed |
| `GITHUB_RUNNER_NAME` | no | Runner name prefix (default `windows_x64_vagrant`) |
| `GITHUB_RUNNER_LABELS` | no | Comma-separated labels (default `windows,win_x64,windows_x64,windows_vagrant_action`) |

2. Create `docker-compose.yml`:

```yaml
services:
windows-github-runner-vm:
image: docker.io/vaggeliskls/windows-github-custom-runner:latest
platform: linux/amd64
hostname: windows-github-runner
env_file: .env
stdin_open: true
tty: true
privileged: true
cgroup: host
restart: unless-stopped
ports:
- 3389:3389 # RDP
- 2222:2222 # SSH
```

3. Run `docker compose up -d` and follow progress with `docker compose logs -f`.

> **First boot takes a long time.** The Windows box is baked into the image, but the VM still has to boot and run the full provisioning (Chocolatey, Visual Studio, Rtools, runner download). Expect 20–40 minutes depending on hardware and bandwidth; the runners register at the very end, once the toolchain is installed.

## Runner names

Runners are registered as `<GITHUB_RUNNER_NAME>_<container hostname>_<n>`, e.g. `windows_x64_vagrant_windows-github-runner_1`. The names are deliberately stable: when the container restarts, `config.cmd --replace` takes over the existing registrations instead of leaving offline duplicates behind in GitHub.

# 🌐 Access via Remote Desktop
For debugging purposes or testing you can always connect to the VM with remote desktop softwares.
If you run more than one container against the same org or repo, give each one a different `hostname:` in its compose file.

Some software that used when developed was
1. Linux: rdesktop `rdesktop <ip>:3389` or [remina](https://remmina.org/)
2. MacOS: [Windows remote desktop](https://apps.apple.com/us/app/microsoft-remote-desktop/id1295203466?mt=12)
3. Windows: buildin `Remote Windows Connection`
# 🧰 What's in the VM

Provisioning installs, on top of the base [Windows Server 2022 box](https://github.com/vaggeliskls/windows-in-docker-container):

- PowerShell 7, Chocolatey, 7-Zip, Git, jq
- Visual Studio 2022 Enterprise with the *Desktop development with C++* workload (`--includeRecommended`)
- Rtools 4.0 with `mingw-w64-x86_64-make`
- Long paths enabled, firewall disabled, `C:` grown to the full virtual disk

> **Licensing:** Visual Studio Enterprise requires a matching subscription for everyone whose builds use this runner. If that does not fit, fork the repo and switch [Vagrantfile](Vagrantfile) to `vs_buildtools.exe` with the `Microsoft.VisualStudio.Workload.VCTools` workload.

# 🌐 Access

For debugging or testing you can connect to the VM directly.

### Remote Desktop (RDP) — port `3389`

| OS | Software |
|---|---|
| **Linux** | [`rdesktop`](https://github.com/rdesktop/rdesktop) → `rdesktop <host>:3389`, or [Remmina](https://remmina.org/) |
| **macOS** | [Windows App](https://apps.apple.com/us/app/windows-app/id1295203466?mt=12) (formerly Microsoft Remote Desktop) |
| **Windows** | Built-in **Remote Desktop Connection** |

### SSH — port `2222`

```bash
ssh vagrant@<host> -p 2222
```

# 🔑 User Login
The default users based on vagrant image are

The default users from the Vagrant box are:

1. Administrator
- Username: Administrator
- Password: vagrant
1. User
- Username: vagrant
- Password: vagrant
- Username: `Administrator`
- Password: `vagrant`
2. User
- Username: `vagrant`
- Password: `vagrant`

# 🛠 Development

- The image only adds a [Vagrantfile](Vagrantfile) on top of [windows-in-docker-container](https://github.com/vaggeliskls/windows-in-docker-container); the container startup script comes from the base image.
- `docker compose build` builds the image locally (the base image is large — it contains the Windows box).
- Pull requests run [lint.yml](.github/workflows/lint.yml): `hadolint` on the Dockerfile, `ruby -c` on the Vagrantfile, `actionlint` on the workflows. Publishing a GitHub release builds and pushes the image to Docker Hub and GHCR via [ci.yml](.github/workflows/ci.yml).
- Dependabot keeps the GitHub Actions and the base image tag current. The runner version is set by `GITHUB_RUNNER_VERSION` in the [Dockerfile](Dockerfile).

# 📚 Further Reading and Resources

- [Windows in docker container](https://github.com/vaggeliskls/windows-in-docker-container)
- [Windows in docker container](https://github.com/vaggeliskls/windows-in-docker-container) (base image)
- [GitHub: self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners)
- [Windows Vagrant Tutorial](https://github.com/SecurityWeekly/vulhub-lab)
- [Vagrant image: peru/windows-server-2022-standard-x64-eval](https://app.vagrantup.com/peru/boxes/windows-server-2022-standard-x64-eval)
- [Vagrant box: peru/windows-server-2022-standard-x64-eval](https://portal.cloud.hashicorp.com/vagrant/discover/peru/windows-server-2022-standard-x64-eval)
- [Vagrant by HashiCorp](https://www.vagrantup.com/)
- [Windows Virtual Machine in a Linux Docker Container](https://medium.com/axon-technologies/installing-a-windows-virtual-machine-in-a-linux-docker-container-c78e4c3f9ba1)
Loading
Loading