The Dockerfile Rails gave you
rails new writes a Dockerfile. Most developers never open it, deploy with Kamal, and it works. That is a good outcome and a missed opportunity: the file is a compact lesson in production containers, and it is already in your repository.
This app has one. Its first lines say what it is for:
# syntax=docker/dockerfile:1
# check=error=true
# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
# docker build -t newbie_on_rails .
# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name newbie_on_rails newbie_on_rails
Two directives before anything else. # syntax=docker/dockerfile:1 opts into the current Dockerfile frontend rather than whatever the daemon ships, which is what makes the modern features in lesson 4 available. # check=error=true turns build lint warnings into failures, so a mistake stops the build instead of being scrolled past.
The vocabulary, in one paragraph
An image is a filesystem plus metadata about how to start a process. It is a build artifact: immutable, identified by a digest. A container is a running instance of one. A layer is a filesystem diff produced by one build instruction. Images are stacks of layers, which can be shared between images and cached between builds.
Those last two sentences are the whole of lessons 2 and 4. Almost every Dockerfile optimization is a statement about layers.
Reading this app's build
The file has three FROM lines, which means three stages:
ARG RUBY_VERSION=4.0.1
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
...
FROM base AS build
...
FROM base
The base stage installs what the app needs to run (curl, libjemalloc2, libvips, postgresql-client) and sets the production environment variables.
The build stage adds what is needed to compile: build-essential, git, libpq-dev, libyaml-dev, pkg-config. It installs gems, copies the app, and precompiles assets.
The final stage starts from base again, not from build, and copies in only the finished artifacts:
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --from=build /rails /rails
A C compiler, the gem cache and the build tooling never reach the shipped image. That is a multi-stage build, and lesson 3 explains why it matters so much.
Choices worth noticing
Four lines in this file each encode a decision you would otherwise learn the hard way.
ENV RAILS_ENV="production" \
BUNDLE_DEPLOYMENT="1" \
BUNDLE_PATH="/usr/local/bundle" \
BUNDLE_WITHOUT="development"
BUNDLE_DEPLOYMENT="1" makes bundler refuse to update Gemfile.lock. The image is built from the lockfile you committed, or the build fails. Bundler never quietly resolves something different.
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
Asset precompilation boots the app, and booting normally wants a secret. SECRET_KEY_BASE_DUMMY=1 supplies a throwaway one, so your real master key is never needed at build time. It never ends up in a build argument, a layer, or a CI log.
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
The container runs as a non-root user, and only the four directories that must be writable are given to it. Everything else stays read-only to the app process.
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]
The default command runs the server behind Thruster, which this app has in its Gemfile. Thruster sits in front of Puma and adds HTTP caching, compression and X-Sendfile acceleration.
What this course is for
The generated file is good. You will still need to change it: a system package a gem needs, a build step for something Rails does not know about, a different base image, a faster CI build. Change a Dockerfile you do not understand, and a five-minute edit becomes a two-gigabyte image with the master key baked into layer four.
- The Dockerfile Rails gave you (you are here).
- Layers and the build cache.
- Multi-stage builds, and what gets thrown away.
- Making builds fast.
- Making images small and safe.
- What happens at runtime.
- Putting it together: a custom Dockerfile.
Open Dockerfile in this repository as you read. Every example in this course is that file.
Key takeaways
rails newgenerates a production Dockerfile; it is a compact lesson in container builds sitting in your repo.- An image is a stack of layers, a container is a running instance, and a layer is the diff from one instruction. Most optimization is about layers.
- This app's file has three stages:
basefor runtime packages,buildfor compilation, and a final stage that copies only finished artifacts back ontobase. BUNDLE_DEPLOYMENT=1builds strictly from the committed lockfile, andSECRET_KEY_BASE_DUMMY=1keeps your real master key out of the build entirely.- The image runs as a non-root user with only
db,log,storageandtmpwritable.
Keep going with lesson 2
Layers and the build cache
How each instruction becomes a cached layer, what invalidates it, and why instruction order matters.
A free account also gives you the quizzes, your progress and notes, and the discussion.