Skip to main content

Documentation Index

Fetch the complete documentation index at: https://tally.wharflab.com/llms.txt

Use this file to discover all available pages before exploring further.

Production stage runs bundle install without excluding the development gem group via BUNDLE_WITHOUT.
PropertyValue
SeverityWarning
CategorySecurity
DefaultEnabled
Auto-fixYes (FixSafe)

Description

Bundler installs every gem in the Gemfile by default. The Rails generator template explicitly excludes the development group at the image level via ENV BUNDLE_WITHOUT="development" precisely because, without it, production images ship web-console, byebug, pry, rspec-rails, letter_opener, bullet, spring, and similar development-only gems — bloating image size and exposing development-only attack surface. web-console in particular has documented RCE history when it leaks into production. The Rails security guide calls this out, and Bundler 2.x documents BUNDLE_WITHOUT and bundle config set without as the supported mechanism to scope the install. This rule fires when a Ruby-shaped production stage runs bundle install and:
  • Neither ENV BUNDLE_WITHOUT=... (with development in the value) nor a prior bundle config set [--local|--global] without ...development... invocation is observable in the stage, AND
  • ENV BUNDLE_ONLY=... (the Bundler 2.5+ inverse selector) does not contain production.
The “production-shaped” heuristic mirrors the tally/ruby/missing-bundle-deployment rule: the Dockerfile binds RAILS_ENV (or RACK_ENV) to "production" somewhere, or the stage has no explicit non-production marker and the rest of the stage shape (Ruby base, no dev-stage name, etc.) implies a runtime image. Stages explicitly named dev, development, test, testing, ci, or debug are skipped, as are stages whose effective env binds RAILS_ENV/RACK_ENV to development or test. Because the rule lives in the Ruby namespace and its remedy is Rails-flavored, it only fires on stages that look like a Ruby runtime: an official ruby:* base, a familiar name containing ruby or rails, a Ruby-runtime derivative, a stage env carrying Ruby/Rails/Bundler signals, or an ENTRYPOINT/CMD that invokes ruby, rails, bundle, puma, etc.

Context-aware refinement

When tally is invoked with --context (or via Bake/Compose), the rule consults the project’s Gemfile to sharpen its behavior:
  • Suppressed when there is nothing to exclude. If the Gemfile is observable and contains no group :development do block (some library-style or ops-tool gems don’t), the rule skips entirely — the recommendation would be moot.
  • Smarter fix wording. If the Gemfile contains both group :development and group :test blocks, the auto-fix emits ENV BUNDLE_WITHOUT="development:test" so production images don’t ship rspec-rails, capybara, or other test-only gems either. With only :development, the fix uses the Rails generator’s exact wording, ENV BUNDLE_WITHOUT="development".
If the Gemfile is not observable (Dockerfile-only mode), the rule applies and the fix defaults to ENV BUNDLE_WITHOUT="development".

Examples

Before

FROM ruby:3.3-slim
ENV RAILS_ENV="production"
COPY Gemfile Gemfile.lock ./
RUN bundle install
COPY . .
CMD ["bin/rails", "server"]

After

The Rails-generator-style fix declares BUNDLE_WITHOUT once at the top of the stage so every nested bundle install (including any inside CI scripts or entrypoints) honors it:
FROM ruby:3.3-slim
ENV RAILS_ENV="production"
ENV BUNDLE_WITHOUT="development"
COPY Gemfile Gemfile.lock ./
RUN bundle install
COPY . .
CMD ["bin/rails", "server"]
The Bundler 2.5+ inverse selector also satisfies the rule:
ENV BUNDLE_ONLY="default:production"

Auto-fix

The rule offers a FixSafe that inserts ENV BUNDLE_WITHOUT="development" (or "development:test" when both groups are observable in the project’s Gemfile) on the line immediately following the stage’s FROM. Insertion is zero-width at column 0, so it composes cleanly with other rule edits in the same stage.

References