Skip to Content
Course content

231: Dockerizing a Rails Application

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably heard the phrase "it works on my machine" enough times to last a lifetime. Docker is the industry's answer to that frustration, but there is a massive gap between "making it run in a container" and "making it run correctly." When I first started dockerizing Rails apps, I treated the Dockerfile like a bash script—just a list of commands to get the server up. It worked, but it was slow, bloated, and frankly, a security nightmare.

The "Just Make It Work" Approach

The naive way to Dockerize a Rails app is to throw everything into one giant layer. You might write a Dockerfile that starts with FROM ruby:latest, runs a massive apt-get install for every possible dependency, copies your entire project directory, and then runs bundle install. It looks something like this:

FROM ruby:latest
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
COPY . /app
WORKDIR /app
RUN bundle install
CMD ["rails", "server", "-b", "0.0.0.0"]

On the surface, this is fine. Your app boots. But in a professional workflow, this approach falls apart quickly. Because you COPY . /app before running bundle install, Docker sees any change to a single CSS file or a README as a reason to invalidate the cache for the entire project. This means every time you change one line of code, you're stuck waiting several minutes for your gems to reinstall. I've seen developers waste hours of their week just waiting for "fast" containers to build because they didn't understand layer caching.

Building for Speed and Security

To do this right, we have to think about the order of operations. Docker caches each line (layer) of your Dockerfile. If a line hasn't changed, Docker skips it. The trick is to move the things that change infrequently to the top and the things that change constantly to the bottom.

First, we stop using latest. Using ruby:3.2-slim (or whatever version you're on) keeps the image size down and prevents your app from randomly breaking when a new Ruby version drops. Second, we copy the Gemfile and Gemfile.lock alone, run bundle install, and then copy the rest of the application code. This way, as long as you aren't adding new gems, the bundle install step is cached and takes milliseconds instead of minutes.

Then there's the security aspect. By default, Docker runs everything as root. If a vulnerability in your Rails app allows an attacker to execute code, they have root access to the container, which is a huge liability. I always tell my juniors: never run your app as root in production. Creating a dedicated rails user is a non-negotiable step for any app hitting a real server.

The Professional Blueprint

When you put these trade-offs together, you get a Dockerfile that is leaner and significantly faster to iterate with. We also add a .dockerignore file to prevent us from copying log/, tmp/, and node_modules/ into the image, which would otherwise bloat the size and potentially overwrite container settings with local environment junk.

# Use a specific, slim version of Ruby
FROM ruby:3.2.2-slim

# Install only the essential runtime dependencies
RUN apt-get update -qq && \
    apt-get install -y build-essential libpq-dev nodejs && \
    rm -rf /var/lib/apt/lists/*

# Create a non-root user for security
RUN useradd -m rails
USER rails
WORKDIR /home/rails/app

# Cache gems by copying Gemfiles first
COPY --chown=rails:rails Gemfile Gemfile.lock ./
RUN bundle install

# Now copy the rest of the app
COPY --chown=rails:rails . .

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

Notice the --chown=rails:rails flag. Without it, the files are copied as root, and your rails user won't have permission to write to tmp/ or log/, leading to those annoying "Permission Denied" errors that usually lead people to just chmod 777 everything (please, don't do that).




📋 Practical Task

Exercise: Optimizing the Bloated Rails Image

You have inherited a legacy project with a Dockerfile that is taking 10 minutes to build every time a developer changes a single line of Ruby code. The current Dockerfile is as follows:

FROM ruby:latest
RUN apt-get update && apt-get install -y build-essential libpq-dev
COPY . /app
WORKDIR /app
RUN bundle install
CMD ["rails", "server", "-b", "0.0.0.0"]

Your task: Rewrite this Dockerfile to implement the following professional standards:

  • Switch to a slim image version (assume Ruby 3.2.2).
  • Implement layer caching so that bundle install only runs when the Gemfile or Gemfile.lock changes.
  • Create and use a non-root user named deploy to run the application.
  • Ensure the files are copied with the correct ownership for the deploy user.
  • Clean up the apt cache in the same layer as the installation to keep the image small.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.