How to use docker to have an uniform development environment for your rails project

Lets say you work on a company, and there is more than one developer at the company, and sometimes other developer is hired and need to configure the development environment.

Or maybe you work on an opensource project and you want to make the life of anyone that is contributing to the project easier.

Or you might want to deploy your application to production without worrying if the environment in the production server is different from the development environment where the application was tested, this way preventing the infamous “it works on my machine”.

These are all valid reasons to learn a little docker, as we’ll see here, docker will help you configure your environment once, and deploy your application to any environment (we’ll have posts in the next few days showing how to deploy it in all major clouds…).

So lets start installing docker, you can get the right Docker CE  for your platform in the official website. Do not forget to also install docker-compose.

After this you’ll just create a new rails application with a command like this (or work on an existing app you have around…)

 rails new rails_docker_sample -d mysql --skip-coffee

(why I’m using MySQL? just because I’m used to 😛 )

(why I’m skipping coffee script? because I do not like it 😛 )

Now, we need to create a “Dockerfile” and I use almost the same for all my rails projects, with very small differences currently.

FROM ruby:2.5.0
 
RUN apt-get update -qq && apt-get install -y build-essential  apt-transport-https
 
# Node.js
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - \
    && apt-get install -y nodejs
 
# yarn
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -\
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
    && apt-get update \
    && apt-get install -y yarn
 
 
#install app
RUN mkdir /myapp
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
COPY . /myapp
RUN yarn install
ENTRYPOINT ["/myapp/bin/rails", "s", "-b", "0.0.0.0"]

The main differences between projects will be the database driver library and any other specificity of your project, the ruby version…

What is important in this Dockerfile:

  • FROM specify the base image we are using, I’m starting with the image that contains ruby 2.5.0
  • RUN runs a command inside the VM that is building the image
  • WORKDIR sets the work directory inside the image
  • COPY copies one file from your machine to the image
  • ENTRYPOINT specifies the command that will start your app when this image is executed as a container, the important thing here is that to maintain compatibility with most cloud servers were we’ll be running this containers later, we need to use this array variant, the array will be the same “ARGV” parameter to the command later.

Now, lets make some changes to our app to enable it to use environment variables to configure what is where.

First, I changed the config/database.yml file so that it will get the database address always from environment variables.

# MySQL. Versions 5.1.10 and up are supported.
#
# Install the MySQL driver
#   gem install mysql2
#
# Ensure the MySQL gem is defined in your Gemfile
#   gem 'mysql2'
#
# And be sure to use new-style password hashing:
#   https://dev.mysql.com/doc/refman/5.7/en/password-hashing.html
#
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV['DATABASE_USERNAME'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>
  host: <%= ENV['DATABASE_HOST'] %>

development:
  <<: *default
  database: rails_docker_sample
 
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: rails_docker_sample_test
 
# As with config/secrets.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#
# Instead, provide the password as a unix environment variable when you boot
# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
# for a full rundown on how to provide these environment variables in a
# production deployment.
#
# On Heroku and other platform providers, you may have a full connection URL
# available as an environment variable. For example:
#
#   DATABASE_URL="mysql2://myuser:mypass@localhost/somedatabase"
#
# You can use this database configuration with:
#
#   production:
#     url: <%= ENV['DATABASE_URL'] %>
#
production:
  <<: *default
  database: rails_docker_sample

The only database with a different name is the test DB, because we do not want trash in any other environment.

then I changed the config/cable.yml to also use environment variables to connect to redis, making it possible to use it in production later.

development:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
  channel_prefix: rails_docker_sample_production

test:
  adapter: async

production:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
  channel_prefix: rails_docker_sample_production

Again, except for the test environment

Now you can build your docker image, and to make it easier to reference later you can add a tag, the command will be similar to this one:

sudo docker build -t rails_docker_sample  .

we are invoking the build command, tagging the image with “rails_docker_sample” and using the current directory as the source for building.

Ok, that is pretty, but pretty useless also, to setup our development environment, we’ll use docker-compose, to do that, we’ll create a docker-compose.yml file similar to this one, describing all the images we need.

version: '3'
services:
  mysqlhost:
    image: mysql:5.7
    environment:
      - MYSQL_ROOT_PASSWORD=password
    volumes:
      - ../mysqldata:/var/lib/mysql
    ports:
      - "3306:3306"
    restart: always
  redishost:
    container_name: redis
    image: redis
    restart: always
  web:
    build: .
    container_name: "myapp"
    image: ubuntu/latest
    environment:
      - DATABASE_HOST=mysqlhost
      - DATABASE_USER=root
      - DATABASE_PASSWORD=password
      - REDIS_URL=redis://redishost:6379/1
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - mysqlhost
      - redishost

and we can run it with the command (do not forget to create the ../mysqldata directory first):

sudo docker-compose up

But what exactly that will do?

It will download any needed images (like the mysql and redis ones).

It will build your docker image, based on your Dockerfile

It will start a docker container for your app passing the configured variables

And there is some magic there also, the “volumes” section for each service, allow the mapping of one local directory to one container directory, for example, the ../mysqldata that was created before, now contains the mysql databases, you can erase the container and still have access to your data, we can use a similar technique while deploying the app to the cloud later.

We are also mapping the project base directory to the app directory in the container, ans since the RAILS_ENV there is “development”  any changes we do in the files will reflect in the running container.

The “ports” section is also interesting, it allows mapping a container TCP/IP port to your local machine, allowing you to access http://localhost:3000 to access your rails app, and if you do it right now, you’ll notice that you’ll receive one error that the database does not exists.

We can fix that easily, just go to another terminal window in the same project directory and type these command:

sudo docker-compose run --entrypoint "bash -c" web "bundle exec rake db:create"

We had to override the entrypoint specified in the Dockerfile because everything we pass as parameters is passed to that entrypoint, another option we have is to not specify the ENTRYPOINT in the docker file, and specify a command in the docker-compose.yml.

That would allow us to simplify this and access a “bash” in the container with this command:

sudo docker-compose run  web bash

So after this, you just need to share your project with any coworker and they can just “sudo docker-compose up” and start working with all the same environment you have.

Of course this is just a quick and dirty introduction to how to use docker with a Rails app, but we’ll expand this with some posts in the next days about how to use what we learned here to deploy to any of the major cloud providers.

If you want to download the code I used to crate this sample, you can get it in my github page https://github.com/urubatan/rails_docker_sample

If you have any questions about this post or suggestions about the next ones, please leave a comment and I’ll answer it ASAP.