Building a Docker Image for your Rails Application - Part 1

Wether you are running apps on your own infrastructure or deploying to the cloud, there are many reasons to containerize your Rails application. However, the template generated by rails new doesn’t help you there, as the generated code has to be adapted to run in Docker. You will need to do some modifications to the app and add a carefully designed Dockerfile, which is important for speeding up build times and reducing the image size. That’s what this post is about.

The text is divided in 3 parts. In this first post, we take a look at what you should change in your application code in order to make it suitable to run inside a docker container. Part 2 is about building a base image that contains the prerequisites needed for a typical Ruby on Rails app. Finally, on part 3, we show how to add the application files, create and run your container. A basic understanding of Docker concepts and Dockerfile syntax is required for understanding the content.

Assumptions

One very important decision that will impact the way you build your image is how you want to handle the assets. Particularly, you should answer these two questions:

1. When will the assets precompilation happen? Will it be done in development time, and pushed to the source code repository? Or will it take place later, during the deploy process?

2. How will these assets be served? By the same application server? Other server on the same infrastructures? Or will you use an external CDN?

There are no correct answers here, as this depends on many factors that are application or environment-specific. In our use case, which is described here, we assume a simple scenario where:

  • We don’t want developers to care about building assets for production. The CI script should handle it.
  • In a small scale environment, the assets can be served by the same container that runs the application.

Configuration Files

Now let’s get started. The first thing to notice when containerizing your application is that you should not have any configuration data in your docker image. Instead, the configuration files should reference environment variables that are defined elsewhere, outside the image. This allows you to use the same image in different environments, just by setting the appropriate values for the variables.

To achieve this, you will have to look at the source code searching for files that hold database connection settings, URLs for external services, and any other parameters that might change depending on the environment. Typical files to look are config/database.yml, config/storage.yml, and config/initializers/*. You should replace hard-coded values with environment variables. Here is what an example database.yml that references environment variables, using postgresql as an example:

default: &default
  adapter: <%= ENV["DB_ADAPTER"] || 'postgresql' %>
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS", 10) %>
  database: <%= ENV["DB_DATABASE"] %>
  username: <%= ENV["DB_USERNAME"] %>
  password: <%= ENV["DB_PASSWORD"] %>
  host: <%= ENV["DB_HOST"] %>
  port: <%= ENV["DB_PORT"] || 5432 %>

development:
  <<: *default

test:
  <<: *default

staging:
  <<: *default

production:
  <<: *default

Providing values for the environment variables

The values for the environment variables need to be informed when you run your container. If you start your application using Docker Compose, it will automatically load environment variables from a file called .env in the working directory, if it finds one. You can also supply additional .env files in your docker-compose.yml, if you like.

For development, you might find it convenient to use the dotenv gem. It will allow you to run your development server in the same way you normally would, while loading environment variables from your .env file. You can also have multiple files, and dotenv will combine them to form your configuration.

Here is an example configuration with 3 files:

# .env
# this file contains settings common to all environments
RAILS_LOG_TO_STDOUT=true
RAILS_SERVE_STATIC_FILES=true
DB_DATABASE=rails_docker_demo
DB_USERNAME=rails_docker_demo

# .env.development
# put settings specific for development environment here
DB_PASSWORD=my_pg_pass123
DB_HOST=dev_server_hostname
DB_PORT=5432

# .env.development.local
# settings specific to this workstation
DB_PASSWORD=my_pg_pass123

Should I commit or .gitignore my .env files?

It is a good practice to commit your .env files to source control. You should avoid, however, including sensitive data like database passwords. For these, you should look for solutions like Docker Secrets or Kubernetes Secrets, depending on your production environment.

Dotenv also supports the use of local env files (.env.*.local). These allow you to make settings local to a specific machine and should be gitignored.

Logging

Did you notice the RAILS_LOG_TO_STDOUT variable that appeared in the example .env?. It is not a good practice to write data to the container’s filesystem, so this setting is used to send the logs to standard output. The default-generated Rails configuration will look for this variable when running in staging or production environment.

Another important variable to configure are RAILS_ENV, that defines if you are in development, staging or production and RAILS_SERVE_STATIC_FILES, to make the static assets available through the same server.

That’s It

This is how you set up your application to run smoothly inside a Docker Container. In the next post we will start to look at how to transform it in a docker image by creating an appropriate Dockerfile.


If you want to run your application on Docker but lack the necessary time or experience, feel free to contact us and we will be happy to give you a hand.

Comments

If you have any questions or feedback regarding this post, please leave your comment below. Keep in mind that comments are subject to moderation and will not be displayed immediately.