Ruby on Rails Docker for local development environment
Mikhail Tereschenko
November 2, 2022
0 mins readHi 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)
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 detailsdevelopment.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:
Developer-first container security
Snyk finds and automatically fixes vulnerabilities in container images and Kubernetes workloads.