
Table of Contents
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.
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 Image | Approximate Size | Use Case | Trade-offs |
|---|---|---|---|
ruby:3.3 | ~1.1 GB | Development, debugging | Contains compilers, docs, man pages; never use in production |
ruby:3.3-slim | ~220 MB | Most production Rails/Sinatra apps | Requires explicit runtime library installation; best balance |
ruby:3.3-alpine | ~180 MB | Extreme size constraints, edge/IoT | musl libc causes native gem compilation issues; slower builds |
distroless/ruby | ~150 MB | High-security, compliance-heavy environments | No 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.
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 updateis in one layer andapt-get installin 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
GemfileandGemfile.lockfirst, runbundle install, then copy the rest of the application. This alone can cut CI build times by 60%. - Using COPY instead of ADD incorrectly:
ADDauto-extracts tars and fetches URLs, adding metadata overhead. UseCOPYunless you specifically need extraction. Never useADDfor simple file transfers. - Forgetting .dockerignore: Without a proper
.dockerignore, your context includes.git,node_modules,tmp,log, and.envfiles. 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
pglink against shared libraries at runtime. If you installlibpq-devin the builder but forgetlibpq5in 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.
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.