Ruby & Rails CI best practices
Ruby and Rails applications have specific CI challenges: slow gem installation, memory-hungry test suites, and complex database setup. RunsOn gives you the flexibility to address each of these with the right instance type and caching strategy.
| Pain point | RunsOn solution |
|---|---|
| Slow bundle install and native gem compilation | Magic Cache with unlimited S3 storage, 5x faster than GitHub cache |
| Memory-hungry parallel test suites | Memory-optimized instances (r7 family) with up to 768GB RAM |
| Matrix strategies = N cold boots | Vertical scaling: single powerful runner with parallel_tests |
| GitHub’s 10GB cache limit causes evictions | Unlimited cache, configurable retention (default 10 days) |
| Expensive large runners | Spot instances for ~90% cost savings |
Quick start
Section titled “Quick start”Once you have installed RunsOn in your AWS account, enable faster builds with two changes:
- Switch
runs-on:to a RunsOn runner with S3 caching enabled - Add the
runs-on/action@v2action before other steps
jobs: test: runs-on: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/extras=s3-cache steps: - uses: runs-on/action@v2 - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: bundler-cache: true - run: bundle exec rspecThe bundler-cache: true option in ruby/setup-ruby automatically caches your gems. With extras=s3-cache, this cache is stored in your S3 bucket instead of GitHub’s limited cache.
Vertical scaling: ditch the matrix
Section titled “Vertical scaling: ditch the matrix”A common pattern for speeding up Rails test suites is splitting tests across matrix jobs:
# ❌ Traditional approach: 4 separate jobsstrategy: matrix: ci_node_index: [0, 1, 2, 3]The problem? Each matrix job means:
- A separate VM boot (~30-60s)
- A separate
bundle install - A separate database setup
- Coordination overhead for test splitting
With RunsOn, vertical scaling is often better: use a single powerful runner with the parallel_tests gem to run tests in parallel processes.
# ✅ Better: single 16-CPU runner with parallel_testsjobs: test: runs-on: runs-on=${{ github.run_id }}/runner=16cpu-linux-x64/extras=s3-cache services: postgres: image: postgres:16 env: POSTGRES_PASSWORD: postgres options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 redis: image: redis:7 options: >- --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 env: DATABASE_URL: postgres://postgres:postgres@postgres:5432/test REDIS_URL: redis://redis:6379/0 RAILS_ENV: test steps: - uses: runs-on/action@v2 - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: bundler-cache: true - name: Setup parallel test databases run: bundle exec rake parallel:setup - name: Run tests in parallel run: bundle exec parallel_rspec spec/ -n 16Right-size your runner for Rails
Section titled “Right-size your runner for Rails”Rails applications are memory-hungry, especially when running parallel tests. Each parallel worker loads the full Rails environment, and system tests with headless Chrome add even more memory pressure.
Rule of thumb: allocate 1-2GB RAM per parallel test worker.
For memory-intensive workloads, use the r7 or r8 (memory-optimized) instance families:
# 16 CPUs with 64GB RAM for parallel testsruns-on: runs-on=${{ github.run_id }}/family=r7+r8/cpu=16/extras=s3-cacheCommon configurations:
| Workers | Recommended config | Use case |
|---|---|---|
| 4 | family=r7+r8/cpu=4 | Small-medium apps (32GB RAM) |
| 8 | family=r7+r8/cpu=8 | Medium apps with system tests (64GB RAM) |
| 16 | family=r7+r8/cpu=16 | Large apps, monoliths (128GB RAM) |
Magic Cache for Ruby tooling
Section titled “Magic Cache for Ruby tooling”With extras=s3-cache enabled, the Magic Cache transparently accelerates all actions/cache-based caching, including ruby/setup-ruby’s bundler cache.
What gets cached automatically
Section titled “What gets cached automatically”When using ruby/setup-ruby with bundler-cache: true:
- All gems in your
vendor/bundledirectory - Compiled native extensions (nokogiri, pg, mysql2, etc.)
Native gem compilation is often the slowest part of bundle install. With Magic Cache, these compiled extensions are restored in seconds instead of being rebuilt.
Bootsnap cache
Section titled “Bootsnap cache”Rails uses Bootsnap to cache compiled Ruby and YAML files. Add this to your workflow to cache it:
- uses: actions/cache@v4 with: path: tmp/cache/bootsnap key: bootsnap-${{ runner.os }}-${{ hashFiles('Gemfile.lock') }}Asset compilation cache
Section titled “Asset compilation cache”For Rails apps with asset pipelines (Sprockets or Propshaft):
- uses: actions/cache@v4 with: path: | public/assets tmp/cache/assets key: assets-${{ runner.os }}-${{ hashFiles('app/assets/**/*', 'app/javascript/**/*') }}Performance tuning options
Section titled “Performance tuning options”Enable YJIT (Ruby 3.2+)
Section titled “Enable YJIT (Ruby 3.2+)”YJIT is Ruby’s JIT compiler and can speed up test suites by 15-30%. Enable it with an environment variable:
env: RUBY_YJIT_ENABLE: "1"Or in your workflow:
- name: Run tests run: bundle exec rspec env: RUBY_YJIT_ENABLE: "1"ARM64 runners
Section titled “ARM64 runners”Ruby runs great on ARM64, and these instances are ~20-30% cheaper:
runs-on: runs-on=${{ github.run_id }}/runner=16cpu-linux-arm64/extras=s3-cachetmpfs for I/O-heavy workloads
Section titled “tmpfs for I/O-heavy workloads”System tests generate lots of temporary files (screenshots, HTML snapshots). For I/O-heavy workloads, use tmpfs to run everything in RAM:
# Memory-optimized instance with tmpfsruns-on: runs-on=${{ github.run_id }}/family=r7+r8/cpu=16/extras=s3-cache+tmpfsThis mounts /home/runner, /tmp, and /var/lib/docker on a tmpfs volume, making file operations significantly faster.
Database setup optimization
Section titled “Database setup optimization”Use parallel database setup
Section titled “Use parallel database setup”With parallel_tests, set up all test databases in one command:
- name: Setup databases run: bundle exec rake parallel:setupThis creates and loads the schema for all parallel test databases (test_0, test_1, etc.).
Faster PostgreSQL with tmpfs
Section titled “Faster PostgreSQL with tmpfs”Mount PostgreSQL’s data directory on tmpfs for maximum speed:
services: postgres: image: postgres:16 env: POSTGRES_PASSWORD: postgres POSTGRES_HOST_AUTH_METHOD: trust options: >- --health-cmd pg_isready --health-interval 5s --health-timeout 3s --health-retries 10 --mount type=tmpfs,destination=/var/lib/postgresql/dataAdvanced: custom AMI
Section titled “Advanced: custom AMI”For teams running hundreds of jobs per day, you can skip ruby/setup-ruby entirely by baking Ruby into a custom AMI.
Benefits:
- Skip 30-60s of Ruby installation
- Pre-install common gems
- Include system dependencies (imagemagick, ffmpeg, etc.)
runs-on: runs-on=${{ github.run_id }}/image=my-ruby-33-rails/family=r7+r8/cpu=16/extras=s3-cachesteps: - uses: runs-on/action@v2 - uses: actions/checkout@v4 - run: bundle install --jobs 4 # Just checking for new gems - run: bundle exec rspecComplete example workflow
Section titled “Complete example workflow”Here’s a production-ready workflow for a Rails application:
name: CI
on: push: branches: [main] pull_request:
jobs: test: runs-on: runs-on=${{ github.run_id }}/family=r7+r8/cpu=16/extras=s3-cache services: postgres: image: postgres:16 env: POSTGRES_PASSWORD: postgres options: >- --health-cmd pg_isready --health-interval 5s --health-timeout 3s --health-retries 10 --mount type=tmpfs,destination=/var/lib/postgresql/data redis: image: redis:7 options: >- --health-cmd "redis-cli ping" --health-interval 5s --health-timeout 3s --health-retries 10 env: DATABASE_URL: postgres://postgres:postgres@postgres:5432/test REDIS_URL: redis://redis:6379/0 RAILS_ENV: test RUBY_YJIT_ENABLE: "1" steps: - uses: runs-on/action@v2 - uses: actions/checkout@v6
- uses: ruby/setup-ruby@v1 with: bundler-cache: true
- uses: actions/cache@v4 with: path: tmp/cache/bootsnap key: bootsnap-${{ runner.os }}-${{ hashFiles('Gemfile.lock') }}
- name: Setup parallel databases run: bundle exec rake parallel:setup
- name: Run tests run: bundle exec parallel_rspec spec/ -n 16
- name: Upload test results uses: actions/upload-artifact@v4 if: always() with: name: test-results path: tmp/rspec-*.xmlSummary
Section titled “Summary”| Optimization | How to enable |
|---|---|
| S3-based caching | extras=s3-cache + runs-on/action@v2 |
| More CPUs | runner=16cpu-linux-x64 or cpu=16 |
| More memory | family=r7+r8/cpu=16 |
| ARM64 (cheaper) | runner=16cpu-linux-arm64 |
| RAM disk | extras=s3-cache+tmpfs |
| YJIT | RUBY_YJIT_ENABLE=1 env var |
Ready to speed up your Ruby CI? Install RunsOn in your AWS account in under 10 minutes.