Shrink Ruby Docker Images

Khimananda Oli 8 min read Programming and Languages
Shrink Ruby Docker Images

By Khimananda Oli | Last reviewed: August 2026

Bloated containers are a silent tax on your infrastructure budget and deployment velocity. When you shrink Ruby Docker images, you reduce attack surface, accelerate CI/CD pipelines, and lower storage costs across registries and clusters. This guide walks through the exact multi-stage build patterns, base image selections, and dependency pruning techniques I use to cut production Ruby image sizes from over 1GB to under 300MB without breaking runtime functionality.

How do you structure a multi-stage build to shrink Ruby Docker images?

The single most effective technique to shrink Ruby Docker images is the multi-stage build pattern. This separates the heavy compilation environment from the lean runtime environment. In my experience auditing containerized Rails applications for SOC 2 compliance, teams often skip this step and ship 1.2GB images containing compilers, headers, and git repositories that serve no purpose in production. For a deeper understanding of layer optimization, see how to reduce Docker image size with multi-stage builds.

Builder Stageruby:3.3 (full)build-essentiallibpq-dev / gitbundle installRuntime Stageruby:3.3-slimApp Code OnlyCompiled GemsNo Build ToolsCOPY --from=builderResult~250-300 MBvs 1.2 GB naive75% smallerAudit-ready
Multi-stage build architecture separating compilation from runtime to shrink Ruby Docker images effectively

Defining the builder stage

Your builder stage needs every tool required to compile native extensions. This includes C compilers, development headers for databases, and version control for fetching gems directly from Git repositories. Never install these in your final image.

# syntax=docker/dockerfile:1
FROM ruby:3.3-bookworm AS builder

RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    libpq-dev \
    git \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment 'true' \
    && bundle config set --local without 'development test' \
    && bundle install -j $(nproc)

Assembling the minimal runtime stage

The runtime stage starts fresh from a slim base. You copy only the compiled vendor/bundle directory and application code. Crucially, you must reinstall any runtime-only system libraries that native gems depend on, as the builder's libraries are not transferred.

FROM ruby:3.3-slim-bookworm AS runtime

RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq5 \
    curl \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY --from=builder /app/vendor/bundle ./vendor/bundle
COPY . .

ENV BUNDLE_PATH=/app/vendor/bundle \
    BUNDLE_WITHOUT=development:test \
    RAILS_ENV=production

EXPOSE 3000
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]

Which base image should you choose when optimizing Ruby containers?

Base image selection dictates your minimum possible size. The official Ruby images vary dramatically in footprint, and choosing incorrectly can add hundreds of megabytes before you even write a line of application code. Always pin specific versions rather than using latest tags to ensure reproducible builds and simplify security patching during audits.

Base ImageApproximate SizeUse CaseTrade-offs
ruby:3.3~1.1 GBDevelopment, debuggingContains compilers, docs, man pages; never use in production
ruby:3.3-slim~220 MBMost production Rails/Sinatra appsRequires explicit runtime library installation; best balance
ruby:3.3-alpine~180 MBExtreme size constraints, edge/IoTmusl libc causes native gem compilation issues; slower builds
distroless/ruby~150 MBHigh-security, compliance-heavy environmentsNo shell or package manager; difficult to debug; advanced setup

In practice, ruby:3.3-slim-bookworm is the default recommendation for 2026. It uses Debian Bookworm as its foundation, which provides excellent compatibility with native gems while stripping documentation, locales, and unnecessary utilities. Alpine saves another 40MB but introduces musl-related pain points with gems like nokogiri, pg, or grpc that assume glibc. Unless you have measured evidence that Alpine's savings justify the maintenance burden, slim is the pragmatic choice.

How do you prune gems and assets to reduce final image size?

Gem bloat is the second-largest contributor to oversized Ruby images after the base OS. Development and test gems like rspec, rubocop, pry, and faker have no place in production containers. Beyond excluding groups, you should actively clean caches and remove unnecessary gem artifacts.

Gemfile Groupsdefault (production)developmenttestassets (optional)production extrasBundle ConfigBUNDLE_WITHOUT=dev:testBUNDLE_DEPLOYMENT=trueBUNDLE_CLEAN=true--no-cache --no-docFinal Vendor BundleProduction gems onlyNative extensions compiledNo .git directoriesNo gem cachesDocumentation strippedSaves 150-400 MB
Gem pruning strategy eliminating development dependencies and caches to shrink Ruby Docker images

Excluding non-production gem groups

Configure Bundler to skip development and test groups at install time. This prevents those gems from ever being downloaded or compiled in the builder stage.

RUN bundle config set --local deployment 'true' \
    && bundle config set --local without 'development test' \
    && bundle config set --local clean 'true' \
    && bundle install -j $(nproc) --retry 3

Cleaning gem caches and documentation

Even with groups excluded, Bundler retains cached .gem files and installs RI/RDoc documentation by default. Strip these in the same RUN instruction to avoid creating an extra layer.

RUN bundle install -j $(nproc) \
    && rm -rf vendor/bundle/ruby/*/cache/*.gem \
    && find vendor/bundle -name "*.rdoc" -delete \
    && find vendor/bundle -type d -name ".git" -exec rm -rf {} + 2>/dev/null || true

Handling asset pipeline dependencies

If your Rails app precompiles assets during the build, you need Node.js and Yarn in the builder stage but not in runtime. Install them conditionally in the builder and exclude them entirely from the runtime stage. For apps serving static assets via CDN or a separate Nginx container, consider removing sprockets or propshaft from the production group entirely.

What common mistakes prevent shrinking Ruby Docker images effectively?

I review dozens of Ruby Dockerfiles quarterly for compliance and cost optimization engagements. The same anti-patterns appear repeatedly, each adding 50-300MB of unnecessary weight. Avoiding these is often more impactful than exotic optimization techniques.

  • Running apt-get update and install in separate layers: Each RUN instruction creates a new layer. If apt-get update is in one layer and apt-get install in the next, the package lists persist forever even after cleanup. Always combine them in a single RUN with && rm -rf /var/lib/apt/lists/* at the end.
  • Copying the entire repository before bundle install: This invalidates the gem cache on every code change. Copy only Gemfile and Gemfile.lock first, run bundle install, then copy the rest of the application. This alone can cut CI build times by 60%.
  • Using COPY instead of ADD incorrectly: ADD auto-extracts tars and fetches URLs, adding metadata overhead. Use COPY unless you specifically need extraction. Never use ADD for simple file transfers.
  • Forgetting .dockerignore: Without a proper .dockerignore, your context includes .git, node_modules, tmp, log, and .env files. These get sent to the daemon and may accidentally be copied into the image. A missing .dockerignore is also a frequent finding in security audits; see container image scanning with Trivy for detection.
  • Installing runtime libraries in the builder but not the runtime stage: Native gems like pg link against shared libraries at runtime. If you install libpq-dev in the builder but forget libpq5 in the runtime stage, the container crashes on boot with cryptic linker errors.

Verifying image composition

After building, inspect what actually made it into the final image. Use dive or docker history to audit each layer's contribution.

# Analyze layer-by-layer size
dive your-ruby-app:latest

# Check for unexpected large files
docker run --rm your-ruby-app:latest find / -xdev -size +10M -exec ls -lh {} \;

# Verify no build tools leaked
docker run --rm your-ruby-app:latest which gcc make git || echo "Clean"

How does image size impact Kubernetes deployments and compliance?

Image size is not just a storage concern; it directly affects cluster operations, security posture, and audit outcomes. In Kubernetes environments, larger images mean slower pod startup times, increased node pressure during rolling updates, and higher egress costs when pulling across regions. For teams managing Kubernetes resource limits and requests, bloated images force higher memory reservations because the entire filesystem must be mapped.

Bloated Image (1.2 GB)Pull time: 45-90 secondsPod startup: 30+ secondsCVE surface: 400+ packagesRegistry storage: $12/monthAudit findings: HighRollback window: ExtendedOptimized Image (280 MB)Pull time: 8-15 secondsPod startup: <5 secondsCVE surface: ~80 packagesRegistry storage: $3/monthAudit findings: MinimalRollback window: Fast75% reduction
Operational impact comparison demonstrating why teams shrink Ruby Docker images for Kubernetes and compliance

From a compliance perspective, every additional package in your image is a potential vulnerability that must be scanned, documented, and justified during SOC 2 or ISO 27001 audits. Smaller images with fewer components produce cleaner scan reports and reduce the evidence collection burden. When auditors ask why gcc or vim exists in a production container, "we didn't optimize the Dockerfile" is not an acceptable answer. Proactively shrinking your images demonstrates deliberate security governance.

Next Steps for Production Ruby Container Optimization

Start by implementing the multi-stage build pattern with ruby:3.3-slim-bookworm as your runtime base. Measure your current image size with docker images, apply the techniques above, and measure again. Most teams see 60-80% reductions on their first pass. Integrate dive into your CI pipeline to catch regressions before they reach production. If you're managing multiple services or need help establishing container standards across your organization, reach out to discuss your infrastructure optimization goals.

Frequently Asked Questions

Alpine Linux remains the smallest option at under 50MB, but requires compiling native gems. Debian slim offers better compatibility with only slightly larger size, typically around 80MB to 100MB depending on the Ruby version installed.

Multi-stage builds typically reduce final image size by sixty to eighty percent by excluding build tools, headers, and development dependencies that are only needed during gem compilation and asset precompilation phases.

Yes, Google distroless Ruby images work well for production since they contain only the runtime and essential libraries. They lack shells and package managers, improving security while keeping images under 150MB for most Rails applications.

Check for leftover cache directories, unremoved build artifacts, or unnecessary system packages. Run docker history to identify bloated layers and verify your Dockerfile removes apt caches and gem documentation in the same layer they were created.

Absolutely. Setting bundle config set without development test excludes unused gems from installation, significantly reducing image footprint and improving container startup time by skipping unnecessary dependency resolution and native extension compilation.

Precompile inside using a dedicated build stage to leverage layer caching. Copy only the compiled public assets to the final runtime stage, avoiding Node.js and Yarn dependencies in your production image entirely.

Ruby-slim provides better glibc compatibility and faster builds since most gems ship precompiled binaries. Alpine saves roughly 30MB but often requires recompiling native extensions, increasing CI time and potential runtime issues with certain libraries.

Add --no-document to your bundle install command or set BUNDLE_FORCE_RUBY_PLATFORM=false in environment variables. This skips rdoc and ri generation, saving 20MB to 50MB depending on your gem dependency tree complexity.

Use dive to inspect individual layer sizes and identify bloat sources. Docker scout and trivy also provide layer analysis alongside vulnerability scanning, helping prioritize which optimizations yield the greatest size reductions for your specific application.

Yes. Smaller images pull faster across nodes, reducing cold start times and scaling latency. A 200MB reduction can cut pod startup by fifteen seconds on typical cluster networks, directly impacting autoscaling responsiveness during traffic spikes.

Yes, removing unused locales from /usr/share/locale saves 50MB to 100MB. Keep only en_US or your application's required locales. Most Ruby applications never access system locale data beyond basic UTF-8 encoding support.

Yarn cache can add 200MB or more if not cleaned. Always run yarn cache clean in the same RUN instruction as yarn install, or better yet, use multi-stage builds to exclude Node tooling from the final runtime image completely.

Over-minimization can remove critical CA certificates, timezone data, or shared libraries causing silent failures. Always test thoroughly in staging and use distroless or slim variants maintained by official sources rather than custom-stripped base images.

Aim for 200MB to 300MB for typical Rails applications in 2026. Images under 150MB are achievable for simple APIs, while complex apps with many native gems may reasonably reach 400MB after proper optimization.

No direct size difference exists, but ARG values don't persist in final images while ENV values do. Use ARG for build-time configuration to avoid embedding sensitive or unnecessary metadata that increases image inspection overhead and potential attack surface.