Docker and Rails: Running Ruby on Rails with Docker

8 minute read     Updated:

Utibeabasi Umanah %
Utibeabasi Umanah

This article explains how to Dockerize Ruby on Rails applications. Earthly improves the Dockerization process for Rails apps by enabling parallel builds and optimizing cache usage. Learn more about Earthly.

When working with large distributed teams, you often run into the issue of something working on one computer but not others. When building and running applications, each developer has a slightly different development environment. For example, one developer may use a Windows PC to build and run an application that was developed on a Mac. Apart from the differences in the command line, the developer trying to run the applications may not have the required dependencies installed, and the process of finding and installing the correct versions of the dependencies slows development. This is where containerization tools, like Docker, can help.

Docker lets you package applications with all their dependencies into a single image that can be distributed across teams. By doing so, you only need to install Docker in your development environment to run the application.

If you’re building a Ruby on Rails application, Docker helps to ensure that everyone on the team is running the same version of Ruby and other dependencies your application needs.

In this tutorial, you’ll learn how to run a Ruby on Rails application inside a Docker container and what some best practices are for doing so.

Docker Image

Before you begin this tutorial, you’ll need the following prerequisites:

  • Docker: should be installed on your local machine. Make sure you install the correct version for your operating system.
  • Docker Compose: is needed to manage multiple containers at once. Instructions to install it are available on the Docker docs.
  • Git: should be installed on your local machine. The sample application you are going to Dockerize is stored in a Git repository on GitHub, and as such, you’ll need to have Git installed in order to clone the repository.
  • Ruby on Rails: needs to be installed if you’re creating a new Ruby application. Otherwise, you can clone the sample project on this GitHub repo.

Writing the Dockerfile

Before you create the Dockerfile, you need a sample application to Dockerize. To clone the sample project, run the following command in your terminal:

git clone https://github.com/utibeabasi6/docker-rails

You should get an output similar to this:

Cloning a sample project

Open up the docker-rails folder in Visual Studio Code or any other IDE, and create a new file called Dockerfile in the root directory. In the Dockerfile, paste in the following code:

  FROM ruby:2.5.9
  RUN apt-get update && apt-get install -y nodejs
  WORKDIR /app
  COPY Gemfile* .
  RUN bundle install
  COPY . .
  EXPOSE 3000
  CMD ["rails", "server", "-b", "0.0.0.0"]

Take a look at what each line of code is doing:

  • FROM: instructs Docker to use a specified parent image as a starting point when building the image. Since you’re building a Ruby application, you’re using the official Ruby image, which has Ruby preinstalled as your base.
  • RUN: is used to run shell commands while building the Docker image. In this tutorial, you’re installing Node.js because Rails depends on it.
  • WORKDIR: sets the specified directory as the working directory inside the Docker image. Any further commands run in the Dockerfile will be run in the context of this directory.
  • COPY: is used to copy files from the host machine into the Docker image. The syntax for this is COPY <source> <destination>.. The period (.) refers to the current directory.
  • EXPOSE: informs Docker that the application will be listening on the specified port when the container is run. This command is mainly for documentation purposes and doesn’t actually open the port.
  • CMD: is the main entry point to your Docker image. It’s the command that is run whenever a container is created.

Building and Running the Docker Image

Now that you’ve created a Dockerfile, it’s time to build the image. In your terminal, change your directory to the docker-rails directory and run the following command:

 docker build -t rubyapp .
Docker build command

Now, if you run the command docker images, you should see the newly created rubyapp image:

Docker images output

It’s important to note that the docker build command doesn’t run the container, it just creates the image. In order to run the application we need to use the image to run a container instance

To create a container from this image, run the command docker run -p 3000:3000 rubyapp. This creates a container and binds port 3000 of your host computer to port 3000 of the container. Now navigate to http://localhost:3000 on your browser, and you should see the newly created Rails application.

Finally, stop the container with CTRL + C.

Best Practices when Building Docker Images

When building Docker images, there are a few best practices you need to take note of, including using an Alpine base image and reducing image size with multistage builds.

Use an Alpine Base Image

Alpine images are typically smaller and lighter in weight because they don’t have most of the unused packages and dependencies other images come with. Making use of these images as a base helps reduce the size of Docker images. This results in faster download speeds when deploying.

Take a look at the current size of our RubyApps image. Run the command docker images .:

Docker images output

As you can see, the image is 995MB in size. This is not ideal, so you need to modify the Dockerfile to make use of an Alpine base image. Replace the code in your Dockerfile with the following:

  FROM ruby:2.5.9-alpine
  RUN apk add \
    build-base \
    postgresql-dev \
    tzdata \
    nodejs
  
  WORKDIR /app
  COPY Gemfile* .
  RUN bundle install
  COPY . .
  EXPOSE 3000
  CMD ["rails", "server", "-b", "0.0.0.0"]

To begin, you replace the base image with an Alpine image. Because Alpine images use apk instead of apt-get as a package manager, you have to modify the RUN command to reflect this. You then install some necessary packages for your Rails app since the Alpine image is mostly bare.

Now, run the command docker build -t rubyapp . to rebuild the image. If you run docker images again, you should see that the image size has reduced to 599MB:

Reduced image size

Do Multistage Builds

Another way of reducing image size is by having multistage build. In a multistage build, your Dockerfile will compose of multiple steps, where all dependencies are installed in one stage, and the application is built. Then only the files necessary to run the application are included in the final stage.

Replace the code in your Dockerfile with this:

  FROM ruby:2.5.9-alpine AS builder
  RUN apk add \
    build-base \
    postgresql-dev
  COPY Gemfile* .
  RUN bundle install
   FROM ruby:2.5.9-alpine AS runner
  RUN apk add \
      tzdata \
      nodejs \
      postgresql-dev
  WORKDIR /app
  # We copy over the entire gems directory for our builder image, containing the already built artifact
  COPY --from=builder /usr/local/bundle/ /usr/local/bundle/
  COPY . .
  EXPOSE 3000
  CMD ["rails", "server", "-b", "0.0.0.0"]

The first stage is named builder with AS as the keyword. Then you install the packages necessary to install the Ruby dependencies. Next, you copy in the Gemfile and RUN bundle install to install the needed gems.

In the second stage, you install the dependencies necessary to run the app and copy the Ruby gems from the builder stage. You do this by specifying --from=builder to tell Docker to copy from the builder stage.

Now you need to rebuild the Docker image by running docker build -t rubyapp . and then run the command docker images. You will see that your image size has reduced further to 387MB:

Docker images output 387MB

Multiple Containers With docker-compose

If you run the Docker image you just built, you’ll be greeted with an error page in your browser:

Ruby error page

Rails expects a PostgreSQL database to be running on localhost, but that isn’t the case since you’re running inside of a container. A solution to this is to start a separate PostgreSQL Docker container and point your application to it. Thankfully, Docker has a tool for setting up and managing multiple containers: Docker Compose. Docker Compose is configured with a YAML file, which must be named docker-compose.yaml or docker-compose.yml.

Modifying Your Database Settings

Before you write the docker-compose file, you need to modify your application to connect to the database. In the config directory, modify the database.yml file with the following:

development:
 <<: *default
 # database: app_development

 username: <%= ENV['POSTGRES_USER'] %>
 password: <%= ENV['POSTGRES_PASSWD'] %>
 host: <%= ENV['POSTGRES_HOST'] %>

Make sure to comment out the database: since you’ll be using the default database for now. The username, password, and host are set from environment variables.

Writing the docker-compose.yaml File

Create a file called docker-compose.yaml and paste in the following code:

  version: "3.9"
  services:
    app:
      image: rubyapp
      environment:
        POSTGRES_USER: postgres
        POSTGRES_PASSWD: example
        POSTGRES_HOST: db
      ports:
        - 3000:3000
    db:
      image: postgres:alpine3.15
      environment:
        POSTGRES_PASSWORD: example

Here, you define the version as 3.9; then, you specify two services. The app service is making use of the RubyApps image you just built, and then you set four environment variables for the POSTGRES_PASSWD, POSTGRES_USER, and POSTGRES_HOST. After that, you expose port 3000.

The second service, db, uses the PostgreSQL image, this is an image that gets pulled down from Docker Hub. Then it sets a single environment variable for the POSTGRES_PASSWORD.

Note that you’re passing in db as the POSTGRES_HOST name because Docker Compose runs all the containers on a single network, and you can send requests to other containers through their hostname, which is the same as the service name. So in this example, the hostname for the database container is db since you’re using db as the service name.

Now, start the containers by running docker-compose up:

docker-compose up output

Navigate to http://localhost:3000. And you should be greeted with the Rails app:

Ruby default homepage

Conclusion

In this article, you learned how to build a Docker image for a Ruby on Rails application and how to reduce image size by making use of Alpine base images and multistage builds. You also learned how to run multiple containers with Docker Compose and connect your application to a database.

Earthly.dev is a free and open source syntax for defining cacheable, parallelizable, and Git-aware build steps. Earthly takes some of the best ideas from Makefiles and Dockerfiles, and combines them into one specification. With Earthly, you can run unit and integration tests, create several Docker images at a time, and easily define multistage builds.

Earthly Cloud: Consistent, Fast Builds, Any CI
Consistent, repeatable builds across all environments. Advanced caching for faster builds. Easy integration with any CI. 6,000 build minutes per month included.

Get Started Free

Utibeabasi Umanah %
Utibeabasi Umanah

Self-taught cloud/DevOps engineer with a passion for building software, driven by a desire to create better products and infrastructure.

Updated:

Published:

Get notified about new articles!
We won't send you spam. Unsubscribe at any time.