Rails Dockerfile for Production (Rails 8, Ruby 3.4 Slim, Thruster)

This Ruby on Rails Dockerfile follows the Rails 8 production pattern: gems and assets are built in a dedicated stage, then copied into a ruby:3.4-slim runtime that has jemalloc, libvips and SQLite installed. Puma runs behind Thruster as an unprivileged rails user on port 80. Supply your master key at runtime and it's ready for Kamal or any container platform.

CI status
Build passing
Image size (uncompressed)
379.2 MB
Build time
105 s
Runtime image
ruby:3.4-slim
Port
80
Last built
2026-10-15
Dockerfile
# syntax=docker/dockerfile:1
FROM ruby:3.4-slim AS base
WORKDIR /rails
ENV RAILS_ENV=production BUNDLE_DEPLOYMENT=1 BUNDLE_PATH=/usr/local/bundle BUNDLE_WITHOUT=development:test
RUN apt-get update -qq && apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 \
 && rm -rf /var/lib/apt/lists/*

FROM base AS build
RUN apt-get update -qq && apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config \
 && rm -rf /var/lib/apt/lists/*
COPY Gemfile Gemfile.lock ./
RUN bundle install && rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache
COPY . .
RUN bundle exec bootsnap precompile app/ lib/ \
 && SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile

FROM base
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --from=build /rails /rails
RUN groupadd --system --gid 1000 rails \
 && useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash \
 && chown -R rails:rails db log storage tmp
USER 1000:1000
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]
.dockerignore
.git
log/*
tmp/*
storage/*
node_modules/
.env*
config/master.key
Dockerfile
.dockerignore

Before you build

How this Dockerfile works

  1. FROM ruby:3.4-slim AS base

    A shared base stage holds settings and runtime libraries used by both the build stage and the final image, so they are defined only once.

  2. ENV RAILS_ENV=production BUNDLE_DEPLOYMENT=1 BUNDLE_PATH=/usr/local/bundle BUNDLE_WITHOUT=development:test

    Locks Bundler to the Gemfile.lock, installs gems into a known path, and skips development and test groups so they never reach production.

  3. RUN apt-get update -qq && apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 \

    Installs runtime libraries only. jemalloc reduces Ruby memory fragmentation, libvips powers Active Storage image variants, and the package lists are removed afterwards.

  4. RUN apt-get update -qq && apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config \

    Compilers and headers needed for native gem extensions exist only in the build stage and are discarded with it.

  5. RUN bundle install && rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache

    Installs gems and deletes Bundler's download cache, which isn't needed once the gems are compiled.

  6. && SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile

    Precompiles assets with a dummy secret, so the build doesn't need your real credentials. Bootsnap precompilation on the line above speeds up boot.

  7. USER 1000:1000

    Switches to the rails user created just before, which owns only the directories Rails writes to: db, log, storage and tmp.

  8. CMD ["./bin/thrust", "./bin/rails", "server"]

    Thruster sits in front of Puma, adding HTTP caching, compression and X-Sendfile support, and listens on port 80 inside the container.

Build and run

terminal
docker build -t rails-app .
docker run --rm -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> rails-app

The master key is passed at run time and never baked into the image, which is why .dockerignore excludes config/master.key. Then open http://localhost to check that the app boots.

Other Rails Dockerfile variants

This mirrors the Dockerfile that Rails 8 generates, so it's the natural default for new apps and Kamal deployments. Variants for PostgreSQL client libraries, jsbundling with Node, or Alpine bases suit apps with those needs. They appear below once verified.

VariantRuntime imageImage sizeCI
Ruby 3.4 slim + Thruster (Rails 8 style)ruby:3.4-slim379.2 MBPassing

Troubleshooting

ActiveSupport::MessageEncryptor::InvalidMessage when the container starts
RAILS_MASTER_KEY is missing or doesn't match the credentials file. Pass the contents of config/master.key as the environment variable when you start the container.
./bin/thrust: No such file or directory
The app doesn't include Thruster. Add the thruster gem and run bundle binstubs thruster, or change the CMD to start ./bin/rails server directly.
Permission denied writing to storage or tmp
Only db, log, storage and tmp are owned by the rails user. Write to those paths, or add other writable directories to the chown line.

FAQ

Why is jemalloc installed?
Ruby's default memory allocator fragments heavily under multi-threaded Puma. jemalloc usually keeps memory flatter over time, which matters when containers have tight memory limits.
Can I use PostgreSQL instead of SQLite?
Yes. Add the PostgreSQL client library to the runtime packages and its development headers to the build packages, then set DATABASE_URL when starting the container.
Is this the same as the Dockerfile Rails generates?
It follows the same structure and choices as the Rails 8 generator, slightly simplified. That keeps it familiar to anyone who has used rails new or Kamal.