Minimal Rails application

Minimal Rails with Full Ruby Image

Create an empty directory, and place the following Dockerfile in there:

# syntax = docker/dockerfile:1

FROM ruby

RUN gem install rails
RUN rails new demo --minimal --skip-active-record

COPY <<-"EOF" /demo/config/routes.rb
Rails.application.routes.draw { root "rails/welcome#index" }
EOF

WORKDIR demo
ENV RAILS_ENV=production
EXPOSE 3000
CMD bin/rails server

If you’ve ever seen a Dockerfile before, the above seems too good to be true. But try it. In your new directory run flyctl launch. Accept all the defaults, then run flyctl deploy, followed by flyctl apps open.

Congratulations, you’ve deployed a Rails app. True, it doesn’t do much, but we will add more later.

Now let’s review that file. The first line is a syntax directive. All dockerfiles should have this. In fact, this example wouldn’t work without it as it uses here docs.

The next line, FROM ruby, specifies a base image. A base image contains a completely preconfigured operating system along with selected other packages. You can find the definition for the ruby image on hub.docker.com.

Next up is two RUN statements. The commands you see there should be familiar: gem install rails and rails new demo. RUN statements are run on the build machine.

Then comes a COPY statement. This statement uses a heredoc which pretty much are the same thing as heredoc in Ruby. The effect of these three lines will be to replace config/routes.rb with a route that shows the Rails welcome page.

WORKDIR and ENV unsurprisingly set the current working directory and an environment variable respectively.

The EXPOSE 3000 line declares the network port that Rails will listed to at runtime.

Finally, there is a CMD which is run on the deployed server as opposed to the build server. It is run in the specified WORKDIR with the environment variables you set.

EZ-Peasy. If you didn’t see it working with your own eyes, it would be hard to imagine it being this straightforward and simple. None of the Dockerfiles you have seen before are as clear as this, so there must be more to this story.

Minimal Rails with Slim Ruby Image

Scrolling by during the build was the size of the image. That size was 948MB. That’s compressed. That seems a bit much for a welcome splash screen. Large images take longer to build and longer to deploy. Let’s make the image smaller.

Returning to hub.docker.com, you can see a lot of alternatives. If you scan that page and find the word slim, you can click on it and find the Dockerfile that was used to create that image. That image, in turn, is based on bullseye, which is the latest release of the Debian distribution of LInux. Knowing this is important.

While the full ruby image contains everything you might need, including plenty that you will never use, ruby:slim doesn’t contain things that will be needed to generate your demo application. Things like make and git.

Scanning the list of debian bullseye packages you can find build-essential and git. OK, at this point, I’m just kidding. I ran a count and found there to be 58,733 packages on that list. The best way to find out what you need is pasting the error message you find when you don’t include what is needed in your favorite search engine and reviewing the answer you find on places like StackOverflow.

The way to install packages on Debian is to use a tool named apt-get, so when we are done, the FROM ruby line gets replaced with the following:

FROM ruby:slim

RUN apt-get update &&\
    apt-get install --yes build-essential git

Note that this time we opted to use a single RUN statement, and use the shell && operator to run the second command only if the preceding one completed. This is a common technique you will find in Dockerfiles and it marginally makes the images smaller as two separate steps are combined into one.

All in all not too bad. Not exactly novice friendly, but still fairly concise.

Concise, and effective. With this one change we got the image size down from 948MB to 520MB, a 45% reduction. Not bad.

But we can do even better.

Multi-stage build

Build tools are only needed at build time, they don’t need to be included in the deployed image.

This is accomplished by building two images. In the first one we build the necessary build tools. We then start fresh with a new image, copy over the necessary files from the first image and then proceed on from there. We can use multi-stage builds to do that.

This is easier than it sounds. You put multiple FROM statements in your Dockerfile, optionally providing aliases to some images with an as keyword, and then use the --from flag on your COPY commands. Seeing it in action helps. Start by adding as build to the existing FROM command, then add the following immediately after the RUN rails new demo line:

FROM ruby:slim

COPY --from=build /demo /demo
COPY --from=build /usr/local/bundle /usr/local/bundle

Your deployed application will contain your demo application as well as all of the necessary gems, but none of the build tools used to create the application. The compressed image size is now down to 209MB. While still substantial, this is a dramatic 78% improvement over where we started.

At this point you might be wondering where /usr/local/bundle came from. Where bundler places installed gems is operating system specific, and can be overridden by setting the GEM_HOME environment variable. Bundler has a docker guide that provides more information.

Recap

A recap of the progress so far. The entire Dockerfile isn’t that big:

# syntax = docker/dockerfile:1

FROM ruby:slim as build

RUN apt-get update &&\
    apt-get install --yes build-essential git

RUN gem install rails
RUN rails new demo --minimal --skip-active-record

FROM ruby:slim

COPY --from=build /demo /demo
COPY --from=build /usr/local/bundle /usr/local/bundle

COPY <<-"EOF" /demo/config/routes.rb
Rails.application.routes.draw { root "rails/welcome#index" }
EOF

WORKDIR demo
ENV RAILS_ENV=production
EXPOSE 3000
CMD bin/rails server

A total of 23 lines, 7 of them blank.

The docker portions (FROM, RUN, COPY, WORKDIR, ENV, CMD) are very straightforward, even including advanced techniques such as multi-stage builds and here docs.

The parts that need explaining are the operating system parts: apt-get, build-essential, /usr/local/bundle, and 3000. This will remain true as you continue your journey: the hard part isn’t learning Docker, but rather getting to know what for many of you is an entirely new operating system.

In this case, the operating system is Debian, but Alpine is common, and you will occasionally see member of the RedHat family (Centos, Fedora) as well as others.

Docker files make working with these operating systems easy, but do nothing to hide them.

To be fair, there still is a bit more to learn about Dockerfiles, but mostly the above statement will continue to be true.

Downloads