惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

月光博客
月光博客
C
Check Point Blog
J
Java Code Geeks
腾讯CDC
Apple Machine Learning Research
Apple Machine Learning Research
宝玉的分享
宝玉的分享
Microsoft Azure Blog
Microsoft Azure Blog
WordPress大学
WordPress大学
量子位
Google DeepMind News
Google DeepMind News
I
InfoQ
The GitHub Blog
The GitHub Blog
aimingoo的专栏
aimingoo的专栏
N
Netflix TechBlog - Medium
Hugging Face - Blog
Hugging Face - Blog
博客园 - Franky
V
V2EX
Blog — PlanetScale
Blog — PlanetScale
T
The Blog of Author Tim Ferriss
小众软件
小众软件
博客园_首页
人人都是产品经理
人人都是产品经理
博客园 - 聂微东
IT之家
IT之家

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Free CI Minutes Are Gone: Setting Up a GitHub Actions Sel...
Furkan Köykı · 2026-05-06 · via DEV Community

GitHub gives you 3,000 free Actions minutes per month on private repositories.

That sounds like a lot until you're running multi-step CI pipelines on every PR, every push, every little tweak. Then it's not a lot. Then you're watching the counter drop and quietly calculating how many commits you can still make this month.

The obvious fix is paying more. The other fix is already having a server sitting there.

I had a server sitting there. I set it up as a self-hosted runner. Here's what that looked like.


Why Not Just Pay?

I don't have a strong principled reason. The server was running anyway, jobs were queuing, and the quota was at zero. The path of least resistance was pointing GitHub at the machine I already had.

Self-hosted runners also run on your hardware, in your network, with your dependencies already installed — which means no time spent on "install Python 3.12, install dependencies, wait for cache" every single run. The first run is slower (Poetry installs everything fresh); subsequent runs are faster because the virtualenv is already there.


The Setup

1. Create a Dedicated User

Don't run the runner as root. Create a system user for it:

useradd -r -m -d /opt/github-runner -s /bin/bash \
  -c "GitHub Actions Runner" github-runner

Enter fullscreen mode Exit fullscreen mode

-r makes it a system account (UID < 1000), -m creates the home directory, -d sets it to /opt/github-runner. No sudo access, no shell login by default. Exactly what you want.

2. Download and Verify the Runner

Find the latest version at github.com/actions/runner/releases, then:

cd /opt/github-runner

curl -o actions-runner-linux-x64-2.334.0.tar.gz -L \
  https://github.com/actions/runner/releases/download/v2.334.0/actions-runner-linux-x64-2.334.0.tar.gz

# Verify SHA256 (hash is in the release body)
echo "048024cd2c848eb6f14d5646d56c13a4def2ae7ee3ad12122bee960c56f3d271  actions-runner-linux-x64-2.334.0.tar.gz" | sha256sum -c

tar xzf actions-runner-linux-x64-2.334.0.tar.gz

Enter fullscreen mode Exit fullscreen mode

SHA256 verification matters here. You're downloading an executable that will have significant access to your server.

3. Get a Registration Token

Go to your repository → Settings → Actions → Runners → New self-hosted runner. GitHub will show you a registration token. Copy it — it expires in about an hour.

4. Register the Runner

sudo -u github-runner ./config.sh \
  --url https://github.com/your-username/your-repo \
  --token YOUR_REGISTRATION_TOKEN \
  --name prod-server-01 \
  --labels "self-hosted,linux,prod" \
  --unattended

Enter fullscreen mode Exit fullscreen mode

Run this as the github-runner user, not root. The --labels let you target this specific runner in your workflow YAML. --unattended skips the interactive prompts.

When it works, you'll see:

√ Connected to GitHub
√ Runner successfully added
√ Settings Saved

Enter fullscreen mode Exit fullscreen mode

5. Install as a systemd Service

cd /opt/github-runner
sudo ./svc.sh install github-runner
sudo ./svc.sh start

Enter fullscreen mode Exit fullscreen mode

Check it's running:

sudo systemctl status actions.runner.*.service

Enter fullscreen mode Exit fullscreen mode

You want active (running). Check the logs:

sudo journalctl -u actions.runner.*.service -n 50 --no-pager

Enter fullscreen mode Exit fullscreen mode

The line you're looking for: Listening for Jobs. Once you see that, the runner is up and waiting.

GitHub Actions Runner Setup
The runner registration flow — straightforward once you have the token.


Security Hardening

A few things worth doing before you call it done:

File permissions. The runner directory should be owned by the runner user only:

chown -R github-runner:github-runner /opt/github-runner
chmod 700 /opt/github-runner

Enter fullscreen mode Exit fullscreen mode

Limit what the runner user can do. Don't add it to sudoers unless your jobs actually need it. If they do, scope the permissions tightly with a specific sudoers rule rather than giving full sudo access.

Consider ephemeral runners for sensitive repos. For public repos especially, ephemeral runners run each job in a fresh environment and auto-deregister. For a private repo on your own hardware, persistent runners are usually fine — just be aware of the trade-offs.


Updating Your Workflow Files

Change runs-on in every workflow YAML:

# Before
jobs:
  test:
    runs-on: ubuntu-latest

# After
jobs:
  test:
    runs-on: [self-hosted, linux, prod]

Enter fullscreen mode Exit fullscreen mode

The labels in runs-on must match what you set with --labels during registration. If you have multiple runners with different labels (e.g., prod, staging), you can target them precisely.


The Moment It Worked

I pushed a PR, watched the workflow page, and saw:

Running job: Backend Lint (ruff)

Enter fullscreen mode Exit fullscreen mode

Not on a GitHub-hosted VM spinning up somewhere in Azure. On the machine I was sitting in front of. The first run took a while — Poetry was installing everything fresh. After that, runs were noticeably faster because the virtualenv persisted between jobs.


Trade-offs at a Glance

GitHub-Hosted Self-Hosted
Cost Free (up to 3k min/mo) Your server's electricity bill
Maintenance None Runner updates, OS patches
Speed Consistent (plan-dependent) Faster after first run (local deps)
Isolation Fresh VM every run Shared filesystem between runs
Network GitHub's network Your network (good for private infra)

Neither is universally better. If you're under the free tier, GitHub-hosted is the right default. Once you've burned through the quota, self-hosted makes sense if you have spare server capacity.


Conclusion

The setup takes about 20 minutes end-to-end: create the user, download the runner, register it, install the service, update the workflow files. The tricky part is the registration token — it expires quickly, so have the config.sh command ready before you generate it.

Once it's running, you stop thinking about it. Jobs queue, the runner picks them up, logs stream in. The CI pipeline works exactly the same as before — just on your hardware instead of GitHub's.

The counter is back to zero. I mean, my quota is back to 3,000. The counter is on my server now.