Dockerizing your Ruby on Rails with a React front-end application can dramatically improve your development workflow and deployment process. By creating a standardized environment for your app, you ensure consistent behavior across different stages of development, testing, production, and even across different systems. In fact, it is designed to minimize issues related to system differences. This guide will walk you through the essential steps to get your Rails and React app running smoothly in Docker containers.

Why Dockerize an Application?

NB: A knowledge of Docker syntax is required

Dockerization involves two key concepts: images and containers. Images serve as blueprints for containers, containing all the necessary information to create a container, including dependencies and deployment configurations. A container is a runtime instance of an image, comprising the image itself, an execution environment, and runtime instructions. Docker in general, establishes a standard for shipping software.

To explain Docker with a simple analogy: think of containers as the shipping containers in a yard, images as the items placed inside these containers, and the shipping vessel as the system on which the containers run.

Whenever you set up and build your application, certain environment configurations are necessary. For example, you cannot run a Rails application without a Ruby environment installed on your system. Similarly, you cannot run a React application without Node.js, and you cannot install React packages without a Node package manager like npm or Yarn etc.

Since the container runs in isolation from the user’s system, we are going to make all these packages available in our container just like we would have done in case we built it directly on our system, thus, the container will act as a system on it own, like a virtual machine. There are differences between docker and virtual machine but this example is just to explain further.

Now, let’s go ahead and dockerize the Rails application. To do this, we will need three files in our Rails application: a Dockerfile, a docker-compose.yml, and a bin/docker-entrypoint. Let’s examine each of these files in detail.

NB: A knowledge of Docker syntax is required

Dockerfile

The Dockerfile is a blueprint for creating a Docker container. It contains a series of instructions that Docker uses to build an image, which can then be used to run containers. Let's break down a Dockerfile for a Ruby on Rails and React application:

. Base Image

ARG RUBY_VERSION=3.1.4
FROM ruby:$RUBY_VERSION

2. Install Dependencies

RUN apt-get update -qq && \
    apt-get install -y build-essential libvips bash bash-completion libffi-dev tzdata postgresql curl && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man

3. Install Node.js and Yarn

RUN curl -fsSL https://deb.nodesource.com/setup_current.x | bash - && \
    apt-get install -y nodejs && \
    curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
    echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
    apt-get update && \
    apt-get install -y yarn

4. Environment Variables

ENV NODE_OPTIONS=--openssl-legacy-provider

5. Set Working Directory

WORKDIR /rails

6. Build Arguments and Environment Variables

ARG RAILS_ENV
ENV RAILS_ENV=$RAILS_ENV

7. Install Application Gems

COPY Gemfile Gemfile.lock ./
RUN bundle install

8. Install Front-end Dependencies

COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile

9. Copy Application Code

COPY . .

10. Pre-compile Bootsnap Code

RUN bundle exec bootsnap precompile --gemfile app/ lib/

11. Pre-compile Assets for Production

RUN if [ "$RAILS_ENV" = "production" ]; then \
    SECRET_KEY_BASE=1 bin/rails assets:precompile; \
    fi

12. Entrypoint Script

COPY bin/docker-entrypoint /rails/bin/
RUN chmod +x /rails/bin/docker-entrypoint

13. Define Entrypoint and Command

ENTRYPOINT ["/rails/bin/docker-entrypoint"]
EXPOSE 5000 // you can use any port of your choice
CMD ["./bin/rails", "server"]

docker-compose.yml

The docker-compose.yml file is used to define and run multi-container Docker applications. It allows you to configure your application's services, networks, and volumes in a single file. In this case, we are going to use two services. Here’s the docker-compose.yml file for the Rails application:

1. Database Service (db)

codedb:
  image: postgres:14.2-alpine
  container_name: demo-postgres-14.2
  volumes:
    - postgres_data:/var/lib/postgresql/data
  command: "postgres -c 'max_connections=500'"
  environment:
    POSTGRES_DB: ${POSTGRES_DB}
    POSTGRES_USER: ${POSTGRES_USER}
    POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
  ports:
    - "5432:5432"

2. Web Application Service (demo-web)

codedemo-web:
  build:
    context: .
    args:
      - RAILS_ENV=${RAILS_ENV}
  command: "./bin/rails server -b 0.0.0.0"
  environment:
    - RAILS_ENV=${RAILS_ENV}
    - POSTGRES_HOST=${POSTGRES_HOST}
    - POSTGRES_DB=${POSTGRES_DB}
    - POSTGRES_USER=${POSTGRES_USER}
    - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    - RAILS_MASTER_KEY=${RAILS_MASTER_KEY}
  volumes:
    - .:/rails
    - app-storage:/rails/storage
  depends_on:
    - db
  ports:
    - "3000:3000"

bin/docker-entrypoint

The bin/docker-entrypoint script is a crucial part of the Docker setup. It is executed when the container starts, and it typically handles environment setup, database preparation, and other initialization tasks needed before starting the main application. Here’s an example bin/docker-entrypoint script and a detailed explanation of each part:

Shebang and Exit on Error

bashCopy code#!/bin/bash
set -e

Conditional Database Creation or Migration

# If running the rails server then create or migrate existing database
if [ "${*}" == "./bin/rails server" ]; then
  ./bin/rails db:create
  ./bin/rails db:prepare
fi

Conclusion

A well-crafted Dockerfile is essential for creating a reliable and consistent environment for your Ruby on Rails and React application. By defining the base image, setting environment variables, and installing dependencies, you ensure that your application runs smoothly across various environments.

Docker not only streamlines your development process but also enhances the reliability of your application in production. There are areas of optimizations, but this is just a general overview of how to dockerize the rails application.

Full Script for the Resulting Dockerfile, docker-compose.yml and bin/docker-entrypoint


ARG RUBY_VERSION=3.1.4
FROM ruby:$RUBY_VERSION

# Install dependencies
RUN apt-get update -qq && \
    apt-get install -y build-essential libvips bash bash-completion libffi-dev tzdata postgresql curl && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man

# Install Node.js and Yarn
RUN curl -fsSL https://deb.nodesource.com/setup_current.x | bash - && \
    apt-get install -y nodejs && \
    curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
    echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
    apt-get update && \
    apt-get install -y yarn

# Set environment variable to enable legacy OpenSSL support
ENV NODE_OPTIONS=--openssl-legacy-provider

# Rails app lives here
WORKDIR /rails

# Set environment variable for the build
ARG RAILS_ENV
ENV RAILS_ENV=$RAILS_ENV

# Install application gems
COPY Gemfile Gemfile.lock ./
RUN bundle install

# Install frontend dependencies
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile

# Copy application code
COPY . .

# Precompile bootsnap code for faster boot times
RUN bundle exec bootsnap precompile --gemfile app/ lib/

# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN if [ "$RAILS_ENV" = "production" ]; then \
    SECRET_KEY_BASE=1 bin/rails assets:precompile; \
    fi

# Entrypoint prepares the database.
COPY bin/docker-entrypoint /rails/bin/
RUN chmod +x /rails/bin/docker-entrypoint

# Use an absolute path for the entry point script
ENTRYPOINT ["/rails/bin/docker-entrypoint"]

# Start the server by default, this can be overwritten at runtime
EXPOSE 5000
CMD ["./bin/rails", "server"]

services:
  db:
    image: postgres:14.2-alpine
    container_name: demo-postgres-14.2
    volumes:
      - postgres_data:/var/lib/postgresql/data
    command: "postgres -c 'max_connections=500'"
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    ports:
      - "5432:5432"
  demo-web:
    build:
      context: .
      args:
        - RAILS_ENV=${RAILS_ENV}
    command: "./bin/rails server -b 0.0.0.0"
    environment:
      - RAILS_ENV=${RAILS_ENV}
      - POSTGRES_HOST=${POSTGRES_HOST}
      - POSTGRES_DB=${POSTGRES_DB}
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - RAILS_MASTER_KEY=${RAILS_MASTER_KEY}
    volumes:
      - .:/rails
      - app-storage:/rails/storage
    depends_on:
      - db
    ports:
      - "3000:3000"
volumes:
  postgres_data:
  app-storage:

#!/bin/bash
set -e

# If running the rails server then create or migrate existing database
if [ "${*}" == "./bin/rails server" ]; then
  ./bin/rails db:create
  ./bin/rails db:prepare
fi

exec "${@}"