Ruby on Rails Docker for local development environment

SnykSec - Nov 14 '22 - - Dev Community

Hi there Ruby developers! If you’ve been looking for an effective way to establish a Ruby on Rails Docker setup for your local development environment, then this post is for you. It’s a continuation of our previous article on how to install Ruby in a macOS for local development.

Ruby developers frequently need to account for a database when building a Ruby on Rails project, as well as other development environment prerequisites. However, ensuring all of these are installed on your host operating system — and use the exact required versions — might sometimes be a challenge.

Every time I was attempting to create a new project, I found myself wishing for a button that created a new, clean environment for it. So, I decided to create a Docker-powered setup that can easily be started and stopped using Docker’s docker-compose.yml.

What is Docker Compose?

Docker Compose is an Infrastructure as Code (IaC) tool that allows you to configure and connect applications and services such as proxies, databases, and volume mounts in order to provision them in a reusable way. It’s installed as part of the Docker software bundle, and defined using YAML.

This Docker Compose setup for a local development environment can be used for different types of projects, and isn’t solely applied to Ruby on Rails. It could also be used in side-projects or frontend client-side projects. The main reasons I prefer to use Docker environments instead of changing my own host operating system include:

  • easy to configure
  • easy to split logic from the environment
  • easy to use the same env for different projects
  • easy to share the environment without any sensitive code to anyone who needs help with it
  • host machine stays clean

A Ruby on Rails Docker setup

In order to setup a Ruby on Rails Docker environment for local development, you’ll need the following:

  • Unix like OS (I use a macOS, but you can use Ubuntu or any other Linux distro. Windows should also work)
  • Git
  • make (3.81 or later)
  • Docker (19.03.12 or later)
  • docker-compose (1.26.0 or later)
  • Your preferred text editor (Visual Studio Code in my case)

Once you have those, you’re ready to get started. For those who don’t want to read – here is the open source repository with all of the required instructions and the full source code to all of the snippets that we’ll review. B since you’re here already, simply follow this article for a step-by-step guide.

To start off I’ll create a Ruby on Rails project named foo_bar_project for the sake of simplicity.

1) Get the Docker Compose environment configuration and remove the git repository. If you want to keep everything tracked through a git source-code repository, you can create your own later.

git clone git@github.com:mtereschenko/simple_ror_environment.git && cd simple_ror_environment && rm -rf .git
Enter fullscreen mode Exit fullscreen mode

Next steps should be executed in the ./environment directory, so let’s make sure you are running commands inside it:

cd ./environment
Enter fullscreen mode Exit fullscreen mode

2) Now, you need to assign a project name to this environment setup. By default, it’s test_project. In our code walkthrough here, we need to update it to match foo_bar_project as we’ve previously described. To do so, make the changes in the ./environment/.env.example file to reflect it in the PROJECT_NAME variable. It’s important to use lower case Snake Case as values to variables:

PROJECT_NAME=foo_bar_project
Enter fullscreen mode Exit fullscreen mode

3) Next you are ready to create your project by running the following command:

make init
Enter fullscreen mode Exit fullscreen mode

As you can see, you now have a new folder called application with Ruby On Rails code inside.

4) Now you need to build your local development environment with Docker Compose. This step is obligatory after any changes in the Dockerfile or Gemfile of your application.

make build
Enter fullscreen mode Exit fullscreen mode

5) This allows you now to spin up the Ruby local development environment as follows:

make start
Enter fullscreen mode Exit fullscreen mode

Notice that this isn’t yet starting a Ruby on Rails application. To do that, we need to open a shell to the Docker environment and instruct it to start the rails process. We’ll do that as follows:

make shell
rails s -p 3000 –binding=0.0.0.0
Enter fullscreen mode Exit fullscreen mode

Finally, if you open your browser and navigate to this url, http://foo_bar_project.localhost/, you should see the Ruby on Rails project that we’ve just built.

So, what’s available for us now?

  • We have a project with an environment directory that contains all we need to run our project, and an application directory which contains our application code. This directory is mapped into the Docker container, so you have full access from the Ruby Docker image.
  • We have quick access to the Ruby shell, so we do not need to have a Ruby environment installed locally, such as Rails, Rake, Bundler, or any other Ruby toolchain.
  • Commands can be executed after the make shell command.
  • This Docker Compose setup of the project has two preconfigured stages, so if you ever need to deploy your Ruby application somewhere, you can create your own stage. It’s as simple as that!

If you’re keen on how the above Docker environment for Ruby on Rails works, we’re going to break it down into the Ruby Docker image, the Makefile, and the Docker Compose definition that makes all of it tick together.

A Ruby Docker image

First off, the Ruby Docker image is an essential part, and building it correctly is also important to ensure that we have an effective re-use of Docker image layers cache, and other concerns relating to a local development environment.

Following is the Dockerfile for the Ruby Docker image:

FROM ruby:2.7.6-alpine3.16 as base_image
RUN apk add --no-cache git \
    build-base \
    libpq-dev \
    tzdata

FROM base_image as development
COPY ./artifacts/rails/Gemfile /tmp/Gemfile
COPY ./artifacts/rails/Gemfile.lock /tmp/Gemfile.lock
COPY ./containers/ruby/runners/runner.development.sh /rdebug_ide/runner.sh
RUN cd /tmp && \
  gem install ruby-debug-ide && \
  gem install debase && \
  bundle install && \
  chmod +x /rdebug_ide/runner.sh && \
  apk add --no-cache git \
  nodejs \
  yarn
WORKDIR /app

ENTRYPOINT ["tail", "-f", "/dev/null"]

FROM base_image as init
COPY ./containers/ruby/initializers/runner.init.sh /tmp/runner.sh
COPY ./containers/ruby/initializers/database.yml /tmp/database.yml
COPY ./containers/ruby/initializers/.gitignore /tmp/.gitignore
COPY ./containers/ruby/initializers/development.rb /tmp/development.rb
RUN chmod +x /tmp/runner.sh

WORKDIR /app

ENTRYPOINT ["/tmp/runner.sh"]
Enter fullscreen mode Exit fullscreen mode

As you can see, it makes some assumptions on peripheral configuration that is needed to have a functional Ruby on Rails application environment, such as:

  • database.yml file that is seeded for the Ruby on Rails database connection details
  • development.rb for Ruby on Rails runtime configuration for the development environment

It does so using the runners.sh script in the ./environment/containers/ruby/initializers directory:

#!/bin/ash

cd /app

gem install rails

rails new . -d=postgresql --skip-git

yes | cp -rf /tmp/database.yml /app/config/database.yml
yes | cp -rf /tmp/development.rb /app/config/environments/development.rb
cp /tmp/.gitignore /app/.gitignore
Enter fullscreen mode Exit fullscreen mode

This Dockerfile also makes use of Multistage Docker, so that specific parts can be effectively reused throughout different environments if you wish to use the same setup for different workflows (testing, staging, and so on).

A Rails Docker Compose

The Docker Compose file in ./environment/docker-compose.development.yml helps glue all the services together for a functional Ruby on Rails application on Docker:

  • An nginx HTTP server
  • A Ruby application
  • A PostgreSQL database server

The following is the Rails Docker Compose file in use by this setup:

version: '3.7'
services:
  nginx:
    image: "${PROJECT_NAME}/nginx:development"
    container_name: ${PROJECT_NAME}-nginx
    build: 
      context: ./
      dockerfile: ./containers/nginx/Dockerfile
    depends_on:
      - ruby
    tty: true
    ports:
      - 80:80
    volumes:
      - ./`artifacts`/nginx/:/`var`/log/nginx:cached
   ruby:
    image: "${PROJECT_NAME}/ruby:development"
    container_name: ${PROJECT_NAME}-ruby
    depends_on:
      - postgres
    build:
      context: ./
      dockerfile: ./containers/ruby/Dockerfile
      target: development
    ports:
      - 13030:13030
    volumes:
      - ${APP_PATH}:/app:cached
    environment:
      DB_NAME: ${DB_NAME}
      PROJECT_NAME: ${PROJECT_NAME}
      DB_USER: ${DB_USER}
      DB_PASSWORD: ${DB_PASSWORD}
      DB_PORT: ${DB_PORT}
      PUMA_WORKERS: 0
      RAILS_MAX_THREADS: 1
     postgres:
    image: "${PROJECT_NAME}/postgres:development"
    container_name: ${PROJECT_NAME}-postgres
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    build:
      context: ./
      dockerfile: ./containers/postgres/Dockerfile
    ports:
      - ${DB_PORT}:5432
    volumes:
      - postgres_volume:/var/lib/postgresql/data

volumes:
  postgres_volume:
Enter fullscreen mode Exit fullscreen mode

Makefile for Docker

Finally, in order to create an accessible interface for developers to easily interact with this local development environment for Ruby, a Makefile is used:

.PHONY: help
# Make stuff

-include .env

export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1

.DEFAULT_GOAL := help

ARTIFACTS_DIRECTORY := "./artifacts"

CURRENT_PATH :=${abspath .}

SHELL_CONTAINER_NAME := $(if $(c),$(c),ruby)
BUILD_TARGET := $(if $(t),$(t),development)

help: ## Help.
    @grep -E '^[a-zA-Z-]+:.*?## .*$$' Makefile | awk 'BEGIN {FS = ":.*?## "}; {printf "[32m%-27s[0m %s\n", $$1, $$2}'

init: ## Project installation.
    @rm -f ./.env
    @cp .env.example .env
    @make init_app_directory
    @make create_postgress_volume
    @docker-compose -f docker-compose.init.yml build
    @docker-compose -f docker-compose.init.yml up

build: ## Build images.
    @make create_project_artifacts
    @cp ${APP_PATH}/Gemfile "${ARTIFACTS_DIRECTORY}/rails/Gemfile"
    @cp ${APP_PATH}/Gemfile.lock "${ARTIFACTS_DIRECTORY}/rails/Gemfile.lock"
    @docker-compose -f docker-compose.$(BUILD_TARGET).yml build

shell: ## Internal image bash command line.
    @if [[-z `docker ps | grep ${SHELL_CONTAINER_NAME}`]]; then \
        echo "${SHELL_CONTAINER_NAME} is NOT running (make start)."; \
    else \
        docker-compose -f docker-compose.$(BUILD_TARGET).yml exec $(SHELL_CONTAINER_NAME) /bin/ash; \
    fi

start: ## Start previously builded application images.
    @make create_project_artifacts
    @make start_postgres
    @make start_ruby
    @make start_nginx

run: ## Run ruby debugger session.
    @docker-compose -f docker-compose.$(BUILD_TARGET).yml exec ruby /bin/ash /rdebug_ide/runner.sh

start_ruby: ## Start ruby image.
    @if [[-z `docker ps | grep ruby`]]; then \
        docker-compose -f docker-compose.$(BUILD_TARGET).yml up -d ruby; \
    else \
        echo "Ruby is running."; \
    fi

start_postgres: ## Start postgres image.
    @if [[-z `docker ps | grep postgres`]]; then \
        docker-compose -f docker-compose.$(BUILD_TARGET).yml up -d postgres; \
    else \
        echo "Postgres is running."; \
    fi

start_nginx: ## Start nginx image.
    @if [[-z `docker ps | grep nginx`]]; then \
        docker-compose -f docker-compose.$(BUILD_TARGET).yml up -d nginx; \
    else \
        echo "Nginx is running."; \
    fi

stop: ## Stop all images.
    @docker-compose -f docker-compose.$(BUILD_TARGET).yml stop

create_project_artifacts:
    mkdir -p ./artifacts/rails
    mkdir -p ./artifacts/db

init_app_directory:
    @mkdir -p ${APP_PATH}

create_postgress_volume:
    @sed -i '' -r "s/postgres_volume:/${PROJECT_NAME}_db_volume:/g" docker-compose.development.yml
Enter fullscreen mode Exit fullscreen mode

Running Ruby applications efficiently

Hopefully you’ve now earned a new skill of running Ruby applications, such as Ruby on Rails, on your local environment, via the use of Docker containers configuration. Another way of running local Ruby applications is through the use of Ruby virtual environments, which we covered previously in our post on how to install Ruby on macOS.

Now that you’ve got your Ruby development environment all worked out, you might also want to learn how to secure your Ruby applications. Check out the following articles for more information on Ruby security:

Secure your Ruby applications for free

Create a Snyk account today to find and fix vulnerabilities in your Ruby containers.

Sign up for free

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .