One of the most interesting thing in starting a new project is the possibility to learn new things. For the last year, I had the occasion to work on the ops side of running an app and not just limit myself to the backend programming. It was the occasion to learn more about docker and also aws services and kubernetes. Now I want to share a bit of what I would have wanted to know as a series of articles. The first one will be about running a rails app in docker.

I. the starting point

 

I know you are probably like "Show me your dockerfile already" so here it is.

 

# 1.1
FROM ruby:2.5
# 1.2
RUN apt-get update -qq \
  && apt-get install -y build-essential libpq-dev nodejs zlib1g-dev liblzma-dev \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/*
# 1.3
RUN mkdir /project-dir
WORKDIR /project-dir
# 1.4
COPY . /project-dir
# 1.5
RUN bundle install
 
#1.6
CMD bundle exec rails s -p 80 -b '0.0.0.0'

 

Now I'll explain briefly the steps described above:


1.1 The first step is the base image you will use. While you can build a docker image from scratch by yourself it's easier to start from a prebuilt image containing most of the needed tools. In our case, we start from the official ruby image that comes with ruby and bundler preinstalled.

1.2 Install the system libraries required for the gems you will use. We will clean the downloaded packages after that to have a lighter image that's easier to upload.

1.3 Prepare a place for your code

1.4 Copy your application's code. The first param to the copy command is the location of the code you want to have in your docker image, the second one is where do you want it on the image's file system.

1.5 Install any gems required to run your application.

1.6 Define the command that will be executed when you run the docker image. In the case of a rails app it will be rails s. Note unlike the previous steps this command will  be executed when running the image not when building it.

Now we can test it by using the command : docker build .

Sending build context to Docker daemon  155.6kB
Step 1/7 : FROM ruby:2.5
 ---> 1d8640b852eb
Step 2/7 : RUN apt-get update -qq   && apt-get install -y build-essential libpq-dev nodejs zlib1g-dev liblzma-dev   && apt-get clean   && rm -rf /var/lib/apt/lists/*
 ---> Using cache
 ---> dcdb4cb153f0
Step 3/7 : RUN mkdir /project-dir
 ---> Using cache
 ---> 9879d7c50311
Step 4/7 : WORKDIR /project-dir
 ---> Using cache
 ---> c784893eae58
Step 5/7 : COPY . /project-dir
 ---> 612e8edf20e8
Step 6/7 : RUN bundle install
 ---> Running in 31203bd4dc81
Fetching gem metadata from https://rubygems.org/............
Fetching rake 12.3.2
Installing rake 12.3.2
Fetching concurrent-ruby 1.1.5
Installing concurrent-ruby 1.1.5
...
Removing intermediate container 31203bd4dc81
 ---> 2a7c8c394066
Step 7/7 : CMD bundle exec rails s -p 80 -b '0.0.0.0'
 ---> Running in 2fb8acf6082a
Removing intermediate container 2fb8acf6082a
 ---> e33b3d9250ff
Successfully built e33b3d9250ff
 

I cut a bit the logs in step 6 to save space.

Note how every command in the dockerfile result in a step. The result of each step is cached so the next builds will be faster. If let say you change your code but no new general library is defined in the dockerfile the new build will reuse the cached versions for steps 1 to 4.

You can check if the image was built successfully by using the command: docker images

 

REPOSITORY                                                                 TAG                 IMAGE ID            CREATED             SIZE
<none>                                                                     <none>              e33b3d9250ff        5 minutes ago       1GB
 

 

Since we built the image without passing any extra params the name and tag of the resulting image is none. The most important info to us is image id that will be useful to run the image.

Now that you have the image id you can also run it: docker run -p 8000:80 e33b3d9250ff

The -p param will map the ports. the first one is the port from which you want to be able to access your container, the second one should be the one set in the dockerfile (Check the -p argument in the CMD command at the end). You can now access the rails application on localhost:8000.

 

II. the next step

The simple flow described in the previous part is enough to run your application but if you need to build often we can optimize it a bit.

1. bundle install

One of the most time-consuming steps is gem installation and it's executed with even the smallest change made to the codebase of the application. But we can use the step caching I mentioned before to help us avoid it when it's not necessary.

To achieve this we will first copy just the gemfile of the app and install it and then copy the rest of the files. This way if no changes to the gemfile were made we won't need to reinstall the gems.

2. Assets

When hosting the website in production you may need to also generate assets. we will add this step also. This will recompile assets with each build which is okay for now.

So this is the final dockerfile:


 

FROM ruby:2.5
RUN apt-get update -qq \
  && apt-get install -y build-essential libpq-dev nodejs zlib1g-dev liblzma-dev \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/*
RUN mkdir /project-dir
WORKDIR /project-dir
COPY Gemfile /project-dir/Gemfile
COPY Gemfile.lock /project-dir/Gemfile.lock
RUN bundle install
COPY . /project-dir
RAILS_ENV=production bundle exec rake assets:precompile
 
CMD bundle exec rails s -p 80 -b '0.0.0.0'
 
 

Thanks for reading! :)