paint-brush
A Simplified Guide for the"Dockerazition" of Ruby and Rails With React Front-End App by@forison
198 reads

A Simplified Guide for the"Dockerazition" of Ruby and Rails With React Front-End App

by Addo Boakye ForisonAugust 4th, 2024
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

Dockerization involves two key concepts: images and containers. Images serve as blueprints for containers, containing all the necessary information to create a container. A container is a runtime instance of an image, comprising the image itself, an execution environment, and runtime instructions. In this article, we will provide a hands-on guide to dockerizing your Rails and React applications in detail.
featured image - A Simplified Guide for the"Dockerazition" of Ruby and Rails With React Front-End App
Addo Boakye Forison HackerNoon profile picture

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.

Docker to the rescue

Why Dockerize an Application?

  • Consistency Across Environments:
    • Docker ensures that the application runs the same way regardless of where it is deployed, whether on a developer's machine, a testing environment, or a production server. This consistency is achieved by containerizing all dependencies and configurations.


  • Dependency Management:
    • Docker containers include all necessary dependencies for the application to run. This means that variations in system libraries or missing dependencies on different systems do not affect the application's functionality.


  • Isolation:
    • Docker containers run in isolation from each other and from the host system. This isolation prevents conflicts between different applications and their dependencies on the same system.


  • Portability:
    • Docker containers can be easily moved and run on any system that supports Docker, whether it is a local machine, a cloud service, or a dedicated server. This makes the application highly portable and flexible in terms of deployment.


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
  • ARG RUBY_VERSION=3.1.4: Defines a build argument named RUBY_VERSION with a default value of 3.1.4. This can be overridden at build time.


  • FROM ruby:$RUBY_VERSION: Uses the ruby base image with the version specified by RUBY_VERSION. This sets up the container with the Ruby runtime. Just like I mentioned earlier, to run a Rails application, you need to have Ruby installed.

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
  • apt-get update -qq: Updates the package list from the repositories, with -qq for quiet output.


  • apt-get install -y ...: Installs various packages:
    • build-essential: Essential packages for building software (like GCC).

    • libvips: Library for image processing.

    • bash, bash-completion: Bash shell and its auto-completion.

    • libffi-dev: Foreign Function Interface library.

    • tzdata: Time zone data.

    • postgresql: PostgreSQL database client.

    • curl: Tool to transfer data from URLs.


  • apt-get clean: Cleans up the local repository of retrieved package files.


  • rm -rf /var/lib/apt/lists/ /usr/share/doc /usr/share/man: Removes package lists and documentation to reduce image size.

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
  • curl -fsSL https://deb.nodesource.com/setup_current.x | bash -: Downloads and runs the NodeSource setup script to install Node.js.


  • apt-get install -y nodejs: Installs Node.js.


  • curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -: Adds the Yarn GPG key to verify its packages.


  • echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list: Adds Yarn's repository to the list of sources.


  • apt-get update && apt-get install -y yarn: Updates the package list and installs Yarn.

4. Environment Variables

ENV NODE_OPTIONS=--openssl-legacy-provider
  • ENV NODE_OPTIONS=--openssl-legacy-provider: Sets an environment variable to enable legacy OpenSSL support for Node.js.

5. Set Working Directory

WORKDIR /rails
  • WORKDIR /rails: Sets the working directory for subsequent instructions to /rails.

6. Build Arguments and Environment Variables

ARG RAILS_ENV
ENV RAILS_ENV=$RAILS_ENV
  • ARG RAILS_ENV: Defines a build argument named RAILS_ENV for specifying the Rails environment (like development, test, production).


  • ENV RAILS_ENV=$RAILS_ENV: Sets the environment variable RAILS_ENV to the value of the build argument.

7. Install Application Gems

COPY Gemfile Gemfile.lock ./
RUN bundle install
  • COPY Gemfile Gemfile.lock ./: Copies the Gemfile and Gemfile.lock to the working directory.


  • RUN bundle install: Installs Ruby gems specified in the Gemfile.

8. Install Front-end Dependencies

COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
  • COPY package.json yarn.lock ./: Copies the package.json and yarn.lock to the working directory.


  • RUN yarn install --frozen-lockfile: Installs front-end dependencies using Yarn, ensuring it uses the exact versions in yarn.lock.

9. Copy Application Code

COPY . .
  • COPY . .: Copies all application code to the working directory.

10. Pre-compile Bootsnap Code

RUN bundle exec bootsnap precompile --gemfile app/ lib/
  • RUN bundle exec bootsnap precompile --gemfile app/ lib/: Pre-compiles Bootsnap cache for faster Rails application boot times. Bootsnap is a gem that speeds up Ruby and Rails boot times by caching expensive computations.

11. Pre-compile Assets for Production

RUN if [ "$RAILS_ENV" = "production" ]; then \
    SECRET_KEY_BASE=1 bin/rails assets:precompile; \
    fi
  • RUN if [ "$RAILS_ENV" = "production" ]; then ...: Conditionally runs the asset pre-compilation only if RAILS_ENV is set to production. This step is crucial for preparing assets for a production environment.

12. Entrypoint Script

COPY bin/docker-entrypoint /rails/bin/
RUN chmod +x /rails/bin/docker-entrypoint
  • COPY bin/docker-entrypoint /rails/bin/: Copies a custom entrypoint script to the container.


  • RUN chmod +x /rails/bin/docker-entrypoint: Makes the entrypoint script executable.

13. Define Entrypoint and Command

ENTRYPOINT ["/rails/bin/docker-entrypoint"]
EXPOSE 5000 // you can use any port of your choice
CMD ["./bin/rails", "server"]
  • ENTRYPOINT ["/rails/bin/docker-entrypoint"]: Sets the entrypoint script that will run when the container starts. This script typically sets up the environment, prepares the database, and starts the application.


  • EXPOSE 5000: Indicates that the container listens on port 5000. This is a documentation feature and does not publish the port.


  • CMD ["./bin/rails", "server"]: Specifies the default command to run when the container starts, which is to start the 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"
  • image: postgres:14.2-alpine: Specifies the Docker image to use for this service. In this case, it's the PostgreSQL 14.2 image based on the Alpine Linux distribution. Alpine images are known for their small size, which can help keep the overall image size down.


  • container_name: demo-postgres-14.2: Names the container demo-postgres-14.2. This name is used to reference the container in commands and logs.


  • volumes:
    • postgres_data:/var/lib/postgresql/data: Mounts a named volume postgres_data to /var/lib/postgresql/data inside the container. This directory is where PostgreSQL stores its data, ensuring that the database data persists between container restarts.


  • command: "postgres -c 'max_connections=500'": Overrides the default command of the PostgreSQL image. It starts PostgreSQL with a configuration option to increase the maximum number of connections to 500.


  • environment:
    • POSTGRES_DB: ${POSTGRES_DB}: Sets the name of the default database to create, using an environment variable POSTGRES_DB.

    • POSTGRES_USER: ${POSTGRES_USER}: Sets the default username for accessing the PostgreSQL database, using the POSTGRES_USER environment variable.

    • POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}: Sets the password for the default user, using the POSTGRES_PASSWORD environment variable.


  • ports:
    • "5432:5432": Maps port 5432 on the host to port 5432 in the container. This allows access to PostgreSQL on the host machine via port 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"
  • build:

    • context: .: Specifies the build context for the Docker image. In this case, . refers to the current directory. This means Docker will use the Dockerfile in the current directory to build the image.
    • args:
      • RAILS_ENV=${RAILS_ENV}: Passes the RAILS_ENV build argument to the Docker build process, allowing you to specify the Rails environment (like development, test, or production).


  • command: "./bin/rails server -b 0.0.0.0": Overrides the default command of the Docker image. Starts the Rails server and binds it to all network interfaces (0.0.0.0), which is necessary for the service to be accessible from outside the container.


  • environment:

    • RAILS_ENV=${RAILS_ENV}: Sets the Rails environment inside the container using the RAILS_ENV environment variable.

    • POSTGRES_HOST=${POSTGRES_HOST}: Sets the PostgreSQL host address.

    • POSTGRES_DB=${POSTGRES_DB}: Sets the database name.

    • POSTGRES_USER=${POSTGRES_USER}: Sets the PostgreSQL user.

    • POSTGRES_PASSWORD=${POSTGRES_PASSWORD}: Sets the PostgreSQL user password.

    • RAILS_MASTER_KEY=${RAILS_MASTER_KEY}: Sets the Rails master key, which is used for encrypting credentials and other secrets.


  • volumes:

    • .:/rails: Mounts the current directory (where the docker-compose.yml file is located) to /rails inside the container. This allows you to edit files on your host and have those changes reflected inside the container.

    • app-storage:/rails/storage: Mounts a named volume app-storage to /rails/storage inside the container. This is typically used for storing Rails-specific files such as logs, uploads, and cached files.


  • depends_on:

    • db: Ensures that the demo-web service waits for the db service to be ready before starting. Docker Compose handles the order of starting services based on this setting.
  • ports:

    • "3000:3000": Maps port 3000 on the host to port 3000 in the container. This allows you to access the Rails application on the host machine via port 3000.

    Volumes

    codevolumes:
      postgres_data:
      app-storage:
    
    
    • postgres_data: Defines a named volume postgres_data used by the db service to persist PostgreSQL data.
    • app-storage: Defines a named volume app-storage used by the demo-web service to persist application-specific data, such as uploads and logs.

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
  • #!/bin/bash: This line specifies that the script should be run using the Bash shell.


  • set -e: This instructs the script to exit immediately if any command returns a non-zero exit code. This helps ensure that if any step fails, the script stops execution, which can prevent subsequent steps from running in an invalid state.


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


  • if [ "${*}" == "./bin/rails server" ]; then ... fi: This conditional statement checks if the command passed to the script ("${*}") is ./bin/rails server. The * is a special parameter that holds all the positional parameters passed to the script.


  • ./bin/rails db

    : If the condition is met, this command will attempt to create the database. It is equivalent to running rails db:create which sets up the database as defined in the database configuration file (config/database.yml).


  • ./bin/rails db

    : This command will run rails db:prepare, which ensures the database is set up and migrated. It will create the database if it doesn't exist and run migrations if the database is already created. This is a combination of rails db:create and rails db:migrate.

    Executing the Main Process

    bashCopy codeexec "${@}"
    
    
    • exec "${@}": This replaces the current shell process with the command passed as arguments to the script. The @ symbol holds all the positional parameters passed to the script. For example, if the script is called with ./bin/rails server, this line effectively runs ./bin/rails server as the main process of the container.

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 "${@}"