Your ‘Fresh’ CI Runner Isn’t As Clean As You Think
By Saket Jain Published Linux/Unix
Your ‘Fresh’ CI Runner Isn’t As Clean As You Think
Technical Briefing | 11/4/2026
You’ve got your CI/CD pipelines humming, builds are passing, deployments are smooth. Your runners are supposedly ephemeral — spun up, do their job, then vanish. Clean slate every time, right? Well, maybe. I’ve seen too many times when that clean slate was actually a dusty old chalkboard hiding chalk from last week’s math lesson. These hidden artifacts or cached dependencies aren’t just an annoyance; they’re a direct path to non-reproducible builds, security holes, and the dreaded “it worked on my machine (the runner)” syndrome.
The Phantom State: Where ‘Ephemeral’ Falls Short
The biggest culprits are usually package manager caches and leftover user data. Think `apt` on Debian/Ubuntu or `yum`/`dnf` on RHEL derivatives. When you install a package, it gets downloaded and stored. The next run, if the runner environment isn’t *completely* destroyed and recreated, that package is still there. Your build might pull from the cache instead of the network, meaning you’re not truly testing against a clean download. And what about `/tmp`? Or `~/.local`? Or `~/.npm`? It’s a goldmine of forgotten state. I once spent days debugging a dependency issue that only manifested when a runner got a *truly* fresh image, because the old one always had a specific cached version from an ancient run.
Aggressive Housekeeping: A Necessary Evil
If a full re-provisioning of the runner VM or container isn’t in the cards for every single build, you’re going to need to get surgical with your cleanup. This isn’t just `rm -rf /tmp/*`; it’s about systematically purging anything that could influence subsequent runs. It’s tedious, it’s easy to miss something, and it adds precious seconds to your pipeline. But it’s often the only way to claw back some semblance of determinism in environments that aren’t truly pristine by design.
sudo apt clean
sudo rm -rf /var/lib/apt/lists/*
sudo yum clean all
sudo rm -rf /tmp/*
sudo rm -rf ~/.cache/*
sudo rm -rf ~/.npm/*
sudo find /var/log -type f -name '*.log' -delete
sudo rm -rf /var/cache/yum/*
That snippet is a blunt instrument, and it’s not exhaustive for every system or every runner daemon (looking at you, various GitHub/GitLab runner intricacies). You’ll need to tailor it to your specific stack and dependencies. But here’s the thing: relying on these manual cleanups means your pipeline is always just one missed `rm` command away from weird, hard-to-diagnose failures. It’s a fragile approach, but sometimes it’s the only practical option when resources or infrastructure limit full ephemerality.
Embracing True Ephemerality: The Better Way
- **Containerize Everything:** If your runner environment is a container, make sure that container is always built fresh, and crucially, doesn’t reuse layers from previous builds if those layers introduce unwanted state.
- **Ephemeral VMs:** The gold standard. Spin up a brand new VM from a golden image for each job. Cloud providers make this relatively painless. This is where you get the most reliable ‘clean slate’.
- **Volume Management:** Use ephemeral volumes for everything temporary. If a runner uses a persistent disk, ensure temporary directories (`/tmp`, `/var/tmp`) are mounted as `tmpfs` or are otherwise wiped/recreated per job.
- **User Isolation:** Have your runner jobs execute as a dedicated user whose home directory is aggressively purged or recreated for each run. Don’t reuse user accounts across jobs unless absolutely necessary.
The ultimate goal is that your pipeline should produce the exact same output, regardless of which runner it lands on or what ran before it. Anything less is a compromise that will eventually bite you in production. Invest in true ephemerality now; your future self debugging a bizarre, intermittent failure will thank you.
