Saying stuff about stuff.

Better Ruby Gem caching on CircleCI

I was grateful to Nick Charlton for his blog post on setting up CircleCI 2.0 for Rails as understanding the new v2 configuration options was something I’d been avoiding (v1 has since been deprecated so this is now a must). However, after using, loving, and getting so much value from Dependabot I noticed that with any change to my Gems they were all being installed from scratch — meaning that the cache wasn’t being used and minutes were being added to the build time. I took a little time to understand more about CircleCI’s caching and discovered that they actually already have this covered.

The restore_cache step can take an array of cache keys which it will look up in the order declared. So the seemingly erroneous final bundler-v1- key in the following is actually really important as it allows CircleCI to fallback to the last cache that has the prefix bundler-v1- (I had seen it in a couple of examples but wrongly assumed it would pointlessly look for a cache named literally bundler-v1-).

steps:
  - restore_cache:
    keys:
      - bundler-v1-{{ checksum "Gemfile.lock" }}
      - bundler-v1-

Now after adding this the restore_cache will always fall back to the latest cache entry with the prefix bundler-v1- and the bundle install step will only have to install missing gems. You can verify that it’s working by expanding the “Restoring Cache” step in the CircleCI UI, when there’s a cache miss you’ll see something like “Found a cache from build 513 at bundler-v1-“:

No cache is found for key: bundler-v1-7cHA+e+3dMj5o8KeEXzZWm_pWslivYO08S8xulWZ4gw=
Found a cache from build 513 at bundler-v1-
Size: 66 MB
Cached paths:
  * /home/circleci/app/vendor/bundle

The next problem you’ll encounter is the cache growing with each change to your Gems. This is also easy to fix by running bundle install with the --clean option.

My full restore/install/cache section looks like this:

- restore_cache:
    keys:
      - bundler-v1-{{ checksum "Gemfile.lock" }}
      - bundler-v1-

- run: bundle install --clean --path vendor/bundle

- save_cache:
    key: bundler-v1-{{ checksum "Gemfile.lock" }}
    paths:
      - vendor/bundle

Adding this caching has reduced my build time by a whopping 2 minutes — even if the change was bumping a patch version of a single Gem — and has helped reduce feedback time and CI utilisation. It’s also generally satisfying.


Updated 2019-11-09: Added v1- to cache keys to make it easier to fully bust the gem cache.