tally-cli
tally is a production-grade Dockerfile/Containerfile linter and formatter that keeps build files clean, modern, and consistent. It uses
BuildKit's official parser and checks (the same foundation behind docker buildx) plus a safe auto-fix engine, runs as a single Go binary, and
needs no Docker daemon.
This RubyGem ships the same tally binary used by Tally's Homebrew, mise, npm, PyPI, and WinGet distributions, packaged so a gem install is
enough.
Installation
gem install tally-cli
Then call tally directly:
# Lint everything in the repo (recursive)
tally lint .
# Apply all safe fixes automatically
tally lint --fix Dockerfile
# JSON / SARIF / GitHub Actions output for CI
tally lint --format sarif . > tally.sarif
Why Ruby developers should care
The Ruby and Rails container story is unusually concrete in 2026 — Rails 7.1+ ships a generated production Dockerfile out of the box, and the wider
ecosystem has converged on Bundler 2.x, the official ruby:*-slim images, jemalloc, YJIT, and Kamal-style deployments. There is, in other words,
an unusually clear right answer for what a Rails Dockerfile should look like — and a corpus of real-world Dockerfiles that drift from it in
predictable ways.
Tally's research surveyed 196 Ruby/Rails Dockerfiles across 144 application repositories (Mastodon, Discourse, Chatwoot, GitLab, Forem, Spree, Solidus, Decidim, Errbit, Loomio, Manyfold, OpenStreetMap, OpenProject, Zammad, Basecamp's Once/Writebook/Fizzy, and many more). The recurring drift, despite the Rails generator template being widely (often imperfectly) copied:
- Bundler production env vars are inconsistent. Only 37 / 196 set
BUNDLE_DEPLOYMENT=1; only 41 / 196 setBUNDLE_WITHOUT. gem install bundleris reinstalled on top of a base image that already ships Bundler 2.x in 55 / 196 files.- Rails generator's bundler-cache cleanup is dropped on copy in roughly 75% of files that run
bundle install. assets:precompileruns withoutSECRET_KEY_BASE_DUMMY=1in 27 / 67 cases — forcingRAILS_MASTER_KEYto be present at build time (and into image history if passed viaARG/ENV).bootsnap precompileruns without-j 1in 39 / 44 cases. The Rails generator explicitly calls out the QEMU multi-arch crash this prevents.libjemallocis installed but never preloaded in 16 / 28 cases — the package size is paid for and the allocator never loads.- Ruby version drift is severe. 48 / 196 still pin Ruby 2.x; 15 / 196 pin Ruby 3.0 or 3.1 — all fully out of upstream support as of 2026.
- YJIT is essentially unused outside Mastodon. Ruby 3.3+ is widespread; YJIT is a near-free 15-30% production speedup; corpus uptake is 3 / 196.
- Modern BuildKit + Bundler patterns are essentially absent. 0 / 196 use
RUN --mount=type=bindforGemfile; 1 / 196 usesRUN --mount=type=secretfor private gem auth; 0 / 196 useRUN --network=nonefor an offline install phase; 2 / 196 wire aHEALTHCHECKagainst Rails 7.1's/upendpoint.
Tally's tally/ruby/* rules target exactly these drifts.
Ruby-specific rules
Tally ships 17 Ruby/Rails Dockerfile rules under the tally/ruby/* namespace. Highlights:
| Rule | What it catches |
|---|---|
tally/ruby/jemalloc-installed-but-not-preloaded |
libjemalloc2 installed but no LD_PRELOAD / MALLOC_CONF — paying image-size cost for an unused allocator. |
tally/ruby/asset-precompile-without-dummy-key |
bin/rails assets:precompile without SECRET_KEY_BASE_DUMMY=1 — forces RAILS_MASTER_KEY into image history. |
tally/ruby/bootsnap-precompile-without-j1 |
bootsnap precompile without -j 1 — known QEMU multi-arch hang. |
tally/ruby/missing-bundle-deployment |
Production stage missing BUNDLE_DEPLOYMENT=1. |
tally/ruby/missing-bundle-without-development |
Production stage installing development/test gems because BUNDLE_WITHOUT isn't set. |
tally/ruby/redundant-bundler-install |
gem install bundler on a base image that already ships Bundler 2.x. |
tally/ruby/leftover-bundler-cache |
bundle install without the cache-cleanup step from the Rails generator (~/.bundle, cache/, bundler/gems/*/.git). |
tally/ruby/eol-ruby-version |
Base image pinned to a Ruby branch that Ruby core no longer supports (Ruby 2.x, 3.0, 3.1 as of 2026). |
tally/ruby/yjit-not-enabled-on-supported-runtime |
Ruby 3.3+ runtime image without RUBY_YJIT_ENABLE=1. |
tally/ruby/state-paths-not-writable-as-non-root |
Rails app runtime sets USER non-root but tmp/, log/, storage/, db/ aren't chown'd. |
tally/ruby/secrets-in-arg-or-env |
SECRET_KEY_BASE / RAILS_MASTER_KEY declared via ARG or ENV. |
tally/ruby/deprecated-bundler-install-flags |
bundle install --without / --deployment / --path (Bundler 2.x deprecated; use env vars). |
tally/ruby/prefer-bundler-cache-mount |
bundle install on BuildKit without RUN --mount=type=cache for ${BUNDLE_PATH}/cache — gem builds re-fetch every layer rebuild. |
tally/ruby/prefer-gemfile-bind-mounts |
COPY Gemfile Gemfile.lock then bundle install — bind-mount instead so the manifests don't bake into image history. |
tally/ruby/prefer-network-none-install |
Encourages the bundle cache + RUN --network=none bundle install --local two-phase pattern for hermetic, offline installs. |
tally/ruby/prefer-secret-mounts-for-build-credentials |
BUNDLE_GITHUB__COM / GEM_HOST_API_KEY / etc. via ARG/ENV — leaks into image cache key data; use RUN --mount=type=secret instead. |
tally/ruby/healthcheck-rails-up-endpoint |
Rails web server runtime image with no HEALTHCHECK — Rails 7.1+ ships /up for free; probe it via the Ruby stdlib's Net::HTTP (no extra apt-get install curl needed). |
Example: what tally fixes
Before:
FROM ruby:3.3-slim
RUN gem install bundler
COPY Gemfile Gemfile.lock ./
ARG RAILS_MASTER_KEY
ENV RAILS_MASTER_KEY=$RAILS_MASTER_KEY
RUN bundle install --without development:test
RUN bundle exec bootsnap precompile app/ lib/
RUN bin/rails assets:precompile
CMD ["bin/rails", "server"]
tally lint Dockerfile flags every line above. Each finding cites the rule code, the corpus base rate, and the upstream documentation that
motivated the rule (Rails generator template, Bundler upgrade notes, bootsnap issue #495, Rails 7.1 release notes, etc.). --fix applies the safe
mechanical edits; --fix-unsafe unlocks the rest.
After (the Rails generator's canonical shape, derived):
# syntax=docker/dockerfile:1
FROM ruby:3.3-slim AS base
WORKDIR /rails
ENV RAILS_ENV=production \
BUNDLE_DEPLOYMENT=1 \
BUNDLE_PATH=/usr/local/bundle \
BUNDLE_WITHOUT="development:test" \
RUBY_YJIT_ENABLE=1
FROM base AS build
RUN --mount=type=bind,source=Gemfile,target=Gemfile \
--mount=type=bind,source=Gemfile.lock,target=Gemfile.lock \
--mount=type=cache,target=${BUNDLE_PATH}/cache,sharing=locked \
bundle install --jobs=4 \
&& rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git \
&& bundle exec bootsnap precompile -j 1 --gemfile
COPY . .
RUN bundle exec bootsnap precompile -j 1 app/ lib/ \
&& SECRET_KEY_BASE_DUMMY=1 bin/rails assets:precompile
FROM base
RUN useradd -u 1000 rails
COPY --chown=rails:rails --from=build /usr/local/bundle /usr/local/bundle
COPY --chown=rails:rails --from=build /rails /rails
USER rails:rails
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD ["ruby", "-rnet/http", "-e", \
"exit Net::HTTP.get_response(URI('http://127.0.0.1:3000/up')).is_a?(Net::HTTPSuccess) ? 0 : 1"]
CMD ["bin/rails", "server"]
Why tally
Modern Dockerfiles deserve modern tooling. tally is opinionated in the right places:
- BuildKit-native: understands modern syntax like heredocs,
RUN --mount=...,COPY --link, andADD --checksum=.... - Fixes, not just findings:
--fixapplies safe, mechanical rewrites;--fix-unsafeunlocks opt-in risky fixes (including AI). - Modernizes on purpose: converts eligible
RUN/COPYinstructions to heredocs, prefers BuildKitADDsources for archives and git repos. - Broad rule coverage: combines Docker's official BuildKit checks, embedded ShellCheck for shell snippets, Hadolint-compatible rules, and
tally-specific rules — including the
tally/ruby/*namespace covered above. - PowerShell-aware: parses full PowerShell syntax for semantic tokens and rule analysis.
- Windows-container aware: detects Windows container OS, understands Windows paths and default shells.
- Registry-aware without Docker: uses a Podman-compatible registry client for image metadata checks (no daemon required).
- Editor + CI friendly: VS Code extension (
wharflab.tally, powered bytally lsp) and outputs for JSON, SARIF, and GitHub Actions annotations. - Single fast Go binary with 92% code coverage on Codecov and 2,900+ Go tests in CI.
Documentation
For installation, usage, configuration, the full rules reference, and per-rule examples and rationale, visit tally.wharflab.com.
The Ruby rules are documented at tally.wharflab.com/rules/tally/ruby.
License
AGPL-3.0-only. See LICENSE for the full license text.