Managing Gems with Bundler: Gemfile, Install, and Update
Bundler is the standard dependency manager for Ruby. Managing gems with Bundler 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, controlling gem versions with 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 cleanly 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 since only the last number can increase. - Greater than:
gem "json", ">= 2.0"permits any version 2.0 or newer. - Range:
gem "ffaker", ">= 2.1", "< 3.0"picks any version from 2.1 up to, but not including, 3.0. - No version:
gem "debug"resolves to the latest version compatible 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 by calling Bundler.setup with only the groups you need. The setup method takes a list of group symbols and activates only those groups, which is critical for keeping production deployments lean and secure:
Bundler.setup(:default, :development) unless Rails.env.production?
The Bundler.setup call must happen before any gems are required; place it near the top of your application’s entry point. The default group always includes gems declared without a group block, while named groups are opt-in. Rails handles this automatically in config/application.rb, but for non-Rails projects you control which groups load where.
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. The resolution step is where Bundler’s value becomes clear: it computes a dependency graph that satisfies every gem’s version requirements simultaneously, backtracking when constraints conflict. This graph becomes the blueprint for your Gemfile.lock.
bundle install
This creates a Gemfile.lock file that pins every gem to exact versions. The lock file records the entire resolved dependency tree, including transitive dependencies you never declared directly. This is why a Rails app with only 5 gems in the Gemfile can easily have 50 entries in Gemfile.lock; each gem brings its own sub-dependencies. 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, storing the excluded groups in .bundle/config. This means subsequent bundle install calls will respect the same exclusion without needing the flag again, which is useful for ensuring CI and production environments never accidentally pull in test frameworks or debugging tools.
To reset excluded groups and reinstall everything:
bundle install --with development test
The --with flag explicitly includes the named groups, effectively clearing any previous --without preference. You can also use this to override a project-wide configuration with a one-off install that pulls in development gems for a specific debugging session.
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, finding 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 bundle install and bundle update, Bundler ships with a set of subcommands that inspect, verify, and optimize your gem environment. Commands like bundle exec ensure your application runs against the exact versions in Gemfile.lock rather than whatever happens to be installed globally. Others like bundle outdated help you track when dependencies need attention. Mastering these commands turns managing gems from a chore into a routine you can automate.
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 searches its default load path ($LOAD_PATH) and may find a system-installed version of a gem rather than the one locked in your Gemfile.lock. This mismatch can cause NoMethodError exceptions when a method was added in a later version, or silent behavioral changes when an older version lacks a bug fix you depend on.
For commands that ship with gems (like rake, rspec, rails), always prepend bundle exec. Bundler injects the exact gem versions from Gemfile.lock into the load path and sets environment variables like BUNDLE_GEMFILE so child processes also run in the correct context. Skipping it is one of the most common sources of “works locally, fails in production” bugs:
# 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. When you first run bundle install, Bundler creates a Gemfile.lock with the latest compatible versions. Over time, new gem releases appear on RubyGems.org, so bundle update is how you pull those in deliberately:
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. This makes bundle check ideal for CI pipelines where you want to skip installation when dependencies are already satisfied, saving build time. The check verifies every gem in Gemfile.lock is present at the correct version on disk, including transitive dependencies. Use it as a fast guard before running tests or deploying:
bundle check || bundle install
The || operator chains the two commands: if bundle check fails (exit code non-zero), bundle install runs to bring the bundle up to date. This pattern is idempotent, safe to run repeatedly without side effects, and appears in Dockerfiles and CI scripts across the Ruby ecosystem.
Bundle list
Show all gems in the current bundle:
bundle list
The output includes each gem’s name, version, and installation path on disk, which is helpful when you need to inspect a gem’s source code or verify that a specific version was installed. If you see unexpected gem versions, run bundle list before reaching for bundle update; the discrepancy might come from a stale Gemfile.lock that needs regenerating.
Bundle outdated
Check for newer versions than what’s in Gemfile.lock:
bundle outdated
bundle outdated --major
bundle outdated queries RubyGems.org for each gem in your lock file and compares the installed version against the latest available release. It groups results by semantic versioning impact (patch, minor, and major) so you can decide how aggressively to update. The --major flag filters to only show gems where a new major version exists, which is useful before planning a compatibility audit. Running this command weekly catches security fixes before they become urgent.
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
The difference between a project that stays maintainable and one buried in dependency conflicts comes down to a few disciplined habits around managing gems. Each practice below addresses a specific failure mode that trips up Ruby teams: unpinned versions breaking CI, stale lock files hiding security vulnerabilities, and missing bundle exec prefixes causing production crashes.
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. The ruby directive in a Gemfile enforces a minimum Ruby version at the Bundler level, which is separate from the .ruby-version file used by version managers like rbenv or chruby. Bundler checks this before resolving any gems, aborting with a clear error message if the running Ruby doesn’t match.
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"
The pessimistic operator avoids the two common failure modes: an unlocked constraint pulling a breaking major version into production, and a pinned version blocking security patches. In a Rails app, the framework gems themselves use ~> internally. Run bundle list and inspect any Rails sub-gem’s dependency tree to see this pattern applied.
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 by passing the --without flag to bundle install. This tells Bundler to exclude the named groups from the resolved dependency set entirely, so gems like rspec-rails and its transitive dependencies are never downloaded in production. The group names you pass match the symbols used in the Gemfile:
bundle install --without development test
The --without flag writes to .bundle/config so the exclusion persists across subsequent runs. This is important for Docker builds and deployment pipelines where you want a single bundle install command rather than repeating flags. Check .bundle/config with cat .bundle/config to verify which groups are currently excluded before deploying.
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 conflicts that frustrate many developers. Managing gems with Bundler is a skill that compounds: once you internalize the lock-and-execute workflow, you can focus on writing code instead of debugging version mismatches. Start with constrained versions in your Gemfile, always commit Gemfile.lock, run bundle exec for every gem command, and managing gems becomes a background habit rather than a daily frustration.
See also
- Debugging Ruby with the Debug Gem: use Bundler groups to install the debug gem only in development
- State machines in Ruby with AASM: a practical guide: integrate a state machine gem into a Bundler-managed project
- Configuration Patterns in Ruby: manage environment-specific configuration alongside your Gemfile
- Ruby Fibers and the Fiber Scheduler: run gem tasks concurrently within a Bundler-managed environment