Skip to content

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 pointRunsOn solution
Slow bundle install and native gem compilationMagic Cache with unlimited S3 storage, 5x faster than GitHub cache
Memory-hungry parallel test suitesMemory-optimized instances (r7 family) with up to 768GB RAM
Matrix strategies = N cold bootsVertical scaling: single powerful runner with parallel_tests
GitHub’s 10GB cache limit causes evictionsUnlimited cache, configurable retention (default 10 days)
Expensive large runnersSpot instances for ~90% cost savings

Once you have installed RunsOn in your AWS account, enable faster builds with two changes:

  1. Switch runs-on: to a RunsOn runner with S3 caching enabled
  2. Add the runs-on/action@v2 action 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 rspec

The 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.

A common pattern for speeding up Rails test suites is splitting tests across matrix jobs:

# ❌ Traditional approach: 4 separate jobs
strategy:
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_tests
jobs:
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 16

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 tests
runs-on: runs-on=${{ github.run_id }}/family=r7+r8/cpu=16/extras=s3-cache

Common configurations:

WorkersRecommended configUse case
4family=r7+r8/cpu=4Small-medium apps (32GB RAM)
8family=r7+r8/cpu=8Medium apps with system tests (64GB RAM)
16family=r7+r8/cpu=16Large apps, monoliths (128GB RAM)

With extras=s3-cache enabled, the Magic Cache transparently accelerates all actions/cache-based caching, including ruby/setup-ruby’s bundler cache.

When using ruby/setup-ruby with bundler-cache: true:

  • All gems in your vendor/bundle directory
  • 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.

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') }}

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/**/*') }}

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"

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-cache

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 tmpfs
runs-on: runs-on=${{ github.run_id }}/family=r7+r8/cpu=16/extras=s3-cache+tmpfs

This mounts /home/runner, /tmp, and /var/lib/docker on a tmpfs volume, making file operations significantly faster.

With parallel_tests, set up all test databases in one command:

- name: Setup databases
run: bundle exec rake parallel:setup

This creates and loads the schema for all parallel test databases (test_0, test_1, etc.).

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/data

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-cache
steps:
- uses: runs-on/action@v2
- uses: actions/checkout@v4
- run: bundle install --jobs 4 # Just checking for new gems
- run: bundle exec rspec

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-*.xml
OptimizationHow to enable
S3-based cachingextras=s3-cache + runs-on/action@v2
More CPUsrunner=16cpu-linux-x64 or cpu=16
More memoryfamily=r7+r8/cpu=16
ARM64 (cheaper)runner=16cpu-linux-arm64
RAM diskextras=s3-cache+tmpfs
YJITRUBY_YJIT_ENABLE=1 env var

Ready to speed up your Ruby CI? Install RunsOn in your AWS account in under 10 minutes.