Dockerizing a Rails 5 App with Multiple Services Part 1: Development

Simply put, Docker is a container management service.

The keywords of Docker are develop, ship and run anywhere. The clear purpose for it is to help developers easily build applications, ship them into containers which can then be deployed anywhere.

But where do you start? What's a container? As you search for answers to these questions, you will quickly find that though Docker serves a distinct and clear purpose, the ecosystem of tools, libraries and commands are vast and can quickly become overwhelming.

Building, managing and deployng applications are also not small affairs these days. There are many ways acheive a workable system without Docker, but as you'll see throughout this Guide, Docker can handle the lion's share of the heavy lifting.

So where do we get started? With a full example starting from scratch.

You miss 100% of the shots you don't take.

Let's give this thing a shot.

Our Goals for the End-Result:

  • To have a fully containerized application with all dependencies and services communicating properly.
  • To have our database changes persist so we don't have to seed the system everytime we need to build a feature.
  • To be able to run test suite, and custom commands in our system easily.
  • To be able to build our system, and run it locally in a development environment.

Our Tools and Application:

  • Working on macOS High Sierra
  • Docker for Mac & Docker Compose
  • Ruby 2.4.3
  • Rails 5.1.5
  • Postgres 9.6
  • Redis 3.2
  • Sidekiq

Setup our Environment:

Download Docker for Mac from the docker website:
https://docs.docker.com/docker-for-mac/install/

Docker for Mac includes Docker compose. Our current versions are:

Engine: Version 17.12.0-ce-mac49 (21995)
Compose: 1.18.0

Open the Docker app and make sure everything's up an running.

Fork or Clone the Source Repo

In your terminal, go to your work directory and clone the source repo for this guide:

https://github.com/devato/docker-rails

$ git clone git@github.com:devato/docker-rails.git
$ cd docker-rails

The Dockerfile

A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image. Using docker build users can create an automated build that executes several command-line instructions in succession.

It runs your commands so you don't have to, and is the blueprint for how your custom rails app image will be built.

We're going to use the ubuntu ruby image that exists in docker hub to eliminate some extraneous steps.

TIP: For the sake of reducing any issues, just use the images outlined in the files below. You can easily remove the containers and images later and customize them to fit your needs.

Here's our Dockerfile:

The Dockerfile is fairly simple and really explains itself. If you have managed any ubuntu servers, these commands will be familiar.

Spend some time reading more about Dockerfile instructions here:
https://docs.docker.com/engine/reference/builder/

The docker-entrypoint.sh file

There's a common issue that comes up when bundling gems with docker. After the initial build, everything runs fine but when you add or change a gem, the entire list of gems is re-installed. This isn't a blocking problem, but is a major inconvenience when you're trying to quickly make some changes.

We're handling this situation by using a custom docker-entrypoint.sh file:

The bundle process will now only update gems that have changed or been added when building and running the containers.

The docker-compose.yml file

Docker Compose will do the heavy lifting to get our containers off the ground. There are a few thing to keep in mind when creating docker-compose.yml files, one of which is the version.

With each version there are different ways to skin the cat, and the best advice I can give is that you spend some time reading through the docs about each feature.

For now, let's just get it running. Here's the complete docker-compose.yml file:

There's alot going on here, but if you break down the services into chunks of config options, it starts to become a little more lucid. If you haven't already, make the investment and spend some time reading through the docs about each of the configuration options and how they work together.

For now let's get back to our Rails app and configure it to use the services we just outlined.

Manage Rails config using .env

To simplify things, We're going to use a .env file to manage our internal rails configurations. Here's the file:

NOTE: ensure that you have ignored .env in .gitignore

# this is prepended to service names
COMPOSE_PROJECT_NAME=dockerapp
RAILS_ENV=development
LOG_LEVEL=debug
SECRET_TOKEN=supersecret

DATABASE_URL=postgresql://postgres:supersecret@postgres:5432/dockerapp?encoding=utf8&pool=5&timeout=5000
REDIS_URL=redis://:supersecret@redis:6379/0
ACTION_CABLE_BACKEND_URL=redis://:supersecret@redis:6379/0
ACTION_CABLE_FRONTEND_URL=ws://cable:28080
WEB_CONCURRENCY=1

COMPOSE_PROJECT_NAME is a env variable for docker-cli.

It sets the project name. This value is prepended along with the service name to the container on start up. For example, if your project name is myapp and it includes two services db and web, then Compose starts containers named myapp_db_1 and myapp_web_1 respectively.

*NOTE: You'll notice that the db hosts' settings in the .env file match the services' we configured in our docker-compose.yml file earlier. *

Set your Rails configs

We're using an env variable to configure the db connection and here's our config/database.yml file:

---

development:
  url: <%= ENV['DATABASE_URL'].gsub('?', '_development?') %>

The environment will be appended automatically to the database name.

We'll also need to configure sidekiq:

# config/initializers/sidekiq.rb
Sidekiq.configure_server do |config|
  config.redis = { url: ENV['REDIS_URL'] }
end

Sidekiq.configure_client do |config|
  config.redis = { url: ENV['REDIS_URL'] }
end

Whew, we've covered a lot of ground here. But, we're close to actually seeing it pay off.

With all this in place, I think we're ready to give it a go.

Fire it up!

Let's see if we can fire this application up and get it running:

First we need to tell Docker Compose to build all our images:

$ docker-compose build

This will pull the images we defined in services: and get everything organized to run our containers, including the commands we set in the Dockerfile for our custom image.

Once this is complete, Let's stand this baby up and see if she can walk:

$ docker-compose up

In the output log, you should see that all the containers are running properly without errors, and have successfully connected to their respective databases.

If it's all good, let's continue to the next step.

Run the db:create command

As a final step, we're going to use docker-compose's exec command to run a rails command inside the app container:

$ docker-compose exec app rails db:create
$ docker-compose exec app rails db:migrate

OPTIONAL: And to run rspec or any other commands in the app container:

Here's an rspec example:

$ docker-compose exec app rspec

If these commands run smoothly, we should be able to visit our application in the browser.

You can access the containerized rails app via: http://localhost:3000. Port 3000 was configured in the docker-compose.yml file for the app service.

Boom! You are off to the races.

Congratulations!

You have fully dockerized/containerized your Rails application and are ready to take on more custom development for you Docker environment.

We have accomplished all our goals for this project:

  • To have a fully containerized application with all dependencies and services communicating properly.
  • To have our database changes persist so we don't have to seed the system everytime we need to build a feature.
  • To be able to run test suite, and custom commands in our system easily.
  • To be able to build our system, and run it locally in a development environment.

Wrapping Up

There are almost no limits to what you can do if you take the time to understand how the components work together. Troubleshooting issues also becomes increasingly less painful.

From here you can learn more about Volumes, Logging, Debugging issues, and working with Docker day-to-day as your development workhorse.

Let me know if you find any issues or errors with this guide, and as always, if you know of ways to help improve it, feel free to reach out anytime!

Happy Sailing!

Resources:

If you're concerned about your Docker system having too many containers/images/volumes that you don't know how to deal with, see our quick guide on how to keep your docker system clean: Keep Your Docker System Clean

NOTE: We're working on a full guide to deploy this application to production and should have it ready soon. Subscribe to the site, or follow me on twitter for updates @tmartin8080