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 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.
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.
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.
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
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:
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.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.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.ENV NODE_OPTIONS=--openssl-legacy-provider
ENV NODE_OPTIONS=--openssl-legacy-provider
: Sets an environment variable to enable legacy OpenSSL support for Node.js.WORKDIR /rails
WORKDIR /rails
: Sets the working directory for subsequent instructions to /rails
.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.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
.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
.COPY . .
COPY . .
: Copies all application code to the working directory.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.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.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.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.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:
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.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.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.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:
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
"${*}"
) 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
.
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.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.
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 "${@}"