Skip to main content

Ruby on Rails Docker for local development environment

Written by:

Mikhail Tereschenko

wordpress-sync/blog-feature-ruby-security

November 2, 2022

0 mins read

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.

1git clone git@github.com:mtereschenko/simple_ror_environment.git && cd simple_ror_environment && rm -rf .git

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

1cd ./environment

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:

1PROJECT_NAME=foo_bar_project

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

1make init

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.

1make build

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

1make start

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:

1make shell
2rails s -p 3000 –binding=0.0.0.0

Ruby on Rails Docker for local development environmentFinally, 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:

1FROM ruby:2.7.6-alpine3.16 as base_image
2RUN apk add --no-cache git \
3    build-base \
4    libpq-dev \
5    tzdata
6
7FROM base_image as development
8COPY ./artifacts/rails/Gemfile /tmp/Gemfile
9COPY ./artifacts/rails/Gemfile.lock /tmp/Gemfile.lock
10COPY ./containers/ruby/runners/runner.development.sh /rdebug_ide/runner.sh
11RUN cd /tmp && \
12  gem install ruby-debug-ide && \
13  gem install debase && \
14  bundle install && \
15  chmod +x /rdebug_ide/runner.sh && \
16  apk add --no-cache git \
17  nodejs \
18  yarn
19WORKDIR /app
20
21ENTRYPOINT ["tail", "-f", "/dev/null"]
22
23FROM base_image as init
24COPY ./containers/ruby/initializers/runner.init.sh /tmp/runner.sh
25COPY ./containers/ruby/initializers/database.yml /tmp/database.yml
26COPY ./containers/ruby/initializers/.gitignore /tmp/.gitignore
27COPY ./containers/ruby/initializers/development.rb /tmp/development.rb
28RUN chmod +x /tmp/runner.sh
29
30WORKDIR /app
31
32ENTRYPOINT ["/tmp/runner.sh"]

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:

1#!/bin/ash
2
3cd /app
4
5gem install rails
6
7rails new . -d=postgresql --skip-git
8
9yes | cp -rf /tmp/database.yml /app/config/database.yml
10yes | cp -rf /tmp/development.rb /app/config/environments/development.rb
11cp /tmp/.gitignore /app/.gitignore

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:

1version: '3.7'
2services:
3  nginx:
4    image: "${PROJECT_NAME}/nginx:development"
5    container_name: ${PROJECT_NAME}-nginx
6    build: 
7      context: ./
8      dockerfile: ./containers/nginx/Dockerfile
9    depends_on:
10      - ruby
11    tty: true
12    ports:
13      - 80:80
14    volumes:
15      - ./`artifacts`/nginx/:/`var`/log/nginx:cached
16   ruby:
17    image: "${PROJECT_NAME}/ruby:development"
18    container_name: ${PROJECT_NAME}-ruby
19    depends_on:
20      - postgres
21    build:
22      context: ./
23      dockerfile: ./containers/ruby/Dockerfile
24      target: development
25    ports:
26      - 13030:13030
27    volumes:
28      - ${APP_PATH}:/app:cached
29    environment:
30      DB_NAME: ${DB_NAME}
31      PROJECT_NAME: ${PROJECT_NAME}
32      DB_USER: ${DB_USER}
33      DB_PASSWORD: ${DB_PASSWORD}
34      DB_PORT: ${DB_PORT}
35      PUMA_WORKERS: 0
36      RAILS_MAX_THREADS: 1
37     postgres:
38    image: "${PROJECT_NAME}/postgres:development"
39    container_name: ${PROJECT_NAME}-postgres
40    environment:
41      POSTGRES_DB: ${DB_NAME}
42      POSTGRES_USER: ${DB_USER}
43      POSTGRES_PASSWORD: ${DB_PASSWORD}
44    build:
45      context: ./
46      dockerfile: ./containers/postgres/Dockerfile
47    ports:
48      - ${DB_PORT}:5432
49    volumes:
50      - postgres_volume:/var/lib/postgresql/data
51
52volumes:
53  postgres_volume:

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:

1.PHONY: help
2# Make stuff
3
4-include .env
5
6export DOCKER_BUILDKIT=1
7export COMPOSE_DOCKER_CLI_BUILD=1
8
9.DEFAULT_GOAL := help
10
11ARTIFACTS_DIRECTORY := "./artifacts"
12
13CURRENT_PATH :=${abspath .}
14
15SHELL_CONTAINER_NAME := $(if $(c),$(c),ruby)
16BUILD_TARGET := $(if $(t),$(t),development)
17
18help: ## Help.
19@grep -E '^[a-zA-Z-]+:.*?## .*$$' Makefile | awk 'BEGIN {FS = ":.*?## "}; {printf "[32m%-27s[0m %s\n", $$1, $$2}'
20
21init: ## Project installation.
22@rm -f ./.env
23@cp .env.example .env
24@make init_app_directory
25@make create_postgress_volume
26@docker-compose -f docker-compose.init.yml build
27@docker-compose -f docker-compose.init.yml up
28
29build: ## Build images.
30@make create_project_artifacts
31@cp ${APP_PATH}/Gemfile "${ARTIFACTS_DIRECTORY}/rails/Gemfile"
32@cp ${APP_PATH}/Gemfile.lock "${ARTIFACTS_DIRECTORY}/rails/Gemfile.lock"
33@docker-compose -f docker-compose.$(BUILD_TARGET).yml build
34
35shell: ## Internal image bash command line.
36@if [[ -z `docker ps | grep ${SHELL_CONTAINER_NAME}` ]]; then \
37echo "${SHELL_CONTAINER_NAME} is NOT running (make start)."; \
38else \
39docker-compose -f docker-compose.$(BUILD_TARGET).yml exec $(SHELL_CONTAINER_NAME) /bin/ash; \
40fi
41
42start: ## Start previously builded application images.
43@make create_project_artifacts
44@make start_postgres
45@make start_ruby
46@make start_nginx
47
48run: ## Run ruby debugger session.
49@docker-compose -f docker-compose.$(BUILD_TARGET).yml exec ruby /bin/ash /rdebug_ide/runner.sh
50
51start_ruby: ## Start ruby image.
52@if [[ -z `docker ps | grep ruby` ]]; then \
53docker-compose -f docker-compose.$(BUILD_TARGET).yml up -d ruby; \
54else \
55echo "Ruby is running."; \
56fi
57
58start_postgres: ## Start postgres image.
59@if [[ -z `docker ps | grep postgres` ]]; then \
60docker-compose -f docker-compose.$(BUILD_TARGET).yml up -d postgres; \
61else \
62echo "Postgres is running."; \
63fi
64
65start_nginx: ## Start nginx image.
66@if [[ -z `docker ps | grep nginx` ]]; then \
67docker-compose -f docker-compose.$(BUILD_TARGET).yml up -d nginx; \
68else \
69echo "Nginx is running."; \
70fi
71
72stop: ## Stop all images.
73@docker-compose -f docker-compose.$(BUILD_TARGET).yml stop
74
75create_project_artifacts:
76mkdir -p ./artifacts/rails
77mkdir -p ./artifacts/db
78
79init_app_directory:
80@mkdir -p ${APP_PATH}
81
82create_postgress_volume:
83@sed -i '' -r  "s/postgres_volume:/${PROJECT_NAME}_db_volume:/g" docker-compose.development.yml

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:

wordpress-sync/blog-feature-ruby-security

Level Up Your CI/CD Pipelines

See how these 8 tips can help you catch security issues in the pipe BEFORE you push to production ⭐️