-
Notifications
You must be signed in to change notification settings - Fork 0
19 runners the compute behind automation
📖 This page is generated from
modules/19-runners-the-compute-behind-automation/README.md. Edit the source, not the wiki; edits here are overwritten on the next sync. Run the hands-on labs from the repo, linked inline.
⬅ Previous: Module 18: Continuous Delivery and Deployment
Every green check in the last five modules ran on someone else's computer. This module is where you find out whose, and decide whether it should be yours. Owning the runner is what turns "I use a CI pipeline" into "I own the pipeline, end to end."
- Module 8: Remotes and Hosting. You push to a forge, and you met the self-host track (Forgejo, Gitea, GitLab CE, and others). Self-hosted runners are the compute half of that same "own your own infrastructure" decision.
-
Module 14: Continuous Integration. You have a CI workflow that lints and tests
tasks-appon every push. Module 14 mentioned, in passing, that the job runs on "a fresh, throwaway Linux machine the forge spins up." This module is the full accounting of that machine. - Module 18: Continuous Delivery and Deployment. The deploy jobs you automated there run on the same compute. Once you self-host, deploy steps get direct line-of-sight to your private infrastructure: a feature and a footgun, both covered here.
- Helpful but not required: Module 16: Containers, since most runners execute jobs in containers and ephemeral runners lean on them.
You don't need to have read Module 18 in full. If you only have CI from Module 14, everything here still lands. CD just gives you a second, higher-stakes reason to care where jobs run.
By the end of this module you can:
- Explain what a runner is, the actual process and machine that executes your pipeline steps, and tell, for any job, whether it ran on hosted or self-hosted compute.
- Make a reasoned hosted-vs-self-hosted decision for a given pipeline, on the five axes that actually move the needle: cost, data control, network reach, hardware, and air-gap/compliance.
- Register a self-hosted runner against your forge and run the
tasks-appCI job on it. - State, without flinching, the central security tradeoff: a self-hosted runner executes arbitrary code, is non-ephemeral by default, and can be a backdoor into your network. Name the mitigations that make it survivable.
A runner is a process, on some machine, that checks out your code and executes the steps in your
pipeline, nothing more exotic than that. When your Module 14 workflow says "set up
Python, install pytest, run the tests," something physical has to do that: pull the repo onto a
disk, run pip install, run pytest, report pass or fail back to the forge. That something is the
runner.
The loop every runner runs, regardless of forge:
- Register with the forge once, using a registration token, so the forge knows it exists.
- Poll the forge: "got any jobs for me?"
- When a job matches, pull the code and the job definition, then execute each step in order.
- Stream logs and the final status (pass/fail) back to the forge.
- Go to 2.
That's the whole machine. Everything else (hosted vs. self-hosted, ephemeral vs. persistent, containerized vs. bare metal) is a variation on which computer runs that loop and who owns it.
Up to now, every job ran on a hosted runner: a machine the forge owns, spins up on demand, and bills you for. This is the default and, for most work, the right default. What you're actually getting:
- A fresh, throwaway machine per job. This is the property Module 14 leaned on: "works on my machine" can't hide, because the machine has nothing of yours on it. The job starts from a clean image and the machine is destroyed afterward. Clean room, every time.
- No ops burden. You don't patch it, scale it, or keep it online. It exists for the length of your job and then it's gone.
- Metered billing. You pay in runner-minutes: wall-clock time your jobs spend executing, usually with a free monthly allotment and then per-minute pricing above it. Different machine sizes (more CPU/RAM, GPUs) bill at higher multipliers.
For a small Python test suite, hosted is perfect. The job is short, needs nothing private, and the clean-room property is pure upside. You will keep using hosted runners for most of what you do.
A self-hosted runner runs that exact same loop (register, poll, execute, report) but on a machine you own: a spare server, a VM in your own cloud account, a box in your homelab, a beefy workstation under a desk. You install the forge's runner agent, register it with a token, and it starts pulling jobs. To the pipeline author, almost nothing changes; the workflow just targets your runner instead of a hosted one (the targeting mechanic is below).
This is the compute analogue of the Module 8 decision. There, you chose between pushing your repo to a hosted forge versus self-hosting one. Here, you choose between renting compute to run your pipeline versus owning it. Same instinct, applied one layer down.
Don't self-host for the vibe of it. Self-host when one of these actually applies:
-
Cost at volume. Runner-minutes are cheap until they aren't. A heavy pipeline (large test matrices, container builds, long integration suites, or the AI eval/agent jobs from Unit 5 that call models on every run) can run the meter hard. If you already own idle hardware, a self-hosted runner turns "per-minute forever" into "electricity you're already paying for." (Verify the crossover with real numbers; see the checklist at the end.)
-
Data control. Hosted runners execute your code, with your secrets, on infrastructure you don't own. For a lot of work that's fine. For regulated data, customer data under contract, or a shop with a "source never leaves our perimeter" rule, it isn't. A self-hosted runner keeps the checkout, the build, and the secrets on hardware you control.
-
Network access to private systems. This is the one IT pros hit first and hardest. Your CD job (Module 18) needs to deploy to a server on your private network. Your tests need a database that lives on an internal VLAN. A hosted runner sits on the public internet and cannot reach any of that without you punching holes in your firewall. A self-hosted runner placed inside your network already has line-of-sight, with no inbound holes and no VPN gymnastics. (This is also exactly why it's a security problem; hold that thought.)
-
Custom or specialized hardware. GPUs for ML work, a specific CPU architecture, more RAM than any hosted tier offers, a hardware security module, a USB device for hardware-in-the-loop tests. If your job needs hardware the forge doesn't rent, you bring your own.
-
Air-gapped or fully on-prem operation. A self-hosted forge (Module 8) on an isolated network has nowhere to send jobs except a self-hosted runner on that same network. There is no hosted option in an air gap. If your whole stack lives behind a wall, the runner lives there too.
If none of these apply, stay on hosted. "I want to" is not on the list.
The shape is the same on every forge; only the command names and config filenames differ. Three moving parts, vendor-neutral.
A registration token ties a runner to a forge. It's generated in the forge's settings, under its "Runners" or "CI/CD" section, at the repo, org, or instance level. It's short-lived and proves the runner is allowed to attach here. Because it lives behind the forge's web UI, this is the one part of standing up a runner that stays a human-in-the-browser step.
A register/config command turns that token into a running agent. The agent and its flags vary by
forge: GitHub-style Actions uses a config script then a run script (or a service); GitLab uses
gitlab-runner register; Forgejo/Gitea use act_runner register then act_runner daemon. Every one
does the same two things, though: write a small local identity file, then start the poll loop. A
successful registration confirms the runner and it shows up online in the forge. What that looks like:
$ act_runner register --instance https://git.example.com --token *** --labels self-hosted,linux
INFO Runner registered successfully.
INFO Runner self-hosted is now online.
The flags drift between releases, so they're something to look up against current runner docs rather than memorize (see the checklist).
A label is how a workflow picks a runner. A runner advertises labels (self-hosted, linux,
gpu, internal-net); a job selects them with runs-on: in Actions-style YAML, or tags: in
GitLab. So moving a job from hosted to your own runner is one line:
# before, hosted:
runs-on: ubuntu-latest
# after, your runner, selected by label:
runs-on: [self-hosted, linux, internal-net]That one line is the whole "I now own this pipeline" switch. Everything else in your Module 14 workflow stays identical, because the runner runs the same loop either way.
A hosted runner is ephemeral: fresh machine per job, destroyed after. A self-hosted runner is persistent by default: the same machine, with the same disk, runs job after job. That difference is the source of nearly every self-hosted runner security incident, so it gets its own section below; flag it now. The clean-room guarantee you got for free with hosted runners is something you have to rebuild on purpose when you self-host.
Two things make runners specifically an AI-era topic, not a generic ops footnote.
1. AI pipelines are compute-hungry, and that changes the cost math. Unit 5 puts agents inside the pipeline: jobs that call a model to review a PR, triage an issue, or attempt a fix on a failing build. Module 25 takes this further, into agents running as triggered or scheduled runner jobs, kicked off on a cron or by an event rather than a human push. Those jobs run longer and fire more often than a lint-and-test pass, and every one of them consumes runner-minutes. The "rent vs. own compute" decision you're learning here is the one that keeps an AI-heavy pipeline from quietly becoming your biggest line item. When you reach Module 25 and stand up an agent that runs unattended on a schedule, this is the machine it runs on.
2. The agent needs hands, and the self-hosted runner is the hands. A self-hosted runner inside your network is the most direct way to give an automated agent real reach: deploy access, internal databases, private services. That's the payoff and the peril in one sentence. The same property that makes a self-hosted runner useful for an unattended agent (it can touch your real systems) is exactly what makes it dangerous when the code it runs isn't yours. Which brings us to the part you cannot skip.
3. AI writes the CI config too. Ask an agent to "set up CI" and it will happily emit
runs-on: self-hosted or wire a deploy step, because it's pattern-matching on examples that did. AI
also opens PRs (Module 11), and a pull request, from a human or an agent, is untrusted code that
your pipeline may execute. You review the code in a PR (Module 10); you also have to review what
your pipeline does with that PR's code before it runs on hardware that can reach your network. The
review reflex from Module 10 has to extend to the workflow files, not just the application code.
Starting point (this lab is skip-friendly). This lab is self-contained and does not depend on the earlier labs. Its files live in
modules/19-runners-the-compute-behind-automation/lab/. Copy them into a working folder and make a first commit so you start clean:cp -r ~/ai-workflow-course/modules/19-runners-the-compute-behind-automation/lab ~/ai-workflow-course/19-runners-the-compute-behind-automation-lab cd ~/ai-workflow-course/19-runners-the-compute-behind-automation-lab && git init -b main && git add -A && git commit -m "start: module 19"
Lab language: shell, plus a one-line edit to the YAML workflow from Module 14. Runs on your own machine and your own forge, with no hosted account required for the core of it.
This lab has two tracks. Track A is mandatory and works for everyone: find out exactly where your
jobs run today and walk the security tradeoffs concretely. Track B is the real thing: register a
self-hosted runner and run tasks-app CI on it. Do Track A always; do Track B if you have a forge you
can attach a runner to (a self-hosted forge from Module 8 is ideal; a hosted account where you control
a repo also works). If a real runner is too heavy right now, Track A alone satisfies the module.
You'll need:
- Your
tasks-apprepo with the Module 14 CI workflow in it. - The two starter files in this module's
lab/folder:-
whoami-runner.yml, a tiny workflow that reports where it ran. -
inspect-runner.sh, a script you run on a candidate runner machine to see what an attacker would see if they got code execution on it.
-
- For Track B: a forge you can register a runner against, and a spare machine or VM to be the runner (your laptop is fine for a one-off; don't leave it registered).
- Claude Code (sub your own agent).
-
Make the invisible visible. Direct Claude Code (sub your own agent) to place
lab/whoami-runner.ymlin the same workflow directory your Module 14ci.ymllives in, then commit and push it. State the goal, not the path: "Drop this whoami-runner workflow into the right workflows directory for this forge, commit it, and push." The agent resolves the directory for an Actions-style forge (.github//.forgejo//.gitea/underworkflows/). You verify: the run shows up on the forge. It runs the same lint-and-test as Module 14, then prints the runner's hostname, OS, user, whether it looks ephemeral, and whether it can reach the public internet. The receipt step carriesif: always()so it still prints even when lint or test fail; a diagnostic shouldn't disappear on a red build (the job still reports red). On GitLab CI the same idea iswhen: alwayson the job. -
Read the receipt. Open the job logs on your forge and read the
Where did this run?step. You're now able to answer, for a real job, the question this module opened with: whose computer was that? On a hosted runner you'll see a generic cloud hostname and a throwaway user. Note it, because you'll compare against your own runner in Track B. -
See what code execution would expose. On the machine you'd consider using as a self-hosted runner (your laptop is fine for the exercise), run:
bash lab/inspect-runner.sh
It inventories what a job (any job, including one from a pull request) could see if it ran here: environment secrets, cloud credential files, SSH keys, Docker socket access, and which private hosts on your network are reachable. This is not hypothetical. A workflow step is a shell command; whatever the script can see, a malicious workflow step can see too.
-
Walk the tradeoff with Claude Code (sub your own agent), grounded in that output. Paste the
inspect-runner.shoutput into the agent and ask: "If this machine were a self-hosted CI runner and someone opened a pull request with a malicious workflow step, what could they reach or steal? Rank it worst-first." Read the answer against your real output. This is the honest version of "why you'd run your own": the network reach that makes a self-hosted runner useful is the exact same reach that makes a compromised one catastrophic.
-
Get a registration token. In your forge's settings, find the Runners / CI/CD section and generate a runner registration token (repo-level is the tightest scope, so start there).
-
Register the runner. Hand this to Claude Code (sub your own agent) on your runner machine: "Look up the current runner-agent docs for my forge, then download the agent, register it against my forge URL with this token, label it
self-hosted, and start it polling." The commands are forge-specific and drift between releases, which is exactly why you let the agent fetch the current docs instead of running a half-remembered command. You verify: the runner shows as online in the forge's Runners list. -
Aim CI at your runner, the one-line switch. Tell Claude Code (sub your own agent): "Change the
runs-on:(ortags:) line in thetasks-appCI workflow to target myself-hostedrunner instead of the hosted image, then commit and push." That's the before/after edit from Key concepts. You verify: from the job log, the run executed on your own runner. -
Watch your own machine do the work. Open the job logs. The lint-and-test pass from Module 14 now runs on hardware you own. Re-run the
whoami-runner.ymlworkflow too and compare its output to step 2: your hostname, your user, and, critically, note that it is not a fresh throwaway machine. Run it twice and look for leftovers (apipcache, files from the previous run). That persistence is the thing to respect. -
Clean up. Have Claude Code (sub your own agent) stop and unregister the runner agent on your machine. Then remove the runner from the forge's Runners list yourself; that side is a forge-UI step. You verify: the runner disappears from the list. A registered-but-forgotten runner is a standing liability, exactly the kind of stale backdoor the security section warns about.
This is the section that earns the module. Self-hosted runners are the single sharpest-edged tool in this course. Be honest about all of it.
-
A runner executes arbitrary code; that's its entire job. A "workflow step" is just a shell command someone put in a file in the repo. The runner runs it, faithfully, with whatever access that machine has. There is no sandbox unless you build one.
-
Pull requests are untrusted code, and this is the headline risk. On a public repository, anyone can fork it, edit the workflow, and open a PR, and on a misconfigured setup, your self-hosted runner will dutifully execute their workflow on your hardware, inside your network. This is not theoretical: in 2025, real attacks used exactly this path. A malicious fork PR pulled a reverse shell onto a self-hosted runner and used the available token to push malicious code back to the origin repo. The blunt, widely-repeated guidance: do not attach self-hosted runners to public repositories. If you must, require manual approval before workflows from forks/first-time contributors run, and never give those jobs your real secrets.
-
Persistent runners accumulate compromise. Because the default self-hosted runner is not ephemeral, anything a job leaves behind (a cached credential, a background process, a tampered tool on
PATH) survives into the next job. A single compromised run can become a permanent implant. The fix is ephemeral runners: tear the environment down and rebuild it after every job (typically by running each job in a fresh container or a disposable VM). This is more setup, and it's the price of getting back the clean-room property hosted runners gave you for free. -
Network reach cuts both ways. The reason you self-host, line-of-sight to internal systems, is also why a compromised runner is a pivot point into your network. Put runners on an isolated segment with only the egress they actually need, run them as a dedicated low-privilege user (never root, never your own login), and scope their secrets to the minimum. Treat the runner as semi-trusted at best.
-
"Free" compute isn't free. You trade per-minute billing for ops work: patching the OS, keeping the agent online and version-matched to the forge (a runner much older than the server can fail jobs in subtle ways), scaling under load, and securing all of the above. For a busy pipeline on idle hardware that math wins. For an occasional test run, the hosted clean room is cheaper once you count your own time.
-
Autoscaling is a real project, not a checkbox. Matching a fleet of runners to bursty demand, spinning ephemeral runners up and down on a queue, is its own piece of infrastructure. Don't assume one box; don't assume it's trivial to make it many.
You're done when:
- You can look at any pipeline run and state whether it executed on hosted or self-hosted compute,
and back it up from the job's own output (you ran
whoami-runner.ymland read the receipt). - You can give the five reasons to self-host and honestly say which, if any, apply to your situation, instead of self-hosting by default.
- (Track B) You ran
tasks-appCI on a runner you own, by changing a single targeting line, and you saw firsthand that it is not a throwaway machine. - You can explain, to a skeptical colleague, the central tradeoff in one breath: a self-hosted runner executes arbitrary code on your hardware with reach into your network, is persistent by default, and must never be casually attached to a public repo. You can name ephemeral runners, network isolation, and least-privilege as the mitigations.
When "where does this run, and what can it touch?" is a question you ask reflexively about every job, and especially every job triggered by a PR or, soon, by an agent, you own the pipeline end to end. Module 25 will put autonomous agents on exactly this compute; you now know what they're standing on.
This is an expansion-zone module and the runner ecosystem moves. Re-check at build/publish time:
- Runner agent commands and config filenames for each forge named (the GitHub-style
config/runscripts,gitlab-runner register,act_runner register/daemon). Flags and script names drift between releases; confirm against current official runner docs, don't pin from memory. - Hosted runner pricing and free-minute allotments, and the machine-size multipliers, for any forge a reader is likely to use. These change and vary by plan; state them as "check current pricing" rather than a hard number, and re-verify the cost-crossover framing.
- Fork-PR / untrusted-workflow defaults: whether the major forges run fork PRs on self-hosted runners by default or require approval, and the exact setting names. The security guidance here depends on current defaults; confirm them.
- Ephemeral-runner mechanics: the current supported way to run jobs ephemerally
(per-job containers, disposable VMs, the
--ephemeral-style flags) on each forge. - The 2025 attack reference: keep it accurate and current; if newer, clearer public incidents exist at publish time, cite the most representative one rather than an aging example.
- Runner-to-server version-compatibility guidance: confirm the "keep the agent version matched to the forge" caveat still reflects current behavior.
Continue to: Module 20: MCP Servers, Giving the AI Hands ➡
Generated from the ai-workflow-course repo • the model is the cheap, swappable part; the workflow is the durable skill.
Unit 1: Get out of the chat window
- 1 · The Copy-Paste Problem
- 2 · Version Control as a Safety Net
- 3 · Version Control for Words, Not Just Code
- 4 · Getting the AI Out of the Browser
- 5 · Commit the AI's Config, Not Just the Code
- 6 · Branches as Sandboxes for Experiments
- 7 · Worktrees for Running Agents in Parallel
Unit 2: Make it shareable, reviewable, recoverable
- 8 · Remotes and Hosting (GitHub, the Alternatives, and Owning Your Repo)
- 9 · Issues and the Task Layer
- 10 · Reviewing Code You Didn't Write
- 11 · Collaboration: Humans and Agents on One Repo
- 12 · When It Goes Wrong: Revert, Reset, and Recovery
Unit 3: Automate the checking and shipping
- 13 · Testing in the AI Era
- 14 · Continuous Integration
- 15 · Security Scanning for AI-Generated Code
- 16 · Containers and Reproducible Environments
- 17 · Secrets, Config, and Environments
- 18 · Continuous Delivery and Deployment
- 19 · Runners, the Compute Behind the Automation
Unit 4: Extend the AI into your systems
- 20 · MCP Servers, Giving the AI Hands
- 21 · Skills: Teaching the AI Your Playbook
- 22 · Securing Third-Party MCP Servers and Skills
- 23 · Working with Existing Codebases
Unit 5: AI in the Loop
- 24 · Assistive Agents (AI Review and Issue Triage)
- 25 · Module 25. Autonomous Agents: Issue-to-PR and Self-Healing CI
- 26 · Orchestrating Multiple Agents
- 27 · Module 27. Evals: Trusting an Agent That Acts Without You
Finale