Dockerizing Rails Applications Part 1: Writing the Dockerfile

Let’s start dockerizing a Rails application and writing an optimized Dockerfile

Building Docker images for services and applications can be done by either committing new Docker images from running containers or by defining the Docker image in a Dockerfile for the corresponding service. In this article, I’ll present some of the best practices for writing Dockerfiles for Ruby/Rails applications. This is also the first of three posts that describes some of the best practices for dockerizing Ruby/Rails applications.

To be able to run applications/services in Docker containers, it’s necessary to build a Docker image that includes the application source code or binary and all of its dependencies. This task can be done by performing the following steps:

Create a Dockerfile for the service : The Dockerfile describes how to build the Docker image, set default configurations for the Docker image, and install all the dependencies for the application

: The describes how to build the Docker image, set default configurations for the Docker image, and install all the dependencies for the application Build the Docker image using the Docker command docker build

Starting with the first step, this link presents the supported Dockerfile instructions, as well as some best practices. I’d highlight the following ones:

Chose the base image carefully: The first instruction of any Dockerfile is the FROM instruction. This instruction is specifying the base image for your Docker image. The next instructions in the file will take care of preparing the source code and installing software dependencies. Things that need to be considered when choosing a base image are:

Use only official images as the basis for your images to reduce the risk of introducing vulnerabilities into the Docker images

Use small-size images like Alpine image: This will reduce the time needed for deployment/rollout and rollback. The application Docker image needs to be downloaded on the Docker host before creating new containers form that image.

Create your own base images to reduce the build time for the application Docker image. This is important in case you install Linux application dependencies and libraries during the Docker build time. The installation of these packages and libraries could take a while, and this may increase the deployment time (if building Docker images is part of the deployment), or it’ll affect the execution time of CI/CD pipelines because the dependencies need to be installed on each docker build. As a solution, it’s recommended to build base images that include all the dependencies, such as the Ruby version, and then use these images as base images for the applications. This post describes how to build base Docker images for Ruby applications.

Always use entrypoint : entrypoint s help us create a simple interface for the Docker images by hiding the complexity of the application commands or scripts behind one keyword/command. This is also helpful for applications that need to perform some kind of logic before running the actual service — for instance, if you’d like to generate some files from templates based on environment variables and then start your service.

entrypoint can be a simple shell script that can handle the supported commands or service of the Docker image. The only requirement for the Docker entrypoint files is that it should be an executable file. For example, to add a Docker entrypoint for a Rails application that provide two services — a web server and a delayed jobs worker — the below instructions can be followed:

Create the Docker entrypoint with the following content.

Note that the above entrypoint is also allowing other commands by using the *) case. If you’d like to restrict the Docker image to only the supported commands, you can remove the last case. Executing other commands will be only feasible if the Docker entrypoint is overwritten.

Make the file executable: chmod a+x docker-entrypoint.sh .

. Copy the file to the Docker image and set the ENTRYPOINT .

COPY ./docker-entrypoint.sh /

ENTRYPOINT ["/docker-entrypoint.sh"]

Minimize the size of the Docker image: Luckily in the recent version of Docker, only the following instructions— RUN , COPY , and ADD — will result in new Docker image layers, and as a result, the size of the docker image will be increased. To keep the Docker image size as small as possible, you may consider the following points

Only add the needed files to the Docker image

Only install the needed packages and libraries to the Docker image

Combine multiple RUN commands together (in a single RUN command) if possible. This will reduce the number of Docker layers. For instance, instead of adding five separate RUN instructions to install packages in the Dockerfile (which will introduce five Docker layers in the generated Docker image), try to combine this instruction in one instruction to reduce the Docker layers to only one.

# Instead of RUN addgroup -g 1000 rails

RUN adduser -S -G rails -u 1000 -h /application rails

RUN apk update

RUN apk add linux-headers build-base curl zlib-dev libxml2-dev libxslt-dev tzdata yaml-dev git nodejs file zip unzip

RUN rm -rf /var/cache/apk/* # It should be one RUN instruction

RUN addgroup -g 1000 rails && \

adduser -S -G rails -u 1000 -h /application rails && \

chown -R rails /usr/local/bundle && \

apk update && \

apk add linux-headers build-base curl zlib-dev libxml2-dev libxslt-dev tzdata yaml-dev git nodejs file zip unzip && \

rm -rf /var/cache/apk/*

Remove unneeded packages after installing the dependencies — such as compilers or other packages that are needed for installing the software.

Pay attention to the order of the instructions: To improve the efficiency and performance of building Docker images, order the instructions in the Dockerfile to have the instructions that are less likely to change on the top of the file and the ones that are more likely to change at the bottom. This order will allow Docker to build the image cache more effectively. For instance, the below instructions should be placed on the top of the Dockerfile .

ENV PORT 8080

ENV RACK_ENV=production RAILS_ENV=production

ENV RAILS_LOG_TO_STDOUT=true

ENV SECRET_KEY_BASE changeme

EXPOSE 8080 COPY ./docker-entrypoint.sh /

ENTRYPOINT ["/docker-entrypoint.sh"]

Combining the above practices will result in the following Dockerfile . In this file, I moved all the shared instructions between Rails applications running with Ruby 2.6.3 and MySQL to the Docker base image wshihadeh/rails-base-image:2.6.3-mysql .

This image can be reused by any Rails/Ruby applications that rely on Ruby 2.6.3 and MySQL. This step reduced the build time of the Docker images for the application dramatically. Next, I included the commands that are less likely to change on the top of the file and the ones that need to be executed each time on the bottom.

Building the Docker image for the application can be done simply by executing the following Docker command.

docker build -t ${IMG}:${IMG_TAG} Dockerfile

docker push ${IMG}:${IMG_TAG}

In the next article, I’ll present how we can introduce a simple interface for building, pushing, testing and working with Docker images.