DEV Community

loading...
Cover image for Boilerplate for a containerized plain Ruby application

Boilerplate for a containerized plain Ruby application

Leandro Proença
Software Developer, CTO @Bugle
・3 min read

Recently I've been poking around automation in order to experiment and build software on destroyable environments, so that I won't mess up with my operating system host.

That's where virtualization and containerization can help: I want to stand on a fast and destroyable environment which can be re-launch as many times as I want.

Virtualization

In this Gist I share how to launch an Ubuntu environment on the host using a lightweight VM manager, called multipass.

Virtualization is not the focus of this post, as you can check the Gist for further details.

Containerization

Throughout this post, I will present how to build a plain Ruby containerized application using just Docker and Makefile, such that the boilerplate can be reused everytime we want to create a new app.

Makefile

Let's create the Makefile, which can be a centralized entrypoint for the commands we want to run. It's a good practice to have a Makefile in every app.

Makefile

console:
  docker run \
    --rm \
    -it \
    -v $(pwd):/$(basename $(pwd)) \
    -w /$(basename $(pwd)) \
    ruby:2.7 \
    bash
Enter fullscreen mode Exit fullscreen mode

Makefile is composed by targets. Each target can run a specific task which in turn can be a single command or a set of commands. In our example, we are running the docker run along with its options:

  • docker run: creates a new container based on an image
  • --rm: will remove the container on exit
  • -it: allows a pseudo-terminal to interact
  • -v $(pwd):/$(basename $(pwd)): mounts the current directory from host to container
  • -w /$(basename $(pwd)): sets up the default working dir on container
  • ruby:2.7: the image from which the container will run. Docker tries to find the image locally, otherwise downloads it from a Docker registry
  • bash: the command executed on the container. bash will request a pseudo-terminal to interact

We can test the target by running:

make console
Enter fullscreen mode Exit fullscreen mode

It will open the bash from the container.

docker-compose

Docker command options can be verbose quickly as we add more complexity to our application. As a means to make it easier to use Docker in development, we can declare our container specification in a single file that can be reused.

Docker comes with docker-compose to solve that problem.

docker-compose.yml

version: '3.9'

services:
  dev:
    image: ruby:2.7
    container_name: my-application
    working_dir: /my-application
    volumes: 
      - ./:/my-application
Enter fullscreen mode Exit fullscreen mode

Now, we can change our Makefile to use the docker-compose command:

Makefile

console:
  docker-compose run dev bash
Enter fullscreen mode Exit fullscreen mode

And check it:

make console
Enter fullscreen mode Exit fullscreen mode

The above configuration does the same job as running docker run with volume option, working dir, image and so on.

Less. Verbosity.

Test-driven

Intending to bootstrap our application with TDD, the first file we create is the test file, which runs a simple dummy test. It seems silly, but enough for the purpose of this boilerplate, being able to be enhanced at a later time.

As for Ruby, we're gonna use test-unit.

app_test.rb

require 'test/unit'

class AppTest < Test::Unit::TestCase
  def test_dummy
    assert_equal 1, 1
  end
end
Enter fullscreen mode Exit fullscreen mode

However test/unit does not come with this standard Ruby, making us to include the gem separately.

Gemfile

source 'https://rubygems.org'

gem 'test-unit'
Enter fullscreen mode Exit fullscreen mode

Now we can run make console, and then from inside the container, run the command to install the gem from Gemfile:

bundle install
Enter fullscreen mode Exit fullscreen mode

Ruby will place the gems by default on /usr/local/bundle.

Named Volume

We can't forget that everytime we run make console, a new container will be created, losing all the gems we have installed. As the application grows, running bundle install can be onerous.

Let's use a named volume to use the host as a "cache":

docker-compose.yml

version: '3.9'

services:
  dev:
    image: ruby:2.7
    container_name: my-application
    working_dir: /my-application
    volumes: 
      - ./:/my-application
      - rubygems:/usr/local/bundle

volumes:
  rubygems:
Enter fullscreen mode Exit fullscreen mode

By doing this way, Docker will use this named volume in host for the gems placed at /usr/local/bundle from running containers.

Running the test

As for now we are able to run the test:

make console
bundle
ruby app_test.rb
Enter fullscreen mode Exit fullscreen mode

Improving the test command

Instead of entering the console everytime to run the test, we can run it directly upon container creation:

docker-compose run dev ruby app_test.rb
Enter fullscreen mode Exit fullscreen mode

The, improving our workflow is easy as follows:

Makefile

console:                                    
  docker-compose run dev bash               

utest:                                      
  docker-compose run dev ruby app_test.rb
Enter fullscreen mode Exit fullscreen mode

Entering the console to run stuff:

make console
Enter fullscreen mode Exit fullscreen mode

Running the test:

make utest
Enter fullscreen mode Exit fullscreen mode

Conclusion

The purpose of this article was to share a way of creating the boilerplate for a containerized Ruby application, allowing us to experiment and play on destroyable environments, remaining our OS host untouchable.

Discussion (0)