GitHub Actions Self Hosted Runners
Deploying GitHub Actions Self Hosted Runners on Railway is an excellent way to run your own CI infrastructure because you only pay for what you use. With self-hosted runners, you also unlock the ability to cache expensive and time-consuming dependencies (node_modules
, cargo
, etc.) or large git repositories. Best of all, Railway's built-in replicas means you can scale your runners horizontally, or even distribute them to different regions with just a click and redeploy. You'll save build times and costs over using standard runners, AND you'll unlock more sophistocated workflows to streamline building your app.
In this guide you'll learn:
- The basics to deploy a GitHub Actions Self Hosted Runner on Railway.
- How to authenticate self-hosted runners on Railway with your GitHub Organization or Enterprise.
- How to scale up replicas to serve bigger Actions workloads.
- Best Practices for configuring your self-hosted runners on Railway.
Quickstart: Deploy your self-hosted Runners with our Railway template.
Deploy a GitHub self-hosted runner on Railway
-
Navigate to the GitHub Actions self-hosted Runner Template. You'll notice the template requires an
ACCESS_TOKEN
. This token, along with ourRUNNER_SCOPE
will determine where our self-hosted runners get registered on GitHub. Thankfully, this template supports self registration of your runners -- which means you can dynamically scale up or down the number of runners you have just by adjusting yourreplicas
! -
Set your
RUNNER_SCOPE
toorg
. We want to set up our self-hosted runners to register with a GitHub Organization, so any repositories within our organization can use the same pool of runners. This is super useful because you don't have to set up permissions for every single repository!
If you have a GitHub Enterprise, you can similarly set up your runners using an ACCESS_TOKEN
, you just need to set your RUNNER_SCOPE
as ent
instead.
If you need additional configuration, then you can simply add a variable to your Service.
Setup a GitHub ACCESS_TOKEN
For this guide, we will create a new GitHub Fine-Grained Personal Access Token. These are modern personal access tokens that obey the principle of least priviledge, making them easy to secure, revoke, and audit!
Note: You need to have Admin access to the organization for which you are making the ACCESS_TOKEN
.
- Create a new fine-grained personal access token. Navigate to your Settings -> Developer Settings -> Personal Access Token -> Fine-grained tokens -> Generate New Token
- Set the Resource owner as your Organization. Alternatively, if you are using a
ent
RUNNER_SCOPE
, select your Enterprise. - Set Expiration
- Under Permissions, Select Organization Permissions -> Self Hosted Runners -> Read and Write (If Enterprise, select Enterprise instead).
- Click Generate. Save your
ACCESS_TOKEN
in a safe place! You won't see it again. (Save it in a Password Vault as an API Key!) - DONE. You don't need any other permissions!
Scaling up your Railway Self Hosted Runners
- Navigate to the Settings tab of your Service to the Region area.
- Change the number next to your region from
1
to your desired number of replicas. - Click Deploy.
- Done! Your new replicas will automatically spin up and register themselves with GitHub.
View Your Registered Self Hosted Runners
You can view all your runners by navigating to your organization's Actions -> Runners page at https://github.com/organizations/(your-organization-name)/settings/actions/runners?page=1
Routing Actions Jobs
You can route jobs by simply changing the LABELS
variable. By default, we include the railway
label on runners you make through the Template. LABELS
is a comma (no spaces) delimited list of all the labels you want to appear on that runner. This enables you to route jobs with the specificity that your workflows need, while still allowing you to make runners available for your entire Organization.
Setting up GitHub Actions workflows for Pull Requests
GitHub Actions uses workflow files located in .github/workflows/<workflow>.yml
. You can easily incorporate pre-built steps to get up-and-running quickly.
-
When you want to run a workflow every time a pull request is opened, set the
on
key topull_request
in your.github/workflows/<workflow>.yml
. -
Set the
runs-on
key when you want to route your workflow job to a particular runner. Use a comma delimited list for greater specificity. For example, a[self-hosted, linux, x86, railway]
workflow needs to match all labels to an appropriate runner in order to route the job correctly.
Example GitHub Actions Workflow
If you've never made a workflow before, here is a basic out-of-the-box example of a NuxtJS project using Bun to execute an eslint
check.
name: eslint check
on:
pull_request:
branches:
- main
permissions:
contents: read
jobs:
build:
name: Check
runs-on: [self-hosted, railway]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install bun
uses: oven-sh/setup-bun@v2
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Cache Files
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
path: ${{ github.workspace }}/**/node_modules
path: ${{ github.workspace }}/**/.nuxt
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock', 'nuxt.config.ts', 'app.config.ts', 'app.vue') }}
- name: Install packages
run: bun install --prefer-offline
- name: Lint
run: bun run lint
Best Practices
-
Only use private repositories and disable forks: Make sure when using self-hosted runners, that you only attach them to private repositories. A known attack vector is for a malicious actor to fork a public repository and then exfiltrate your private keys from your self-hosted runners by executing workflows on them. Disabling forks can also mitigate this attack, and it's a good idea in general for locking down security on your repositories!
-
Seal your
ACCESS_KEY
: While all variables are encrypted on Railway, you can prevent prying eyes (including your future self) from ever viewing your API Key. Navigate to the Variables tab and next to theACCESS_KEY
variable click the three-dots-menu...
->Seal
. Make sure yourACCESS_KEY
is stored in a secure Password Vault before doing this! -
Security Harden your self-hosted Runners: Security Hardening will make your runners robust and prevent any concerns about your build infrastructure. GitHub's detailed guide can help you secure secrets, authentication, auditing, and managing your runners. Similarly dduzgun-security has an excellent guide to securing your runners that's worth your time.
Known Limitations
-
Because Railway containers are non-priveleged, GitHub Workflows that build-and-then-mount containers on the same host (i.e. Docker-in-Docker) will fail.
-
Using the Serverless Setting on this Service is not recommended and will result in idle runners disconnecting from GitHub and needing to reauthenticate. GitHub Runners have a 50 second HTTP longpoll which keeps them alive. While the runners in this template can automatically reauth with an
ACCESS_TOKEN
it will result in unnecessary offline / abandoned runners. If you want your runners to deauthenticate and spin down, consider using ephemeral runners instead.
Troubleshooting self-hosted runner communication
A self-hosted runner connects to GitHub to receive job assignments and to download new versions of the runner application. The self-hosted runner uses an HTTPS long poll that opens a connection to GitHub for 50 seconds, and if no response is received, it then times out and creates a new long poll. The application must be running on the machine to accept and run GitHub Actions jobs.
GitHub's documentation details all of the different endpoints your self-hosted runner needs to communicate with. If you are operating in a GitHub Allow List environment you must add your self-hosted runners IP Address to this allow list for communication to work.
If you are using a proxy server, refer to GitHub's documentation on configuring your self-hosted runner. You can simply add the required environment variables by adding them to the Variables tab of your Service.
Cost Comparison
On Railway you only pay for what you use, so you'll find your GitHub workflows are significantly cheaper. For this guide we tested over ~2,300 1 minute builds on Railway self-hosted runners and our usage costs were $1.80
compared to GitHub's Estimated Hosted Runner cost of $18.40
for the same workload. Even better? We had 10x Railway replicas with 32 vCPU and 32GB RAM for this test, meaning that our actions workflows would never slow down.
On other platforms you pay for the maximum available vCPUs and Memory. On Railway, you're only paying for usage, or in the below screenshot, the filled in purple area. This enables your workloads to still burst up to the maximum available resources you have configured, with no tradeoffs on cost.
Edit this file on GitHub