DEV Community

Cover image for Dockerizing a Rails application for production deployment
Fernando Shayani
Fernando Shayani

Posted on

Dockerizing a Rails application for production deployment

After many years using manual tools to deploy my Ruby on Rails applications, I changed my production deployment strategy to use Docker containers few years ago, and it was a great decision!

This tutorial is for begginers that want to dockerize their new (or existing) Rails application.

ℹ️ You can download all files on my Github repository.


Table Of Contents

Why dockerize my Rails application?

Turn you Rails application into docker containers for production deployment can speed up your deployment, prevent errors and make it much easier to move from one service provider to another. It made my life easier (and I bet it can make yours too!).

Once in a container, all dependencies will always be installed and the system already configured to run an optimized version of you application.

It's also easy to update your container, run tests, use CI/CD and automatically deploy new version.

Starting with a basic Rails application

For demonstration purposes, this Rails application is a simple contact list running Rails 6.1. The user can add, view, edit and delete records from the database. All records are saved on a local PostgreSQL instance.

The setup

Everything is setup on the local machine. Rails connects to PostgreSQL through port 5432 and serves the application to browsers through port 3000.

Localmachine diagram

To follow this tutorial, I assume you already have your Rails application working and running. If not, you can use the tutorial application for your tests.

Follows a screenshot of the application running.

Application screenshot

Let's start!

There are few steps to build a container to run our application.

  1. Adjust your database connection configuration.
  2. Write a dockerfile and build the container.
  3. Upload your container to any could provider (free and optional).
  4. Write a Docker Compose file to execute your application on production.

The config/database.yml file

This is the basic Rails configuration to connect the application to the datbase. It assumes there's a local instance of PostgreSQL running with username and password configured as below.

default: &default
  adapter: postgresql
  port: 5432
  pool: 5
  timeout: 5000
  host: localhost
  username: postgres
  password: postgres

development:
  <<: *default
  database: devel_db

test:
  <<: *default
  database: test_db

production:
  <<: *default
  database: production_db
Enter fullscreen mode Exit fullscreen mode

As you can see, all database connection configuration is fixed to the current environment. This can work for local environments, but as a good practice we should change them to environment variables, so we can adjust easily for any new environment we publish our app.

Let's change host, username and password for now. The others settings can also be changed, but I don't want this tutorial to be too complex 😉

host: <%= ENV['DB_HOST'] || 'localhost' %>
username: <%= ENV['DB_USERNAME'] || 'postgres' %>
password: <%= ENV['DB_PASSWORD'] || 'postgres' %>
Enter fullscreen mode Exit fullscreen mode

⚠️ Notice that we set default values for everything.

The Dockerfile

The next step is create a file called Dockerfile on the root path of your application. This file will instruct docker on how to build the application based on a image you choose.

I decided to use the image ruby:2.7.6-bullseye because it's based on Debian 11 (Bullseye) and I like (and I'm used to) this distribuition.

FROM ruby:2.7.6

# Directory where the app will be installed on the container
WORKDIR /app

# Install NodeJS 14 repository
RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash -

# Update the system and install NodeJS
RUN apt-get update && apt-get -y install \
  nodejs \
  vim \
  && rm -rf /var/lib/apt/lists/*

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

# Install Yarn globally
RUN npm install -g yarn

# Run `yarn install`
COPY package.json yarn.lock ./
RUN yarn install

# Copy the application file to the container
COPY . .

# Pre-compile assets for production
RUN RAILS_ENV=production bundle exec rails assets:precompile

# Sets the default command that will run when the container starts
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
Enter fullscreen mode Exit fullscreen mode

ℹ️ More information of Dockerfile can be found on this link.

The .dockerignore file

There are files and folders that we do not want to be copied to our docker container because they are not relevant to the application or they will be generated when the container is created. They are build folders, temporary files, log, etc.

Create a .dockerignore file on your project root folder and add these lines:

node_modules/
log/
tmp/
Enter fullscreen mode Exit fullscreen mode

Building the container

Now it's time to build our application container.

⚠️ IMPORTANT: The tag attribute is the name of the container. Be sure to change it to your Doker Hub repository name, or use any name if you do not intent to push it to a cloud.

docker build . --tag fshayani/dockerizing_rails_application:latest
Enter fullscreen mode Exit fullscreen mode

After the container is built successfully, we will upload it to Docker Hub. This way I can download it automatically on my production server.

ℹ️ This is a optional step. You can skip this step if you intent to build the container and use it on the same machine.

docker push fshayani/dockerizing_rails_application:latest
Enter fullscreen mode Exit fullscreen mode

Going production!

Now that we have built our app container (and maybe pushed it to a cloud; did you?), it's time to move to the production server and deploy that application!

We need tell Docker how to pull and start the container and this can be done easly with docker compose.

ℹ️ There are others technologies to deploy containers in production instead of Docker Compose, like Kubernetes, Docker Swarm, AWS ECS, etc; but that's not the scope of this tutorial.

The Docker Compose file

Create a docker-compose.yml file on production server. It can be placed anywhere, but be organized! 😉

services:
  app: # That's the name of our application container
    image: fshayani/dockerizing_rails_application:latest # The container Docker will use
    ports:
     - 3000:3000 # Map localhost:3000 to the container's port 3000
    environment:
      - DB_HOST=db # The name of our PostgreSQL container
      - DB_USERNAME=postgres # PostgreSQL credentials
      - DB_PASSWORD=postgres
      - RAILS_ENV=production # We are on Production now!
      - RAILS_SERVE_STATIC_FILES=true # Assets were already built by Dockerfile ;)
      - RAILS_LOG_TO_STDOUT=true # So we can see Rails logs with `docker compose logs` command
      - RAILS_MASTER_KEY=my_ultra_top_secret_master_key!! # Use you master key. There are better ways to keep this secret.
    depends_on:
      - db # Will start the container `db` before

  db:
    image: postgres:14
    environment:
      - POSTGRES_USER=postgres # Will create a instance of PostgreSQL with this credentials
      - POSTGRES_PASSWORD=postgres
    volumes:
      - postgres:/var/lib/postgresql/data # Map PostgreSQL data to a persistant volume called `postgres`

volumes:
  postgres: # Create a persistant volume on local machine, so we do not loose our DB on restarts

Enter fullscreen mode Exit fullscreen mode

Starting up our application

Now that everything is ready, let's start our application on the production server!

⚠️ ATTENTION: If that's our first time to run the application on the server,
probably your database will be empty. You have two options: dump and restore
your database on the db container or create a new from scratch running this command first:
docker compose run --rm app bundle exec rails db:setup

docker compose up
Enter fullscreen mode Exit fullscreen mode

and open your browser on http://localhost:3000 😄 You will see your application running!

ℹ️ TIP: Add the flag -d to the command to run it on the background.

Accessing Rails console

With Docker Compose, we can interact with our container and run any command on it (including bash if necessary).

docker compose exec app bundle exec rails console
Enter fullscreen mode Exit fullscreen mode

Updating the containers

When you make a new version of your application and want to deploy it to the production, all you need to do is:

  1. Build the new image.
  2. Push it to the cloud again.
  3. Pull it on the production server the production server (docker compose pull).
  4. Rebuild your docker compose stack (docker compose down; docker compose up).

Configure your web server

Now that you application is deployed on the production server, you will have to configure any web server (Apache, Nginx, etc) to reverse proxy any calls to your domain to the port 3000 on localhost.

That's a topic for another post ;)

Final tip!

There a application called Watchtower that can automatically fetch any new container you upload to Docker Hub and automatically pulls it and rebuild the docker compose stack. Take a look at it ;)

Top comments (1)

Collapse
 
mhenrixon profile image
Mikael Henriksson

Great stuff! For a production image I’d use a multi-stage build that installs dependencies and compiles assets in one step and then copies the files in the second step.

Your image will be around 1,5GB. With my suggestion and using an alpine image you’d cut that down to 400MB.