In this follow-up post about slow GitHub Actions workflows on Windows hosted runners, I'll walk through a virtual hard disk (VHDX) caching technique that can drastically cut your Windows CI times.
GitHub Actions Windows Runners: VHDX Caching to the Rescue
Remember when I wrote about how slow GitHub's Windows hosted runners can be? I've been tinkering with the VHDX approach I mentioned, and I wanted to share it. In this follow-up post, I'll walk through a practical implementation that can drastically cut your Windows build times.
TL;DR
- VHDX caching can significantly speed up GitHub Actions Windows workflows
- By storing pre-installed tools in a VHDX file and caching it between runs, you avoid reinstalling everything
- I've created a sample repo that demonstrates this technique
The Problem, Revisited
In my previous post, I wrote how Windows runners suffer from I/O bottlenecks, especially on the C: drive backed by Azure remote storage. We explored some mitigations like moving I/O operations to the D: drive (faster, but temporary storage) and tweaking .NET builds via environment variables. But what if we could take this a small step further?
The fundamental issue remains: every time a GitHub Actions workflow runs, your workflows may need to download and install the same tools over and over again. This repetitive setup is particularly painful on Windows, where installing development tools is notoriously I/O intensive.
Enter VHDX Caching
Virtual Hard Disks (VHDX) offer an elegant solution to this problem. Here's the general idea:
- Create a VHDX file once
- Install all your tools inside it
- Cache the entire VHDX between workflow runs
- Mount the cached VHDX in subsequent runs
This approach effectively lets you "freeze" your entire development setup environment and skip the lengthy setup process on each run.
The Implementation
I've created a sample repo that showcases this technique using two workflows:
1. The Cache Creation Workflow
The first workflow (cache-vhdx.yml
) handles the heavy lifting:
- name: Create and Mount VHDX
run: |
$Drive = New-VHD -Path D:\dev_drive.vhdx -SizeBytes 50GB -Dynamic |
Mount-VHD -PassThru |
Initialize-Disk -PassThru |
New-Partition -UseMaximumSize |
Format-Volume -FileSystem NTFS -Confirm:$false -Force
$Drive | Get-Partition | Set-Partition -NewDriveLetter 'X'
It then installs common tools like PowerShell 7, Node.js, Git, Visual Studio Build Tools, and .NET SDK directly to the X: drive. Finally, it caches the entire VHDX file for future use:
- name: Save VHDX to Cache
uses: actions/cache/save@v4
with:
path: D:\dev_drive.vhdx
key: vhdx-cache-${{ github.run_id }}
Depending on what software is being installed, this can be time consuming. Ideally, workflows consuming the cached VHDX file will be run much more frequently than needing to recreate a new VHDX file.
2. The Test Workflow
The second workflow (test-cache-vhdx.yml
) is where the magic happens:
- name: Restore VHDX from Cache
uses: actions/cache/restore@v4
with:
path: D:\dev_drive.vhdx
key: vhdx-cache
fail-on-cache-miss: true
- name: Mount VHDX
run: |
$Drive = Mount-VHD -Path D:\dev_drive.vhdx -PassThru
$Drive | Get-Disk | Get-Partition | Where-Object { $_.Type -eq 'Basic' } | Set-Partition -NewDriveLetter 'X'
Just like that, all tools are available without any installation steps!
Real-World Considerations
Before you rush off to implement this in your production workflows, there are some things to keep in mind:
- Tool Compatibility: Not all tools work gracefully with this approach. Some may still require manual installation or repair steps. Test carefully!
- Cache Size Limits: GitHub Actions has a 10GB cache size limit per repository, so you'll need to be mindful of what you include in your VHDX.
- Cache Invalidation: You'll need a strategy for updating tools. I recommend using a specific cache key pattern that includes version information.
- Windows Server 2025: This example uses
windows-2025
runners, which include built-in support for VHDX operations. For older runner versions, you might need additional setup steps. Windows Server 2025 includes WinGet out of the box, so I personally found that very helpful for this approach. - Tool Organization: Think carefully about how you organize tools within the VHDX to make path management simpler.
- Software Bill of Materials (SBOM): Consider generating an SBOM during VHDX creation to track exactly what versions of tools are cached. This becomes necessary for security compliance, as you'll need to know when vulnerabilities are discovered in cached components and when to invalidate your cache. Without proper tracking, you might be using outdated, vulnerable tools without realizing it.
Getting Started
Want to try this yourself? It's surprisingly straightforward:
- Create the two workflow files in your repository
- Run the cache creation workflow once
- Enjoy lightning-fast setup times in all subsequent workflows!
Check out my sample for code examples and instructions.
What's Next?
GitHub, at the time of this writing, has it in their public roadmap to implement a Custom Images feature that allows producing custom VM images that can be used as the base of workflows and developer environments. While the techniques in this article are a clever workaround for now, I welcome this blog post becoming obsolete!
Until next time, happy building! 👋