cyberangles guide

How to Manage Dependencies in Ruby Projects with Bundler

In Ruby development, "dependencies" refer to external libraries (gems) that your project relies on to function—whether for core features, testing, or development tools. Without a system to manage these dependencies, you risk version conflicts, inconsistent environments across teams, and the dreaded "it works on my machine" problem. Enter **Bundler**: the de facto dependency manager for Ruby. Bundler simplifies tracking, installing, and updating gems, ensuring that every developer and deployment environment uses the exact same versions of dependencies. This guide will walk you through everything you need to know to master Bundler, from setup to advanced workflows.

Table of Contents

  1. What is Bundler?
  2. Installation
  3. Setting Up Bundler in a Project
  4. The Gemfile: Defining Dependencies
  5. Gemfile.lock: Ensuring Consistency
  6. Installing Dependencies
  7. Updating Dependencies
  8. Removing Dependencies
  9. Working with Different Environments
  10. Advanced Bundler Features
  11. Troubleshooting Common Issues
  12. Conclusion
  13. References

What is Bundler?

Bundler is a Ruby gem that manages your project’s dependencies by:

  • Declaring which gems your project needs (via a Gemfile).
  • Resolving version conflicts between gems.
  • Installing missing gems.
  • Locking dependency versions to ensure consistency across environments.

Before Bundler (pre-2010), Rubyists relied on manual gem installation or tools like gemsets, which were error-prone and lacked cross-environment consistency. Bundler solved these problems by centralizing dependency management, making it a cornerstone of modern Ruby development.

Installation

Bundler itself is a Ruby gem, so installation is straightforward.

Prerequisites

  • Ruby (2.3.0 or later recommended; Bundler 2.x requires Ruby 2.3+).
  • RubyGems (usually included with Ruby).

Install Bundler

Run this command in your terminal:

gem install bundler  

To verify installation, check the version:

bundler -v  
# Output: Bundler version x.y.z  

Note: Some Ruby distributions (e.g., via RVM, rbenv, or system Ruby) include Bundler pre-installed. If you see a version, you’re good to go!

Setting Up Bundler in a Project

To start using Bundler in a project, navigate to your project directory and run:

bundle init  

This generates a Gemfile—the heart of Bundler’s workflow. The default Gemfile looks like this:

# frozen_string_literal: true  

source "https://rubygems.org"  

git_source(:github) { |repo| "https://github.com/#{repo}.git" }  

# gem "rails"  

Now you’re ready to define your dependencies!

The Gemfile: Defining Dependencies

The Gemfile is a human-readable manifest that lists your project’s gems and their requirements. Let’s break down its key components.

Sources

Bundler needs to know where to fetch gems. The default source is RubyGems.org, the official gem repository:

source "https://rubygems.org"  # Default: fetch gems from RubyGems  

You can add additional sources (e.g., for private gems):

source "https://gems.example.com"  # Private gem server  

Specifying Gems

To add a gem, use the gem keyword followed by the gem name. For example:

gem "rails"  # Core web framework  
gem "rspec"  # Testing library  
gem "nokogiri"  # HTML/XML parser  

Version Constraints

By default, Bundler installs the latest version of a gem. To avoid unexpected breaking changes, specify version constraints:

SyntaxMeaningExample
~> 2.0Pessimistic: “up to the next major version” (e.g., 2.0, 2.1, …, 2.9)gem "rails", "~> 7.0" (7.0.x)
= 2.1.3Exact versiongem "rspec", "= 3.12.0"
>= 2.0Minimum version (no upper limit)gem "nokogiri", ">= 1.13.0"
>= 2.0, < 3.0Range: between 2.0 (inclusive) and 3.0 (exclusive)gem "rack", ">= 2.2.0", "< 3.0"

Groups

Group gems by environment (e.g., development, testing) to avoid installing unnecessary gems in production:

# Development-only tools (e.g., linters, debuggers)  
group :development do  
  gem "rubocop"  # Code linter  
  gem "pry"      # Debugger  
end  

# Test-only tools  
group :test do  
  gem "rspec"        # Testing framework  
  gem "simplecov"    # Code coverage  
end  

# Production-only gems  
group :production do  
  gem "pg"           # PostgreSQL adapter (for production DB)  
end  

Non-Rubygems Sources

Install gems from Git repositories, local paths, or custom URLs:

Git Repositories

For gems hosted on GitHub (or other Git services):

# From GitHub (uses the `git_source` defined earlier)  
gem "devise", git: "https://github.com/heartcombo/devise.git"  

# Specific branch/tag/commit  
gem "devise", git: "https://github.com/heartcombo/devise.git", branch: "main"  
gem "devise", git: "https://github.com/heartcombo/devise.git", tag: "v4.9.0"  
gem "devise", git: "https://github.com/heartcombo/devise.git", ref: "a1b2c3d"  # Commit hash  

Local Paths

Test a gem you’re developing locally:

gem "my_gem", path: "../my_gem"  # Path to local gem directory  

Platforms

Specify gems for specific Ruby implementations (e.g., MRI, JRuby) or operating systems:

# Only install on MRI Ruby (not JRuby)  
gem "sqlite3", platforms: :ruby  

# Install on Windows  
gem "win32console", platforms: :mswin  

# Multiple platforms  
gem "jdbc-postgres", platforms: [:jruby, :mingw]  

Example Gemfile

Here’s a realistic Gemfile with comments:

# frozen_string_literal: true  

source "https://rubygems.org"  
git_source(:github) { |repo| "https://github.com/#{repo}.git" }  

# Core dependencies (all environments)  
gem "rails", "~> 7.0"  # Rails 7.0.x  
gem "pg", "~> 1.4"     # PostgreSQL adapter  
gem "redis", ">= 4.0"  # Redis client  

# Development tools  
group :development do  
  gem "rubocop", "~> 1.30"  # Linter  
  gem "pry"                 # Debugger  
end  

# Testing tools  
group :test do  
  gem "rspec-rails", "~> 6.0"  # RSpec for Rails  
  gem "capybara", "~> 3.38"    # Integration testing  
end  

# Local gem (for development)  
gem "my_custom_gem", path: "../my_custom_gem", group: :development  

# Git-based gem (bleeding-edge feature)  
gem "devise", git: "https://github.com/heartcombo/devise.git", branch: "main"  

Gemfile.lock: Ensuring Consistency

When you run bundle install, Bundler generates a Gemfile.lock file. This file:

  • Records the exact version of every gem installed (including transitive dependencies).
  • Locks the dependency tree to ensure everyone working on the project (or deploying it) uses the same gem versions.

Example Snippet of Gemfile.lock

GEM  
  remote: https://rubygems.org/  
  specs:  
    rails (7.0.4)  
      actioncable (= 7.0.4)  
      actionmailbox (= 7.0.4)  
      ...  
    pg (1.4.5)  
    redis (4.6.0)  

PLATFORMS  
  ruby  

DEPENDENCIES  
  capybara (~> 3.38)  
  devise!  
  pg (~> 1.4)  
  rails (~> 7.0)  
  redis (>= 4.0)  
  rspec-rails (~> 6.0)  

BUNDLED WITH  
   2.4.10  

Key Notes:

  • Commit Gemfile.lock to version control (Git, SVN, etc.). This ensures consistency across development, testing, and production.
  • BUNDLED WITH shows the Bundler version used to generate the lock file.

Installing Dependencies

Once your Gemfile is configured, install gems with:

bundle install  # Shortcut: `bundle`  

Basic Installation

bundle install does the following:

  1. Reads your Gemfile and resolves dependencies (checks for version conflicts).
  2. Installs missing gems to your system’s gem directory (e.g., ~/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0).
  3. Updates Gemfile.lock with the exact versions installed.

Project-Specific Gems (—path)

To install gems locally in your project (instead of globally), use --path:

bundle install --path vendor/bundle  

This creates a vendor/bundle directory with all gems. Use this to avoid polluting your global gem set or conflicting with other projects.

Excluding Environments (—without)

To skip gems in specific groups (e.g., development or test in production):

bundle install --without development test  

Bundler saves this preference to .bundle/config, so future bundle install commands will automatically exclude these groups.

Deployment Mode (—deployment)

For production deployments, use --deployment for strict consistency:

bundle install --deployment  

This:

  • Requires Gemfile.lock (fails if missing).
  • Installs gems from Gemfile.lock only (no network checks for updates).
  • Disables gem installation to the global directory (uses vendor/bundle by default).

Updating Dependencies

Over time, gem maintainers release updates (bug fixes, new features). Use Bundler to update safely.

Checking for Outdated Gems

First, see which gems have newer versions available:

bundle outdated  

Example output:

Outdated gems included in the bundle:  
  * rspec (3.12.0 < 3.13.0)  
  * nokogiri (1.13.3 < 1.14.0)  

Updating Specific Gems

To update a single gem (and its dependencies) to the latest compatible version:

bundle update rspec  # Updates rspec and its dependencies  

This modifies Gemfile.lock with the new versions.

Updating All Gems

To update all gems to the latest versions allowed by your Gemfile constraints:

bundle update  # Updates all gems (use with caution!)  

Warning: Updating all gems increases the risk of breaking changes. Always test thoroughly in development first!

Removing Dependencies

To remove a gem:

  1. Delete the gem "gemname" line from your Gemfile.
  2. Run bundle install (Bundler updates Gemfile.lock to remove the gem and its unused dependencies).

To clean up unused gems from your system (e.g., after removing a gem):

bundle clean  # Removes gems not in Gemfile.lock  

Note: Use bundle clean --dry-run first to preview changes.

Working with Different Environments

Bundler uses groups to separate gems by environment. For example:

Install Gems for Production Only

bundle install --without development test  # Skips dev/test groups  

Run Commands in a Specific Group

To execute a command using gems from a group (e.g., run tests with rspec):

bundle exec rspec  # Ensures `rspec` from the `test` group is used  

Advanced Bundler Features

bundle exec

bundle exec ensures you use the project-specific gem versions (from Gemfile.lock) instead of global gems. For example:

bundle exec rails server  # Uses the Rails version in your Gemfile  
bundle exec rspec spec/   # Uses the RSpec version in your Gemfile  

Without bundle exec, you might accidentally use a globally installed gem with a different version.

bundle doctor

Diagnose common issues (e.g., missing gems, outdated Bundler):

bundle doctor  

Example output:

The following gems are missing from the current bundle:  
  * pry (3.1.2)  

Bundler version 2.4.10 is out of date. Update with `gem install bundler`.  

bundle open

Open a gem’s source code in your default editor (great for debugging):

bundle open rails  # Opens the Rails gem directory  

bundle cache

Cache gems locally for offline installation (e.g., for deployments with no internet):

bundle cache  # Creates `vendor/cache` with gem files (.gem)  

Later, install from the cache with:

bundle install --local  # Uses cached gems instead of downloading  

Troubleshooting Common Issues

1. Dependency Resolution Error

Problem: Bundler can’t find compatible versions of gems (e.g., “conflicting dependencies for gem ‘rack’”).

Solution:

  • Check bundle outdated to identify conflicting gems.
  • Loosen version constraints in your Gemfile (e.g., use ~> 2.0 instead of = 2.1.3).
  • Update one of the conflicting gems to a version that resolves the conflict.

2. Gemfile.lock Out of Sync

Problem: “Your Gemfile.lock is corrupt” or “Gemfile changed since Gemfile.lock was generated”.

Solution:

  • Run bundle install to regenerate Gemfile.lock.
  • If that fails, delete Gemfile.lock and run bundle install (last resort—this may update gem versions).

3. BUNDLED WITH Version Mismatch

Problem: “Bundler could not find compatible versions for gem ‘bundler’“.

Solution:

  • Install the Bundler version specified in Gemfile.lock (e.g., gem install bundler -v 2.4.10).

4. Permission Errors

Problem: “You don’t have write permissions for the /Library/Ruby/Gems/2.6.0 directory”.

Solution:

  • Use --user-install to install gems to your user directory: gem install bundler --user-install.
  • Or use a Ruby version manager (rbenv, RVM) to avoid system Ruby permission issues.

Conclusion

Bundler is an indispensable tool for Ruby development, ensuring your project’s dependencies are consistent, reproducible, and easy to manage. By mastering Gemfile syntax, bundle install, bundle update, and bundle exec, you’ll avoid version conflicts and streamline collaboration.

Best Practices:

  • Commit Gemfile and Gemfile.lock to version control.
  • Use version constraints to avoid unexpected updates.
  • Test gem updates in development before deploying to production.
  • Use bundle exec to run project-specific commands.

References