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.