
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Slow builds kill developer momentum and inflate cloud costs, especially when your team runs dozens of pipeline executions daily. If you need to cache Ruby dependencies in CI pipelines effectively, the solution lies in correctly configuring Bundler’s installation path alongside your CI provider’s native caching mechanism. This guide provides battle-tested configurations for GitHub Actions and GitLab CI that actually persist between runs.
Many teams adopt general build caching strategies but fail with Ruby because they cache the wrong directory or use volatile keys. In my experience helping Nepal-based startups and global clients optimize their delivery workflows, misconfigured Ruby caches are the single most common source of "flaky" CI performance. When done right, you reduce dependency installation from minutes to seconds.
How do you configure Bundler paths for reliable caching?
The most frequent mistake engineers make when attempting to cache Ruby dependencies in CI pipelines is relying on system-wide gem directories. Default Bundler behavior installs gems to a global location like /usr/local/bundle or a user-specific path. These locations are problematic in CI because they often require root permissions to write, vary between container images, and contain extraneous files that bloat the cache archive.
Set a local vendor path
Always configure Bundler to install dependencies into a project-local directory. This makes the cache portable, permission-safe, and predictable across different runner environments.
# .github/workflows/ci.yml or .gitlab-ci.yml setup step
- name: Configure Bundler
run: |
bundle config set --local path 'vendor/bundle'
bundle config set --local deployment 'true'
bundle config set --local without 'development test' - path 'vendor/bundle': Isolates gems within the workspace, making them easy to target with cache actions.
- deployment 'true': Enforces strict Gemfile.lock adherence. If the lockfile is out of sync, the build fails immediately rather than silently updating dependencies.
- without 'development test': Excludes unnecessary groups in production-focused pipeline stages, reducing cache size and install time.
This configuration should be committed to your repository via .bundle/config if possible, ensuring consistency between local development and CI. However, setting it explicitly in the pipeline guarantees the environment matches your cache key expectations regardless of developer machine state.
How do you implement caching in GitHub Actions for Ruby?
GitHub Actions provides the official ruby/setup-ruby action, which includes built-in caching support. This is superior to manual cache steps because it understands Ruby versioning and Bundler internals natively. For teams managing complex reusable workflow architectures, encapsulating this setup in a composite action prevents configuration drift.
Optimal ruby/setup-ruby configuration
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
cache-version: 1 Setting bundler-cache: true automatically handles three critical tasks: it sets the bundle path to vendor/bundle, computes a hash of your Gemfile.lock combined with the Ruby version, and restores/saves the cache. The optional cache-version parameter allows you to manually invalidate stale caches without changing dependencies—useful when debugging corruption issues.
Fallback manual caching strategy
If you cannot use setup-ruby (e.g., custom Docker containers), use the generic cache action with explicit keys:
- name: Cache Ruby Gems
uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gems- The restore-keys fallback is essential. When your Gemfile.lock changes, an exact match fails. The prefix match restores the previous cache, allowing Bundler to only fetch updated gems rather than downloading everything from scratch. This partial restoration typically reduces install time by 70–80% even on dependency updates.
How do you cache Ruby dependencies in GitLab CI?
GitLab CI uses a different caching primitive based on key and paths directives. Unlike GitHub’s action-based approach, GitLab’s cache is declarative within the job definition. Teams migrating from other platforms often overlook GitLab’s distributed cache limitations—runners may not share cache instantly across regions, affecting self-hosted runner fleets deployed across multiple data centers.
Standard GitLab CI Ruby cache block
test:
image: ruby:3.3
variables:
BUNDLE_PATH: vendor/bundle
BUNDLE_DEPLOYMENT: "true"
cache:
key:
files:
- Gemfile.lock
paths:
- vendor/bundle
script:
- bundle install
- bundle exec rspec The key.files directive automatically hashes the specified file content. This is functionally equivalent to GitHub’s hashFiles() but integrated into the YAML schema. Always define BUNDLE_PATH as a variable rather than relying on inline bundle config commands—this ensures the path is available during both cache restoration and the install step.
Handling multi-project monorepos
In monorepo setups where multiple Ruby services share a runner, namespace your cache keys to prevent cross-contamination:
cache:
key:
prefix: api-service
files:
- services/api/Gemfile.lock
paths:
- services/api/vendor/bundle Without the prefix, two projects with identical Gemfile.lock hashes could overwrite each other’s caches, causing intermittent build failures that are notoriously difficult to diagnose.
What are common cache invalidation pitfalls and fixes?
Even with correct configuration, caching Ruby dependencies in CI pipelines can fail silently. Understanding these failure modes separates junior implementations from production-grade setups.
| Pitfall | Symptom | Fix |
|---|---|---|
| Caching system gems | Permission errors, massive cache size (>500MB) | Always use vendor/bundle with local config |
| Missing OS in cache key | Linux/macOS runners sharing incompatible binaries | Prefix keys with ${{ runner.os }} or $CI_RUNNER_TAGS |
| Ruby version mismatch | Native extension load failures after upgrade | Include Ruby version in cache key hash |
| Stale lockfile | Cache hits but runtime missing new gems | Enable BUNDLE_DEPLOYMENT=true to fail fast |
| Unbounded cache growth | Storage costs rising, slow uploads | Add cache-version for manual rotation |
Debugging cache misses
When caches consistently miss despite unchanged dependencies, verify these three items:
- Line endings: Windows-generated Gemfile.lock files with CRLF endings produce different hashes than LF. Normalize via
.gitattributes. - Path consistency: Ensure
BUNDLE_PATHmatches exactly between cache restore and bundle install steps. Trailing slashes matter. - Runner architecture: ARM64 and x86_64 runners cannot share caches containing native gems. Include architecture in your key.
Should you use Docker layer caching instead of Bundler caching?
For containerized Ruby applications, Docker layer caching complements—but does not replace—Bundler-level caching. A well-structured Dockerfile copies Gemfile and Gemfile.lock before application code, creating a stable layer that survives code changes. However, this only helps during image builds, not during test jobs that run directly on VM runners.
In practice, use both: Docker layer caching for deployment pipelines, and Bundler caching for test/lint jobs. This dual approach ensures fast feedback loops during development while maintaining efficient production image builds. Teams following lean CI/CD practices often see the highest ROI from this hybrid strategy, as test jobs typically outnumber deploy jobs 10:1.
Conclusion
Properly configured, caching Ruby dependencies in CI pipelines transforms sluggish feedback cycles into responsive development workflows. The core principles remain constant across platforms: isolate gems to vendor/bundle, derive cache keys deterministically from Gemfile.lock, and validate your configuration against real pipeline runs rather than assuming defaults work. Start by auditing your current cache hit rates in your CI dashboard—if you’re below 90% on unchanged branches, apply the configurations above. Need help optimizing your Ruby infrastructure or designing compliant CI systems? Reach out to discuss your specific pipeline challenges.