7 July 2017 · About 10 minutes read

Rails on Docker: Getting Started with Docker and Ruby on Rails

Docker is a fantastic tool for isolating your app and its environment, and allows easy distribution and state-replication across multiple environments (dev, test, beta, prod, etc.). Using Docker can get rid of the “it works on my machine” problem, and help you to easily scale your app as it grows.

Docker is particularly great when your app has a lot of dependencies, or requires specific versions of libraries and tools to be configured.

In this tutorial, you’ll learn how to take a basic Rails app and prepare it for use in a Docker container (“dockerise” it).

PreRequisites

For this tutorial, I’m using a simple Rails 5 application configured to use a PostgreSQL database. If you use a different database, you’ll need to tweak a few of the files below.

You can use the following template to create a basic Rails application that is configured with a Dockerfile and config/database.yml as below:

$ rails new --database=postgresql --skip-bundle --template=https://gist.githubusercontent.com/cblunt/1d3b0c1829875e3889d50c27eb233ebe/raw/01456b8ad4e0da20389b0b91dfec8b272a14a635/rails-docker-pg-template.rb my-app
$ cd my-app

Database Configuration

We can make use of environment variables to configure the details for our app’s database. You’ll use this later so that your app’s docker container can connect to a PostgreSQL container.

Edit your config/database.yml configuration

(Note: You don’t need to do this if you’ve used the application template above)

Update your app’s config/database.yml to use environment variables:

# config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  host: db
  username: <%= ENV.fetch('POSTGRES_USER') %>
  password: <%= ENV.fetch('POSTGRES_PASSWORD') %>

development:
  <<: *default
  database: my-app_development

test:
  <<: *default
  database: my-app_test

production:
  <<: *default
  database: my-app_production

Create a Dockerfile

With your app prepared, it’s time to start using Docker. Let’s start by creating a Dockerfile. This is a plain text file that instructs Docker how to build an image for your application.

You use it to install dependencies, configure default environment variables, copy code into the container, and so on.

To keep your image size small, I prefer to use the alpine-linux Ruby base image. Alpine linux is a tiny linux distribution that’s perfect for containers, and Docker provides a default ruby:alpine base image that we can use.

Write Your Dockerfile

Let’s start by creating a basic Dockerfile for your rails app. In your app’s folder, create the following Dockerfile.

(Note: You don’t need to do this if you’ve used the application template above)

# /path/to/app/Dockerfile
FROM ruby:2.3-alpine

# Set local timezone
RUN apk add --update tzdata && \
    cp /usr/share/zoneinfo/Europe/London /etc/localtime && \
    echo "Europe/London" > /etc/timezone

# Install your app's runtime dependencies in the container
RUN apk add --update --virtual runtime-deps postgresql-client nodejs libffi-dev readline sqlite

# Bundle into the temp directory
WORKDIR /tmp
ADD Gemfile* ./

RUN apk add --virtual build-deps build-base openssl-dev postgresql-dev libc-dev linux-headers libxml2-dev libxslt-dev readline-dev && \
    bundle install --jobs=2 && \
    apk del build-deps

# Copy the app's code into the container
ENV APP_HOME /app
COPY . $APP_HOME
WORKDIR $APP_HOME

# Configure production environment variables
ENV RAILS_ENV=production \
    RACK_ENV=production

# Expose port 3000 from the container
EXPOSE 3000

# Run puma server by default
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

What if I don’t use PostgreSQL?

If you’re using a different database server (e.g. MySQL), you’ll need to tweak the Dockerfile to install the appropriate packages.

You can search for the correct package(s) using the following Docker command:

$ docker run --rm -it ruby:2.3-alpine apk search --update mysql | sort
...
mariadb-client-libs-10.1.22-r0
mariadb-dev-10.1.22-r0
mariadb-libs-10.1.22-r0
mysql-10.1.22-r0
mysql-bench-10.1.22-r0
...

With your Dockerfile written, you can now instruct Docker to build an image for your app:

Build an image for your app

$ docker build . -t my-app

Once the image is built, we’re ready to get started! You can spin up a new container based on your app’s image using the following command:

$ docker run --rm -it --env RAILS_ENV=development --env POSTGRES_USER=postgres --env POSTGRES_PASSWORD=superSecret123 --publish 3000:3000 --volume ${PWD}:/app my-app

We’re passing a few arguments to the Docker run command:

  • -it is actually two arguments that allow you to interact with your container via the shell (e.g. to issue Ctrl+C commands).
  • --env allows you to pass environment variables into the container. Here, you’re using it to set the database connection values.
  • --rm will instruct Docker to remove the container once it finishes (i.e. when you hit Ctrl+C).
  • --publish forwards port 3000 from the container to port 3000 on your host. This allows you to access the container as if it was running on your host (i.e. http://localhost:3000).
  • Finally --volume instructs Docker to “mount” the current folder (from your host machine) into the container. This means as you edit code on your workstation, it will be available to the container. Without this, you’d need to recreate the container with every code change you make.

Once it’s up and running, you can access your container by opening localhost:3000 in your browser.

Run a Database Container

Unfortunately, although the container runs successfully, if you try to access the app in your browser, it will crash with a database error.

could not translate host name “db” to address: Name does not resolve

At the moment, there isn’t a PostgreSQL server available for the app too connect. We’ll fix that now by spinning up a separate Docker container in which PostgreSQL will run:


ProTip Remember that in Docker, a container should be designed to do one thing (and one thing only).

In our case, we’ll use two containers: one for our app, and one for our database (PostgreSQL).


Start a new container running PostgreSQL:

Hit Ctrl+C to stop (and remove) your running app container, then spin up a new container for PostgreSQL:

$ docker run -d -it --env POSTGRES_PASSWORD=superSecret123 --env DB_NAME=my-app_development --name mydbcontainer postgres:9.6

The -d flag will detach the container from our terminal, allowing it to run in the background. We also give the container a name (mydbcontainer) which we’ll use below.

Using Single–Task Containers

Docker containers are disposable, and their single-purpose nature means that once they have “finished”, they are stopped (and, optionally, removed).

This makes them perfect for running one-off tasks, such as rails commands (e.g. bin/rails db:setup).

We’ll do that now to setup your app’s database on mydbcontainer:

Run the rails db:migrate task using a container:

Use the following command to spin up a copy of your app’s container, run the bin/rails db:setup task, and then shut down.

Note that you’ll need to configure environment variables for the database connection (these are injected into the config/database.yml file you edited earlier).

You’ll also use the --link option which allows the container to connect to PostgreSQL container that is running (mydbcontainer), using the hostname db:

$ docker run --rm --env RAILS_ENV=development --env POSTGRES_USER=postgres --env POSTGRES_PASSWORD=superSecret123 --link mydbcontainer:db --volume ${PWD}:/app my-app bin/rails db:create db:migrate

The --rm flag will remove (delete) the container once it has finished running.

Once that command has run, your app’s database will be setup on the mydbcontainer container. Finally, we can run our app!

Run your app!

Let’s spin up a new container using our app’s image. Notice that there are a couple of additional arguments when running the command:

$ docker run --rm -it --env RAILS_ENV=development --env POSTGRES_USER=postgres --env POSTGRES_PASSWORD=superSecret123 --publish 3000:3000 --volume ${PWD}:/app --link mydbcontainer:db my-app

=> Puma starting in single mode...
=>  * Version 3.8.2 (ruby 2.4.1-p111), codename: Sassy Salamander
=>  * Min threads: 5, max threads: 5
=>  * Environment: development
=>  * Listening on tcp://0.0.0.0:3000
=>  Use Ctrl-C to stop

Open your browser to localhost:3000, and you should see your app running entirely on Docker!

Next Steps

Docker is a great tool for developing your applications. As time goes on, you can begin to move all components of your app (database, redis, sidekiq workers, cron, etc.) to Docker.

The next step is to use Docker Compose to declare all your containers, and how they should work together.

Chris Blunt
Chris Blunt @cblunt
Chris is the founder of Plymouth Software. As well as code and business, he enjoys being a Dad, swimming, and the fine art of drinking tea.