Meta: Absolutely superb readme for an open-source project. Can anyone point to a better opensource readme today?
That said, do many see value is in this? The current stance is you either self-host or accept paying github's runner rates out of laziness. In the end it's all very mild when it comes to extra costs.
When things go wrong, you need people going through logs working out that there's nothing actually wrong with CI and in the end finding "oh it was a re-provisioning of compute", so...let's run it again and hope that goes well, actually no...let's schedule it for 11pm Eastern which probably isn't that optimal.
I can see it working for some, but horrendous for others. Maybe if you have burstable CI loads it's worth doing and re-rolling on failure.
Still, a marvellous readme. If only others in open-source would take note that this is how you do it.
Cost isn’t the only vector. Someone may want to host the runners within a given VPC so they have access to private resources / PrivateLink etc. the job API is a lot narrower of an attack surface area than giving your auth info to GitHub that has enough power to do CI/CD infra changes and such.
There’s also people who use GitHub Enterprise who still need to host a runner fleet, so they could benefit from this too.
I agree usually “CI is running” means “human is sorta blocked waiting on this”* means the compute cost to run it reliably is probably negligible. Whether you this on the Github network or your own private cloud is then an orthogonal concern. Spot is silly although spot with immediate fallback to in demand might be a neat hack.
There's also the ability to pre-provision your runners with all of your base container images. Base images don't change that often and can be large. At an AI startup I worked at, self-hosting runners that used our own AMIs saved us a lot of time and egress costs, because so many CI steps begin by pulling some image, whether it be the base image or one you built at a previous step and pushed to your container registry. We also ran CI steps on some projects that required ARM or GPU. You also have the option of keeping a runner up during the busiest part of the day to benefit from cached images. We'd automatically rebuild the runner AMIs once a week and set runner lifetime to max 24 hours, ensuring freshness and ensuring that we didn't run into disk space issues.
We were baking AMI’s like this for a little while but our current setup is quite even fancier - you can do everything in containers and then a distributed in cluster buildkit cache and registry be hosted and backed by S3, and not only do you get a remote reusable cache that syncs extremely fast but you no longer have to rely on external artifact registries being flaky, which if you pull from Dockerhub frequently know this is an actual significant problem.
We actually looked at using this project from OP at the beginning of our ci journey but eventually decided on actions-runner-controller due to its higher level of robustness and configurabiltiy
We use Dagger’s implementation. The basic approach is to have a buildkit engine run as a daemonset on the cluster, and clients specify the same docker socket that buildkit uses. The magic is in cache synchronization, eg only lazily pulling layers as the client requests them. This is scalable but obviously since caching is hard there are some complexities with efficient synchronization of cache layers and cache volumes. This is currently a long lived service that runs as a deployment alongside a bunch of ephemeral runners to manage the cache synchronization.
There are several other different architectures that range from simpler to more complex. The architecture I recommend people start out with is a single long lived beefy buildkit instance that a bunch of runners share, since that is much much simpler to implement. It of course has the downside that you have to refresh/rebuild the cache if the instance ever goes down. For runs that need read/write locks on volumes (eg Gradle build cache) my recommendation after trial and error to rsync those to the runners and then rsync them back after the run completes so you don’t have a bunch of locks fighting each other for the same folder.
Add network egress controls and telemetry as two more benefits of self hosted runners. Denied network connections are a sign there could be a problem with a dependency.
Absolutely, there are lots of workloads that do not run on actions and require custom hardware (GPU? Big boxes) or greater privileges to Linux API than actions provide, or just better caching.
You even need to self-host if you want to test code that uses AVX512 outside of an emulator since the default runners do not support that. Same if you want to test aarch64-specific code paths on Linux, Windows, or macOS.
GitHub don't host runners for Linux × arm64, so if you need this, you need to self-host. You can also run custom AMIs with pre-installed packages, which can speed up workflows that depend on those packages.
> When things go wrong, you need people going through logs working out that there's nothing actually wrong with CI…
I'm on a small team who've been running the Philips Lab self-hosted runners for the past year. It hasn't been difficult to operate. Once deployed, it pretty much "just works".
In my experience, the things that go wrong originate from the GitHub workflows themselves. We usually have to review workflow logs regardless of whether the workflow uses a self-hosted runner or not.
> Maybe if you have burstable CI loads it's worth doing and re-rolling on failure.
We have extremely bursty CI loads. One push can kick off up to 60 different CI jobs. If a few people push at the same time, we easily have 300 jobs running in parallel. This happens the whole day long.
It absolutely makes sense to self host this, since the cost to run this on Github runners would be prohibitive (Github runners are 8x the cost of the equivalent AWS instance I think). All our runners are ephemeral, so we only pay for them when they are actually running jobs. After all the jobs are over, the runners are immediately scaled to zero.
I build this whole thing myself, so it's a bit sad to find someone had already built the whole thing before.
I am in the process of setting it up on a cheap Hetzner box. If it works, would be a great deal! You can get a 64 GB RAM box for 35 EUR/mo at server auctions with unlimited traffic. I don't mention CPU or GPU, as typically this isn't a bottleneck for my projects.
Plus, I can configure cache sharing via host-mounted dir. E.g. pnpm cache can be all in one place, and be locally available to pods via a mounted dir. Same for the Docker image cache. This would speed up CI runs and also reduce network traffic by a huge margin.
GitHub Actions effectively has no local caching. There's an action for caching, but it uses a blob storage for cache artifacts. Which then gets network fetched, gzip'ed and gunzip'ed each time, and from my experience this has never been a gain for medium to large npm projects, as they have thousands of small .js files in node_modules, and thus takes a long time to compress and decompress. I think npm edge cache servers are already so optimized and fast, that in my experience almost always it's faster to install from npm directly. I even tested this on AWS, where the cache was stored in S3, in the same region as CodeBuild (CI), and direct installs from npm were still faster by about 30%.
So other than adding more hardware resources, local caching is the only way to significantly speed up GH Actions, from my experience, and thus you must have your runner.
Hey there, we offer Ubicloud Runners that are 10x cheaper than GitHub and bill them by the minute. You get a fresh VM with each job; and we use Hetzner as our underlying provider.
If you're already setting up a Hetzner box, I'd love to get your input. Any thoughts or feedback for us?
Does it address my main issue of cache sharing? If cache is still over the network, then it's not optimal, as it's the bottleneck for everything. E.g. our node_modules is 3 GB, and once you gzip and gunzip it takes minutes. With pnpm cache store we can reduce this to seconds.
I think you're confusing the unit in our pricing. We charge 0.16¢/min for 4 vCPU and 16 GB, which is $0.0016/min. That's 5x cheaper than BuildJet's pricing.
We didn't quote our prices in $ because the number of trailing zeros confused people. Maybe still go ahead and switch back to that?
The industry standard is $/min, so yes I think you should stick to it. And emphasize that it's 10x less than github actions pricing (most competitors are 2x cheaper instead)
In the past, I used GHCR for (large) Docker layer image caching in CI. Now, I’m using the GHA cache. For either case, it really doesn’t feel like I’m gaining that much, because of the time required to pull from the cache. Really unfortunate.
The only frustrating part of this project is GitHub actions itself. It could be so much better (https://news.ycombinator.com/item?id=37612420). Part of the hassle is figuring out why on earth actions/checkout stopped working in your self-hosted infrastructure... Why you suddenly have what once were ephemeral SQL containers persisting and blocking specific ports in subsequent runs... And why that sweet python provisioning action isn't doing its job anymore...
It all comes down to the instances. GitHub runners are insanely complex and also the best part of actions, along with community driven reusable workflows. They not only pack so much stuff without conflict, they also spin up really fast (forgot which virtualization infrastructure they use). So when you move to self-hosted you now have instances that take longer to be ready, increasing cold start times from pipelines, that will require that you install everything you need in it to run your jobs, and that, depending on how you set them, will require more thorough cleanup after each job.
The scalability is really good though, and all the benefits already cited, like having access to private resources on your VPC, and delegating permissions to instance profile through IAM roles, make this project a godsend.
We maintain a little action called Action-Debugger that let's you ssh into a running GitHub action workflow to help debug pesky issues. It has a few additional features when you use it with our runners but works very well by itself.
If you are interested in having self-hosted auto scaling github actions runners but don't want to manage your own webhook handling infrastructure, https://cirun.io is worth a look.
I was using it for my project because the workflows required access to VPC resources (the database, etc). It worked... ok. There would be times when the runner didn't start, or parallel runs would not work, felt a bit random. Possibly was my config, there were quite a few knobs and dials.
Eventually I stumbled on the idea of running the VPC-requiring commands from an AWS CodeBuild script, and invoking it from a workflow executed on a GitHub-owned runner. Works beautifully and I was able to remove a ton of complexity from my infra that this Terraform module adds.
My experience is that it works until it does not, and then you are down on a rabbit hole trying to figure out why.
The only reason we keep using it is because we have other priorities, but once we have more spare time, this module is going to hell, it's not reliable at all.
I had a similar experience with ARC (actions-runner-controller).
One of the machines in the fleet failed to sync its clock via NTP. Once a job X got scheduled to it, the runner pod failed authentication due to incorrect clock time, and then the whole ARC system started to behave incorrectly: job X was stuck without runners, until another workflow job Y was created, and then X got run but Y became stuck. There were also other wierd behaviors like this so I eventually rebuilt everything based on VMs and stopped using ARC.
Using VMs also allowed me to support the use of the official runner images [0], which is good for compatibility.
I feel more people would benefit from managed "self-hosted" runners, so I started DimeRun [1] to provide cheaper GHA runners for people who don't have the time/willingness to troubleshoot low-level infra issues.
It's only really usable for anything that doesn't involve secrets, I'd be very concerned using anything third party in CI, let alone the runner itself. Supply chain attack senses tingling :).
Yes I totally understand the concern. We are actively working on SOC 2 and other compliance stuff to help with this. But honestly I feel the compliance requirements are weaker than what we actually implemented. For example proper secure boot and whole disk encryption (without sacrificing performance) are mandatory in our mindset but these specific things don't get reflected in compliance.
Instead of being a service, I'm also open to sell the software+hardware solution behind it, so you can have it on-prem. Do you think that's something you would consider given the constraints on supply chain security?
We're too small for on-prem services, so not your target market, just shared my 2c as someone who had been burned by self-hosting github runners too many a time.
I'm not sure why you had that experience. We just saw issues with no spot availability for the instance types we needed but you could get that info from the lambda logs. Most debugging could come from there.
With Karpenter becoming an official EKS add on, I’m really hoping the new “scalesets” gets the same treatment and then we’re somewhat close to a an out of the box ci on EKS experience.
Right now I’m using Karpenter, ARC, EFS and buildkit and it’s great, but it was also like a month of setup and is nontrivially complex.
CodeBuild can be a bit slow to start sometimes. It also doesn't support Docker on Windows builds and it's limited to 8 hours per run.
Shameless plug: I wrote a CDK construct that lets you create runners on demand in response to a GitHub webhook (so basically what was suggested here only with handling of more corner cases that came up). It lets you start runners in CodeBuild, Fargate, ECS, EC2, or Lambda so you can pick whichever works best for you. There is a table in the readme that shows the difference between them and why you might want to choose one over the other.
https://github.com/CloudSnorkel/cdk-github-runners
This is super cool. Private runners are often so much cheaper, especially in large teams. Comes with a "fat tail" of reliability risk though: taking maintenance of runners means that that say 0.01% of builds would be failing for unknown reasons and it'd take significant effort to fix those. But then again that'd probably be only relevant in large organisations which likely have bigger bottlenecks in their devops practice than that; and if that's just about cost then a no-brainer.
56 comments
[ 3.2 ms ] story [ 126 ms ] threadIt’s funny because just the other day I thought about implementing the same for GitLab in order to learn more about terraform.
(Note: I work for GitLab, but as a Frontend Engineer and usually not on CI topics)
We're using this Philips Labs module at $dayjob. It's a great piece of work!
That said, do many see value is in this? The current stance is you either self-host or accept paying github's runner rates out of laziness. In the end it's all very mild when it comes to extra costs.
When things go wrong, you need people going through logs working out that there's nothing actually wrong with CI and in the end finding "oh it was a re-provisioning of compute", so...let's run it again and hope that goes well, actually no...let's schedule it for 11pm Eastern which probably isn't that optimal.
I can see it working for some, but horrendous for others. Maybe if you have burstable CI loads it's worth doing and re-rolling on failure.
Still, a marvellous readme. If only others in open-source would take note that this is how you do it.
There’s also people who use GitHub Enterprise who still need to host a runner fleet, so they could benefit from this too.
* Dependabot and AI PRs might be exceptions.
We actually looked at using this project from OP at the beginning of our ci journey but eventually decided on actions-runner-controller due to its higher level of robustness and configurabiltiy
There are several other different architectures that range from simpler to more complex. The architecture I recommend people start out with is a single long lived beefy buildkit instance that a bunch of runners share, since that is much much simpler to implement. It of course has the downside that you have to refresh/rebuild the cache if the instance ever goes down. For runs that need read/write locks on volumes (eg Gradle build cache) my recommendation after trial and error to rsync those to the runners and then rsync them back after the run completes so you don’t have a bunch of locks fighting each other for the same folder.
You even need to self-host if you want to test code that uses AVX512 outside of an emulator since the default runners do not support that. Same if you want to test aarch64-specific code paths on Linux, Windows, or macOS.
GitHub don't host runners for Linux × arm64, so if you need this, you need to self-host. You can also run custom AMIs with pre-installed packages, which can speed up workflows that depend on those packages.
> When things go wrong, you need people going through logs working out that there's nothing actually wrong with CI…
I'm on a small team who've been running the Philips Lab self-hosted runners for the past year. It hasn't been difficult to operate. Once deployed, it pretty much "just works".
In my experience, the things that go wrong originate from the GitHub workflows themselves. We usually have to review workflow logs regardless of whether the workflow uses a self-hosted runner or not.
Is this not possible via QEMU like everyone has been doing for a very long time now?
The real issue here seems to be large companies wanting their own syntactical turf everyone abides by that they can later profit from.
We have extremely bursty CI loads. One push can kick off up to 60 different CI jobs. If a few people push at the same time, we easily have 300 jobs running in parallel. This happens the whole day long.
It absolutely makes sense to self host this, since the cost to run this on Github runners would be prohibitive (Github runners are 8x the cost of the equivalent AWS instance I think). All our runners are ephemeral, so we only pay for them when they are actually running jobs. After all the jobs are over, the runners are immediately scaled to zero.
I build this whole thing myself, so it's a bit sad to find someone had already built the whole thing before.
https://github.com/actions/actions-runner-controller
I think Kubernetes is a better platform than EC2 for runners, it's faster and more integrated with your tooling (if you're using Kubernetes).
I am in the process of setting it up on a cheap Hetzner box. If it works, would be a great deal! You can get a 64 GB RAM box for 35 EUR/mo at server auctions with unlimited traffic. I don't mention CPU or GPU, as typically this isn't a bottleneck for my projects.
Plus, I can configure cache sharing via host-mounted dir. E.g. pnpm cache can be all in one place, and be locally available to pods via a mounted dir. Same for the Docker image cache. This would speed up CI runs and also reduce network traffic by a huge margin.
GitHub Actions effectively has no local caching. There's an action for caching, but it uses a blob storage for cache artifacts. Which then gets network fetched, gzip'ed and gunzip'ed each time, and from my experience this has never been a gain for medium to large npm projects, as they have thousands of small .js files in node_modules, and thus takes a long time to compress and decompress. I think npm edge cache servers are already so optimized and fast, that in my experience almost always it's faster to install from npm directly. I even tested this on AWS, where the cache was stored in S3, in the same region as CodeBuild (CI), and direct installs from npm were still faster by about 30%.
So other than adding more hardware resources, local caching is the only way to significantly speed up GH Actions, from my experience, and thus you must have your runner.
Hey there, we offer Ubicloud Runners that are 10x cheaper than GitHub and bill them by the minute. You get a fresh VM with each job; and we use Hetzner as our underlying provider.
If you're already setting up a Hetzner box, I'd love to get your input. Any thoughts or feedback for us?
https://www.ubicloud.com/use-cases/github-actions
https://github.com/ubicloud/ubicloud/blob/main/routes/web/we... (our github actions integration is also openly available)
We use BuildJet now and it's $0.008/min for 4 vCPU and 16 GB, while your offer is 0.16¢/min for the same (20x more expensive).
We didn't quote our prices in $ because the number of trailing zeros confused people. Maybe still go ahead and switch back to that?
It all comes down to the instances. GitHub runners are insanely complex and also the best part of actions, along with community driven reusable workflows. They not only pack so much stuff without conflict, they also spin up really fast (forgot which virtualization infrastructure they use). So when you move to self-hosted you now have instances that take longer to be ready, increasing cold start times from pipelines, that will require that you install everything you need in it to run your jobs, and that, depending on how you set them, will require more thorough cleanup after each job.
The scalability is really good though, and all the benefits already cited, like having access to private resources on your VPC, and delegating permissions to instance profile through IAM roles, make this project a godsend.
https://docs.warpbuild.com/tools/action-debugger
https://github.com/WarpBuilds/action-debugger
Eventually I stumbled on the idea of running the VPC-requiring commands from an AWS CodeBuild script, and invoking it from a workflow executed on a GitHub-owned runner. Works beautifully and I was able to remove a ton of complexity from my infra that this Terraform module adds.
My experience is that it works until it does not, and then you are down on a rabbit hole trying to figure out why.
The only reason we keep using it is because we have other priorities, but once we have more spare time, this module is going to hell, it's not reliable at all.
One of the machines in the fleet failed to sync its clock via NTP. Once a job X got scheduled to it, the runner pod failed authentication due to incorrect clock time, and then the whole ARC system started to behave incorrectly: job X was stuck without runners, until another workflow job Y was created, and then X got run but Y became stuck. There were also other wierd behaviors like this so I eventually rebuilt everything based on VMs and stopped using ARC.
Using VMs also allowed me to support the use of the official runner images [0], which is good for compatibility.
I feel more people would benefit from managed "self-hosted" runners, so I started DimeRun [1] to provide cheaper GHA runners for people who don't have the time/willingness to troubleshoot low-level infra issues.
[0]: https://github.com/actions/runner-images [1]: https://dime.run
If something fails and you don't have idle runners (hence wasting unnecessary resources), things start to snowball.
Instead of being a service, I'm also open to sell the software+hardware solution behind it, so you can have it on-prem. Do you think that's something you would consider given the constraints on supply chain security?
Right now I’m using Karpenter, ARC, EFS and buildkit and it’s great, but it was also like a month of setup and is nontrivially complex.
1. A codebuild job running a container image which starts a runner with custom labels
2. A lambda to receive the webhook from github and run the codebuild job on demand.
What would implementing all the infra give you in terms of benefits over codebuild?
The readme doesn't mention anything about codebuild that I can see.
Shameless plug: I wrote a CDK construct that lets you create runners on demand in response to a GitHub webhook (so basically what was suggested here only with handling of more corner cases that came up). It lets you start runners in CodeBuild, Fargate, ECS, EC2, or Lambda so you can pick whichever works best for you. There is a table in the readme that shows the difference between them and why you might want to choose one over the other. https://github.com/CloudSnorkel/cdk-github-runners
It will be in production with a lot of usage soon so I can evaluate how it scales.