Container Builds

Optimal Dockerfile for Ruby on Rails with Bundler

Below is an example Dockerfile that we recommend at Depot for building images for Ruby on Rails applications with Bundler.

# syntax=docker/dockerfile:1

FROM ruby:3.4 AS build

WORKDIR /app

ENV RAILS_ENV=production

COPY Gemfile ./

RUN bundle config set --local without 'development test' && \
    bundle config set --local jobs $(nproc)

RUN --mount=type=cache,target=/usr/local/bundle/cache \
    --mount=type=cache,target=/app/vendor/cache \
    bundle cache && \
    bundle install && \
    bundle clean --force

COPY . .

FROM ruby:3.4-slim AS runtime

RUN groupadd -g 1001 appgroup && \
    useradd -u 1001 -g appgroup -m -d /home/appuser -s /bin/bash appuser

WORKDIR /app

COPY --from=build --chown=appuser:appgroup /app .
COPY --from=build --chown=appuser:appgroup /usr/local/bundle /usr/local/bundle

RUN mkdir -p tmp/pids tmp/cache log storage && \
    chown -R appuser:appgroup tmp log storage

ENV RAILS_ENV=production \
    RUBY_YJIT_ENABLE=1 \
    BUNDLE_WITHOUT=development:test

USER appuser

ENTRYPOINT ["bundle", "exec", "puma", "-C", "config/puma.rb"]

Explanation of the Dockerfile

At a high level, here are the things we're optimizing in our Docker build for a Ruby application with Bundler:

  • Multi-stage builds for cleaner separation
  • Bundler cache mounts for faster dependency installation
  • YJIT enabled for improved Ruby performance
  • Security optimizations with non-root users

Stage 1: FROM ruby:3.4 AS build

FROM ruby:3.4 AS build

We use the official Ruby 3.4 image as our build stage base. This provides a full Ruby environment with all necessary build tools for compiling native gems.

Environment and dependency configuration

WORKDIR /app

ENV RAILS_ENV=production

COPY Gemfile ./

RUN bundle config set --local without 'development test' && \
    bundle config set --local jobs $(nproc)

We set the production environment and configure Bundler:

  • without 'development test' excludes development and test gems
  • jobs $(nproc) enables parallel gem installation using all available CPU cores

Gem installation with caching

RUN --mount=type=cache,target=/usr/local/bundle/cache \
    --mount=type=cache,target=/app/vendor/cache \
    bundle cache && \
    bundle install && \
    bundle clean --force

We install gems with dual cache mounts for maximum build efficiency:

  • bundle cache downloads and caches gems locally before installation
  • bundle install installs the cached gems
  • bundle clean --force removes any gems not in the current Gemfile, keeping the installation clean

The dual cache mounts optimize both Bundler's internal cache and the vendor cache directory.

COPY . .

Stage 2: FROM ruby:3.4-slim AS runtime

FROM ruby:3.4-slim AS runtime

RUN groupadd -g 1001 appgroup && \
    useradd -u 1001 -g appgroup -m -d /home/appuser -s /bin/bash appuser

The runtime stage uses Ruby 3.4 slim image for a smaller footprint and creates a non-root user for security:

  • groupadd creates a group with GID 1001
  • useradd creates a user with UID 1001, home directory, and bash shell
WORKDIR /app

COPY --from=build --chown=appuser:appgroup /app .
COPY --from=build --chown=appuser:appgroup /usr/local/bundle /usr/local/bundle

We copy the application and installed gems from the build stage with proper ownership.

Application setup and permissions

RUN mkdir -p tmp/pids tmp/cache log storage && \
    chown -R appuser:appgroup tmp log storage

ENV RAILS_ENV=production \
    RUBY_YJIT_ENABLE=1 \
    BUNDLE_WITHOUT=development:test

USER appuser

ENTRYPOINT ["bundle", "exec", "puma", "-C", "config/puma.rb"]

We create necessary directories for Rails runtime files (PIDs, cache, logs, storage) with correct permissions and configure the runtime environment:

  • RAILS_ENV=production sets the Rails environment
  • RUBY_YJIT_ENABLE=1 enables YJIT for improved Ruby performance (Ruby 3.1+)
  • BUNDLE_WITHOUT=development:test ensures development gems aren't loaded
  • Puma web server with configuration file

Understanding BuildKit Cache Mounts

Cache mounts in this Dockerfile speed up builds by persisting the package manager cache. This means that even when a layer needs to be rebuilt, your package manager only fetches what's new or updated. This Dockerfile uses dual cache mounts:

RUN --mount=type=cache,target=/usr/local/bundle/cache \
    --mount=type=cache,target=/app/vendor/cache \
    bundle cache && \
    bundle install && \
    bundle clean --force

Cache Mount Parameters Explained

  • type=cache: Specifies this is a cache mount that persists across builds.

  • Bundler cache mounts:

    • target=/usr/local/bundle/cache: Mount point for Bundler's internal gem cache
    • target=/app/vendor/cache: Mount point for vendored gem cache

For more information regarding Bundler cache mounts, please visit the official Bundler documentation.