FFxF runs your GitHub Actions jobs in Montréal: each job gets a fresh virtual machine,
built from an image that follows GitHub's ubuntu-24.04, and destroyed
once the job ends. You pay for the minutes your jobs run, from your credit; the
runner's startup never counts. GitHub runners are in beta, open to every account. See
the GitHub runners page for the prices next to GitHub's.
Link GitHub
In the console, open GitHub runners and press Connect GitHub. GitHub asks where to install the FFxF Runners app (an organization or your personal account) and which repositories it may serve; you come back to the console with that account already linked. If the app is already installed, App already installed? Link it does the same without reinstalling.
The console asks GitHub who you are before it links anything. An organization can be linked by the person who installed the app on it, or who last changed its repositories (both need to be an owner or an app manager of the organization); a personal account, by its owner. An installation is linked to one FFxF account, which pays for its minutes. Unlink stops new jobs from being picked up; uninstall the app on GitHub to remove it entirely.
Labels
Put a single label in runs-on, the one for the size you want:
| Label | vCPU | Memory | Per minute |
|---|---|---|---|
ffxf-2vcpu-ubuntu-2404 | 2 | 8 GB | 0.004 CAD (0.003 USD) |
ffxf-4vcpu-ubuntu-2404 | 4 | 16 GB | 0.008 CAD (0.006 USD) |
ffxf-8vcpu-ubuntu-2404 | 8 | 32 GB | 0.016 CAD (0.012 USD) |
jobs:
build:
runs-on: ffxf-2vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
- run: make test
A job that asks for another label as well (self-hosted, linux…)
is refused: our runners only carry their own label, so GitHub would never hand them
that job. The image has what GitHub's ubuntu-24.04 has: the tool cache
(actions/setup-python, setup-node, setup-java… find
their versions locally), Docker without sudo, service containers, and
passwordless sudo. Outbound traffic works over IPv6 and IPv4, containers
included; no inbound connection can reach the runner.
Actions cache
actions/cache, the cache option of setup-*
actions (setup-node, setup-python, setup-java…)
and docker buildx with a type=gha cache keep their cache in
Montréal, next to the runners, with no change to your workflows: the runner gives
them the address of the FFxF cache instead of GitHub's. The rules are GitHub's: a job
reads its branch's cache, then the base or default branch's, and only writes to its
own branch's; a repository never sees another one's cache.
- Price: 0.30 CAD (0.21 USD) per GB per month, counted by the hour on what is kept, drawn from the credit with the minutes.
- Limit: the repository's limit on GitHub (10 GB by default), 50 GB at most; beyond that, the least recently used entries go.
- An entry unused for 7 days is deleted; Empty, in the console, deletes a repository's whole cache.
- Without credit to pay for the hour, the account's caches are emptied.
- Container jobs (
container:) use GitHub's cache.
Docker cache
Enabled repository by repository in the console (GitHub runners,
Docker cache card), it keeps the /var/lib/docker of the
repository's last successful job that changed Docker: pulled images,
docker build layers, buildx builders and volumes. The next job
finds them with no change to the workflow: a docker build only rebuilds the
layers that changed.
- The cache disk is attached at the start of the job, before the first step, in a few seconds counted in its minutes; the job log says so (Set up runner).
- Only a successful job that changed Docker (images, BuildKit cache, named volumes) updates the cache. A job that does not use it, or only runs containers from images already there, leaves it as it is: the job log says so at the end of the job (Complete runner).
- The update takes about ten seconds after the end of the job. A job that starts in
the meantime, such as one behind a
needs:, waits for it (30 seconds at most). Two jobs that change Docker at the same time start from the same cache, and the last one to succeed wins. - Up to 100 GB per repository; only what is left at the end of the job counts, at the actions cache price (0.30 CAD per GB per month).
- A cache with no job for 7 days is deleted; Empty starts again from the image, Turn off deletes the cache and the option.
During the beta
- Private repositories only: a job from a public repository is refused.
- In an organization, the repositories must be allowed to use the Default runner group, which GitHub allows for private repositories out of the box.
- Linux x64 only: no Windows, macOS, ARM or GPU, and no nested virtualization.
- Up to 32 vCPU of jobs at a time per account; beyond that, jobs wait their turn in GitHub's queue.
Billing
A job is billed from the moment GitHub reports it started to the moment it reports it finished, per minute started, prorated by vCPU: a 3 min 10 s job on 4 vCPU costs 4 × 0.008 = 0.032 CAD. The credit is drawn as the job runs; the end of the job settles the rest. Each month, the first 500 minutes of 2 vCPU (250 on 4 vCPU, 125 on 8) are free once the account has made a payment, and they are used before the credit.
- Not enough credit for a job's first five minutes: the job waits in GitHub's queue without a runner, and starts within a minute of a top-up. GitHub gives up on a job queued for 24 hours.
- Credit runs out during a job: the job is stopped and GitHub shows it as failed.
- Monthly budget exceeded by 20 %: new jobs wait until the budget is raised; running jobs finish.
The console e-mails the account when a job is stopped or held, once a day at most. The minutes show on the consumption page and in your budget like every other service.
Follow your jobs from the API
curl -s https://api.ffxf.net/v1/runners -H "Authorization: Bearer $FFXF_TOKEN"
curl -s "https://api.ffxf.net/v1/runners/jobs?status=queued" -H "Authorization: Bearer $FFXF_TOKEN"
{ "data": [ { "id": 812, "repository": "acme/app", "workflow": "CI", "job": "build", "label": "ffxf-2vcpu-ubuntu-2404",
"status": "queued", "waiting_reason": "credit", "minutes": null, "amount": null,
"url": "https://github.com/acme/app/actions/runs/12345678901" } ],
"meta": { "next_cursor": null, "has_more": false } }
Both calls need the billing.read scope. GET /runners returns
the labels and their price, the account's concurrency, the linked organizations and
accounts, this month's minutes per repository, and each repository's cache (bytes,
entries, last use). GET /runners/jobs lists
jobs most recent first, filtered by repository or status
(queued, running, succeeded, failed,
cancelled, refused). In GET /usage, runner minutes
carry "kind": "github_runners", one record per hour. Linking GitHub goes
through GitHub in a browser, so it stays in the console.