Managing Gems with Bundler
Bundler is the standard dependency manager for Ruby. It ensures your project uses the exact gem versions you specify, preventing the dreaded “it works on my machine” problem that plagues Ruby projects. Whether you’re building a Rails application or a simple Ruby script, understanding Bundler will save you from dependency headaches.
Why Bundler Matters
Before Bundler existed, Ruby developers struggled with gem version conflicts. Your project might need version 2.0 of a library, while another project on the same machine required version 1.0. RubyGems alone couldn’t resolve these conflicts, and gem updates would often break existing applications.
Bundler solves this by locking your project to specific gem versions. It reads your project’s dependencies, resolves conflicts between different gems’ requirements, and installs exactly what you need. More importantly, it records exactly what was installed so anyone else working on your project gets the same versions.
Every Ruby project should use Bundler. It handles transitive dependencies (gems that your gems depend on), ensures reproducible builds, and integrates seamlessly with Rails, Sinatra, and standalone Ruby projects alike.
The Gemfile
The Gemfile lives in your project’s root directory and declares which gems your application needs. This is your source of truth for dependencies.
A basic Gemfile starts with specifying the Ruby version required, then lists your gems:
source "https://rubygems.org"
ruby "3.2.0"
gem "rails", "~> 7.1"
gem "puma", "~> 6.0"
gem "sqlite3", "~> 1.6"
The source directive tells Bundler where to fetch gems. The ruby directive ensures anyone running your project uses a compatible Ruby version.
Version Specifiers
You control which versions Bundler installs using version specifiers:
- Exact version:
gem "nokogiri", "1.15.0"— installs only this version - Pessimistic:
gem "rake", "~> 13.0"— allows 13.x but not 14.0 (the last number can increase) - Greater than:
gem "json", ">= 2.0"— any version 2.0 or newer - Range:
gem "ffaker", ">= 2.1", "< 3.0"— any version from 2.1 up to (but not including) 3.0 - No version:
gem "debug"— gets the latest version that works with other dependencies
The pessimistic operator ~> is the most common. ~> 7.1 means “7.1.0 or newer in the 7.x series, but not 8.0.” This gives you bug fixes while protecting against breaking changes.
Groups and Environments
You can group gems for different environments:
group :development, :test do
gem "rspec", "~> 3.12"
gem "factory_bot", "~> 6.2"
end
group :test do
gem "faker"
end
group :development do
gem "web-console"
end
Skip loading certain groups in production:
Bundler.setup(:default, :development) unless Rails.env.production?
Conditional Dependencies
Install gems based on the platform or Ruby version:
gem "google-protobuf", platforms: [:ruby]
platforms :ruby do
gem "sqlite3"
end
gem "rb-inotify", platforms: :jruby
Installing Gems with bundle install
Run bundle install to install all gems specified in your Gemfile. Bundler reads the file, resolves dependencies, and installs everything needed.
bundle install
This creates a Gemfile.lock file that pins every gem to exact versions. Commit this file to version control so your team and deployment environments use identical dependencies.
Common Installation Options
Install without the development group:
bundle install --without development test
The --without flag remembers your preference for future runs. To reset:
bundle install --with development test
Install gems in a specific path for offline or CI environments:
bundle install --deployment
This installs to vendor/bundle and requires all dependencies to be resolved upfront.
Installation Flow
When you run bundle install, Bundler:
- Reads your Gemfile
- Resolves dependencies — finds compatible versions for everything
- Downloads gems from RubyGems.org (or your configured source)
- Writes Gemfile.lock with exact versions
- Installs gems to your system
If dependency conflicts exist, Bundler aborts with an error explaining the issue.
Understanding Gemfile.lock
The Gemfile.lock is Bundler’s most important artifact. It contains the exact versions of every gem installed, including transitive dependencies.
Here’s what a Gemfile.lock looks like:
GEM
remote: https://rubygems.org/
specs:
rails (7.1.3)
actioncable (= 7.1.3)
actionmailer (= 7.1.3)
actionpack (= 7.1.3)
actionview (= 7.1.3)
activejob (= 7.1.3)
activemodel (= 7.1.3)
activerecord (= 7.1.3)
activestorage (= 7.1.3)
activesupport (= 7.1.3)
bundler (>= 1.15.0)
railties (= 7.1.3)
actioncable (7.1.3)
actionpack (= 7.1.3)
activesupport (= 7.1.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.3)
nio4r (2.5.9)
PLATFORMS
ruby
x86_64-linux
DEPENDENCIES
rails (~> 7.1)
puma (~> 6.0)
RUBY VERSION
ruby 3.2.0p0
BUNDLED WITH
2.4.21
The GEM section lists every gem and its dependencies. The PLATFORMS section specifies which platforms your bundle works on. DEPENDENCIES mirrors your Gemfile but with resolved versions. RUBY VERSION records what Ruby was used. BUNDLED WITH shows the Bundler version.
Why Gemfile.lock Matters
Gemfile.lock guarantees reproducibility. When your teammate runs bundle install on a different machine, they get identical gem versions. Your CI pipeline gets the same versions as production. No surprise failures from updated dependencies.
Never edit Gemfile.lock manually. Let Bundler manage it.
The exception: version control. Always commit Gemfile.lock. Exclude it only if you intentionally want different environments to resolve their own versions (rare and usually a bad idea).
Common Bundle Commands
Beyond install and update, you’ll use several other bundle commands regularly.
bundle exec
Run any Ruby command within your project’s bundle context:
bundle exec rake db:migrate
bundle exec rails console
bundle exec ruby script.rb
This ensures your command uses gems from your Gemfile, not system gems. Without bundle exec, Ruby might load a different version of a gem, causing confusing errors.
For commands that ship with gems (like rake, rspec, rails), always prepend bundle exec:
# Without bundle exec - may use wrong gem version
rake db:migrate
# With bundle exec - uses exact version from Gemfile.lock
bundle exec rake db:migrate
bundle update
Update gems to newer versions that satisfy your Gemfile constraints:
bundle update
bundle update rails
bundle update nokogiri --conservative
Updating a single gem (bundle update rails) updates only that gem and its dependents. The --conservative flag minimizes changes by updating only to the next compatible version.
Be careful: bundle update can change multiple gems. Always review Gemfile.lock changes before committing.
bundle check
Verify if all dependencies are satisfied:
bundle check
Returns exit code 0 if the bundle is complete, non-zero if gems are missing. Useful in CI pipelines:
bundle check || bundle install
bundle list
Show all gems in the current bundle:
bundle list
Includes gem path information for debugging.
bundle outdated
Check for newer versions than what’s in Gemfile.lock:
bundle outdated
bundle outdated --major
Shows which gems have updates available. Useful before running bundle update.
Gem Binaries
Instead of using bundle exec constantly, add gems’ bin directories to your PATH:
bundle binstubs rspec
This creates executable stubs in bin/rspec that automatically run with the correct bundle context. Add bin/ to your PATH or run commands as ./bin/rspec.
Best Practices
Follow these patterns to avoid common pitfalls.
Always Commit Gemfile.lock
Treat Gemfile.lock as essential as your source code. It ensures every environment uses identical dependencies. The only exception is when you intentionally want to test updated dependencies in a controlled way.
Specify Ruby Version
Include the ruby directive in your Gemfile:
ruby "~> 3.2.0"
This fails fast if someone tries to run your project with an incompatible Ruby version.
Use Pessimistic Version Constraints
Prefer ~> over exact versions or loose constraints:
# Good - gets bug fixes, won't break on major changes
gem "sidekiq", "~> 7.1"
# Risky - could pull incompatible versions
gem "sidekiq", ">= 7.1"
# Too restrictive - no updates without manual change
gem "sidekiq", "7.1.0"
Run bundle install After Gemfile Changes
Never manually edit Gemfile.lock. After changing Gemfile, run bundle install to regenerate the lock file.
Use Groups for Environment-Specific Gems
Keep development and test dependencies separate from production:
group :development, :test do
gem "rspec-rails"
end
Skip them in production:
bundle install --without development test
Keep Gems Updated
Outdated gems miss security patches. Run bundle outdated periodically and update intentionally:
bundle update --conservative
Review changes, test thoroughly, then commit.
Use bundle exec Consistently
Make it a habit. CI pipelines should always use bundle exec. Don’t rely on PATH manipulation that might load system gems.
Bundler is essential to Ruby development. Master these fundamentals — Gemfile syntax, installation, Gemfile.lock management, and the core commands — and you’ll avoid the dependency problems that frustrate many developers. Start with constrained versions in your Gemfile, always commit Gemfile.lock, and use bundle exec for running any gem command.