🚂 Derails

Where dictators code in peace, free from GitHub's gulag

Tech

Rails 8.1 Local CI: Your Laptop is Your Pipeline

October 24, 2025

“Why trust the cloud when you can trust your overpriced laptop?” - Kim Jong Rails

The Revolution Will Be Run Locally

Rails 8.1 just dropped bin/ci, and it’s the most dictator-friendly feature since rails new.

For years, we’ve been shipping our tests to GitHub Actions, CircleCI, or whatever CI service promised “generous free tier” before inevitably rug-pulling us. Rails 8.1 says: run your entire CI pipeline on your own hardware.

Your M4 Max has 48GB of RAM. Time to use it.

What is bin/ci?

A simple DSL for defining your CI pipeline that runs locally or in any environment with your code.

config/ci.rb
CI.run do
step "Tests: Unit", "bin/rails test"
step "Tests: System", "bin/rails test:system"
step "Style: Rubocop", "bin/rubocop"
step "Security: Brakeman", "bin/brakeman -q"
end

Then run:

Terminal window
bin/ci

All your tests, linters, security checks—executed sequentially on your machine. No YAML. No waiting for runners. No “CI is down” excuses.

Why This Matters for Self-Hosters

1. Zero CI Service Dependency

Remember when GitHub Actions had that 6-hour outage? Your local CI doesn’t care. Server in a bunker? Still works. Internet down? Tests still run.

You own the pipeline.

2. Faster Feedback Loops

Cloud CI startup time:

  • Spin up runner: 30s
  • Checkout code: 15s
  • Install dependencies: 2m
  • Run tests: 45s
  • Total: 3m 30s

Local CI:

  • Run tests: 45s
  • Total: 45s

Physics > YAML optimization.

3. Cost Savings for Heavy Workloads

GitHub Actions pricing (as of Oct 2025):

  • Free tier: 2,000 minutes/month
  • Paid: $0.008/minute for Linux

If you run CI 50 times a day (you deploy a lot):

  • 50 runs × 3.5 minutes = 175 minutes/day
  • 175 × 30 days = 5,250 minutes/month
  • Cost: $42/month (after free tier)

Local CI cost:

  • $0 (electricity: ~$0.50/month)

Your MacBook Pro just became infrastructure.

Setting Up bin/ci

Step 1: Create config/ci.rb

CI.run do
step "Setup", "bin/setup --skip-server"
step "Tests: Models", "bin/rails test test/models"
step "Tests: Controllers", "bin/rails test test/controllers"
step "Tests: Jobs", "bin/rails test test/jobs"
step "Style: Rubocop", "bin/rubocop --parallel"
step "Security: Brakeman", "bin/brakeman -q -z"
step "Security: Bundler Audit", "bin/bundler-audit check --update"
# System tests last (slowest)
step "Tests: System", "bin/rails test:system"
end

Step 2: Run It

Terminal window
bin/ci

That’s it. All steps run sequentially.

Step 3: Integrate with Git Hooks

.git/hooks/pre-push
#!/bin/bash
set -e
echo "🚂 Running local CI before push..."
bin/ci
echo "✅ CI passed, pushing to remote"

Now you literally cannot push broken code. If any step fails, the script exits and the push is blocked.

Advanced Patterns

Conditional Success/Failure Handling

CI.run do
step "Setup", "bin/setup --skip-server"
step "Tests: Rails", "bin/rails test"
step "Security: Brakeman", "bin/brakeman -q"
if success?
step "Signoff: All systems go", "gh signoff"
else
failure "Signoff: CI failed", "Fix the issues above and try again."
end
end

The success? check allows different behavior based on whether all previous steps passed.

Environment-Specific Steps

CI.run do
step "Tests: Unit", "bin/rails test"
if ENV['CI']
# Only run slow tests in GitHub Actions
step "Tests: System", "bin/rails test:system"
end
end

The Anti-Cloud Argument

“But CI should run in a clean environment!”

You know what else runs in a “clean environment”? Production. And production breaks anyway.

Local CI philosophy:

  • If it passes on your machine, it passes
  • If your machine is misconfigured, fix your machine
  • Dependencies match what you actually use
  • Instant feedback > theoretical purity

“What about matrix testing (Ruby 3.3, 3.4)?”

Docker exists. You can script it outside the CI config:

#!/bin/bash
for version in 3.3 3.4; do
echo "Testing Ruby $version..."
docker run ruby:$version bundle exec rails test
done

Or just test on the Ruby version you actually deploy. Still faster than waiting for cloud runners.

When to Still Use Cloud CI

Real talk: Cloud CI has valid use cases.

Use cloud CI for:

  • Pull request checks (external contributors)
  • Multi-OS testing (Windows, macOS, Linux)
  • Deployment pipelines (you don’t deploy from your laptop
 right?)
  • Compliance requirements (audit logs, runner isolation)

Use local CI for:

  • Pre-commit/pre-push checks
  • Rapid iteration during development
  • Full test suite before opening PR
  • Offline development (planes, bunkers, etc.)

The Hybrid Setup

Best of both worlds:

config/ci.rb
CI.run do
# Fast local checks
step "Style: Rubocop", "bin/rubocop --parallel"
step "Tests: Unit", "bin/rails test --exclude system"
# Save slow tests for cloud
if ENV['CI']
step "Tests: System", "bin/rails test:system"
step "Tests: E2E", "bin/rails test:e2e"
end
end

Local CI: Fast checks in 1 minute. Cloud CI: Full suite in 5 minutes.

Real-World Benchmarks

Testing on Derails codebase (M3 Max, 64GB RAM):

TaskCloud CILocal CISpeedup
Rubocop45s12s3.75x
Unit tests2m 15s38s3.55x
System tests4m 30s3m 45s1.2x
Total7m 30s4m 35s1.63x

Not counting:

  • Queue wait time (0-60s)
  • Runner boot time (20-40s)
  • Checkout time (10-20s)

Actual time saved per run: ~4 minutes

50 runs/day = 200 minutes saved = 3.3 hours back per day

The Self-Hosting Stack

Pair local CI with our infrastructure:

  1. Local CI (bin/ci) - Fast feedback on your machine
  2. Gitea Actions - PR checks on self-hosted runners
  3. Kamal Deploy - Push to production from bin/deploy

Total monthly cost: €3.49 (Hetzner CX23)

GitHub Actions equivalent: $50+/month

Installation

Rails 8.1+:

Terminal window
rails new myapp
cd myapp
# Create CI config
cat > config/ci.rb << 'EOF'
CI.run do
step "Tests: All", "bin/rails test"
step "Style: Rubocop", "bin/rubocop"
end
EOF
# Run it
bin/ci

Existing app:

Terminal window
# Add to Gemfile (if not already on Rails 8.1)
bundle update rails
# Create config/ci.rb (see above)
# Run bin/ci

The Philosophy

Rails 8.1’s local CI is part of a larger trend: owning your tools.

  • Rails 8.0: Removed Redis/Sidekiq dependencies (SolidQueue, SolidCache)
  • Rails 8.1: Removed CI service dependencies (bin/ci)
  • Rails 8.2 (probably): Removes deployment platform dependencies (even more Kamal)

DHH’s vision: A single developer can build, test, and deploy a production app with zero external dependencies.

We’re not there yet, but we’re close.

Conclusion

Is local CI for everyone? No.

Is it for dictators who:

  • Deploy 20+ times a day
  • Work on planes/trains/bunkers
  • Distrust cloud providers
  • Own expensive laptops
  • Value speed over theoretical purity

Absolutely.

“The best CI pipeline is the one that doesn’t make you wait.” - BasharAlCode

Try It Yourself

Terminal window
# Install Rails 8.1
gem install rails
# Create new app
rails new my_ci_app
cd my_ci_app
# Setup CI
cat > config/ci.rb << 'EOF'
CI.run do
step "Tests: All", "bin/rails test"
end
EOF
# Run it
bin/ci

Your laptop is now your CI server. Act accordingly.

Resources

← Back to Blog | Home