Ruby and Bundler in WSL: Native Gems, Lockfiles, and Linux Toolchains
Keep Ruby projects reproducible in WSL by aligning the Linux interpreter, Bundler lockfile, compiler dependencies, native gems, and workspace filesystem.
Ruby itself is portable, but a Ruby development environment contains more than source files. The interpreter, RubyGems, Bundler, native gem extensions, system libraries, and compiled artifacts all belong to a specific operating system and architecture. A gem with a native extension built for Windows is not a Linux extension that WSL can load. Likewise, a gem compiled under one Linux Ruby ABI may not be reusable after switching Ruby versions or architectures.
Use the WSL distribution as the owner of Linux-targeted Ruby work. Keep the project, Ruby executable, Bundler process, build tools, and native libraries inside that distribution. Windows can still provide the editor interface, but editor terminals, test tasks, and language servers should use the Linux workspace and runtime if the application will execute on Linux.
Select and identify the Ruby installation
Ruby’s official documentation describes multiple installation approaches, including packages and version managers. Choose one ownership model for the project. A distro package is simple and integrates with the distribution’s update lifecycle; a version manager can provide several Ruby releases but must be installed and initialized consistently for interactive shells, IDE tasks, and CI. Avoid mixing package-managed files with a second installer that overwrites the same executable paths.
Record the runtime visible in the actual project shell:
command -v ruby
ruby --version
ruby -e 'puts RUBY_PLATFORM; puts RbConfig.ruby'
command -v gem
gem --version
command -v bundle
bundle --version
If bundle resolves to a different Ruby installation than ruby, fix the PATH or reinstall Bundler through the intended environment. A terminal launched by an IDE can initialize a different shell profile than an interactive terminal, so capture the result from the failing test or task process rather than assuming the login shell configuration applies.
Keep a Windows Ruby installation separate if Windows-native tools need it. Do not set Linux GEM_HOME, GEM_PATH, or RUBYOPT to Windows paths. Avoid adding both runtimes to one undifferentiated PATH; command order can silently change when another package or tool is installed.
Put the checkout and gem state on Linux storage
For Linux Ruby tools, keep the repository under the distro’s Linux filesystem. Microsoft recommends the WSL filesystem for Linux command-line projects, while Windows-drive mounts are an interoperability path. Ruby dependency installs touch many small files, generate native extension output, and update timestamps and metadata. A Linux-owned workspace reduces cross-boundary I/O and avoids expecting Windows ACL behavior to reproduce Linux filesystem semantics.
Treat /mnt/c as an intentional exception. It may be appropriate if Windows tools must own the same checkout, but then test the exact operations your team needs: case-sensitive paths, symlinks, file mode, rename behavior, native gem builds, and editor watching. Never place one vendor/bundle directory on a shared checkout and expect it to serve both Windows Ruby and Linux Ruby.
Keep gem caches and installed extensions as disposable environment state. The committed project contract is the Gemfile, Gemfile.lock, Ruby version selection, and any documented system prerequisites. Rebuild local gems from those inputs rather than copying compiled extension directories between machines.
Use Bundler as the dependency boundary
Bundler’s Gemfile describes application dependencies and its lockfile records the resolved versions and platforms. Commit the lockfile for an application so a clean Linux checkout can reproduce the same dependency selection. Run commands through bundle exec so the process loads the bundle selected by the project instead of an unrelated globally installed gem.
An existing project should normally follow this sequence:
cd ~/src/example-app
ruby --version
bundle check || bundle install
bundle exec ruby -e 'puts RUBY_PLATFORM'
bundle exec rake test
The specific test command depends on the repository. The key distinction is that bundle install installs the dependencies already selected by a current lockfile, while changing dependency resolution should be a deliberate reviewable update. When modifying dependencies, run the project’s update process, inspect the lockfile diff, test, and commit the changed lockfile.
Bundler records platform information because dependencies can differ across operating systems and architectures. Inspect it when a lockfile created on Windows behaves unexpectedly under Linux. The correct response is not to delete the lockfile reflexively: first identify whether the dependency has a platform-specific gem, whether the Linux platform is represented, and whether the bundle needs an intentional platform addition or resolution update. Re-run tests in a clean WSL environment afterward.
Build native gems with Linux prerequisites
Some gems include C or C++ extensions, invoke external libraries, or require generated executables. A failed native gem install is often a missing compiler, Ruby header, library development package, or compatible Ruby version. The first meaningful error from the compiler or linker is more useful than the final Bundler summary. Install the Linux development package that owns the missing header or library, then retry inside the bundle.
Use diagnostics that identify both Ruby and the failing gem:
bundle config list
bundle exec ruby -e 'require "rbconfig"; puts RbConfig.ruby; puts RbConfig::CONFIG["arch"]'
gem env
bundle install --verbose
Do not use sudo bundle install for an application. Installing a project’s dependencies as root can change ownership of the bundle and allow package install hooks to run with unnecessary privilege. Keep project dependencies scoped to the developer environment or a project-local path if the team requires that layout.
When the gem needs an external shared library, confirm that the library is available to the runtime after compilation. Successful compilation alone does not prove the dynamic loader can resolve it later. A deployment image may use a different library set than the WSL build environment, so test in the same class of Linux environment used for deployment.
Keep Ruby, Bundler, and CI contracts aligned
Declare the supported Ruby version in the project using its established version file or Gemfile declaration. Avoid relying only on a developer’s global Ruby default. Record the Bundler version compatible with the lockfile and project tooling. An old lockfile can encode a Bundler version that differs from the currently installed command; diagnose the compatibility message before rewriting metadata.
A dependable CI check starts from a clean Linux checkout and uses the declared Ruby version, installs the committed bundle, and runs the project’s tests. WSL should follow the same major runtime contract. Compare ruby –version, Bundler version, and platform output between CI and the distro when debugging inconsistent native extension behavior.
If the project targets multiple platforms, make that matrix explicit. Pure Ruby code can be portable while a dependency’s extension or optional executable remains platform-specific. Include Windows in the test matrix only if it is a supported runtime, and keep each platform’s gem build artifacts isolated. Do not treat a Windows run as evidence that the Linux native dependency path works.
Troubleshoot shell and editor boundary mistakes
When a test works in one terminal but not another, inspect command resolution and environment variables from the exact process. Look for PATH order, shell initialization, GEM_HOME, GEM_PATH, RUBYOPT, and the project’s selected Ruby version. In an IDE, confirm that the terminal and test runner execute inside WSL rather than in a Windows shell pointed at a network path.
If Bundler reports a dependency mismatch, compare the lockfile platform, installed Ruby platform, and gem’s native extension requirements. If a command runs but loads the wrong constant or library, print the resolved file path with Ruby’s $LOADED_FEATURES or Bundler’s environment rather than reinstalling everything. If a project has been copied between Windows and Linux, regenerate ignored native build outputs after confirming the repository state.
Do not solve Linux path problems by adding a Windows Ruby bin directory to the Linux PATH. Windows interop is useful for invoking selected tools, but it does not make a Windows Ruby environment compatible with a Linux bundle. Keep one clear interpreter per task and make its identity part of the test log.
Acceptance criteria for a WSL Ruby project
A clean checkout passes when the project can report its Linux Ruby executable and version, install the committed bundle without root, load required native dependencies, and run its documented test command from the Linux filesystem. Confirm Git sees only intended source and lockfile changes, not machine-specific cache or build output.
For extensions, test a clean rebuild after deleting only the generated extension directory or recreating the bundle. For editor integration, run the same test command from the IDE and an interactive WSL shell and compare the runtime identity. If the Windows environment is also supported, run its own clean dependency installation and test independently.
Finally, keep the service lifecycle separate from the language environment. A Ruby web process or background worker inside WSL exists only while its distribution is running. WSL can shut down independently of a Bundler-managed service, so use CI or a dedicated runtime for persistent service guarantees. A successful local bundle is evidence about dependency resolution, not a promise of uptime or production readiness.
Related:
- Python Environments in WSL: Keep Interpreters, Wheels, and Projects Native
- Go Builds in WSL: Host Identity, Cross-Compilation, and cgo
Sources: