Dockerizing a Rails application for production deployment
Tags: docker, rails, ruby, tutorial
Published: 2022-08-29 12:13:00


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.

> <small>ℹ️ You can download all files on [my Github repository](https://github.com/shayani/dockerizing_rails_app).
> </small>

---

## Table Of Contents
  * [Why dockerize my Rails application?](#why)
  * [Starting with a basic Rails application](#starting)
    * [The setup](#the-setup)
  * [Let's start!](#lets-start)
    * [The config/database.yml file](#config-database)
    * [The Dockerfile](#the-dockerfile)
    * [The .dockerignore](#the-dockerignore)
    * [Building the container](#building)
  * [Going production!](#going-production)
    * [The Docker Compose file](#docker-compose)
    * [Starting up our application](#starting-up)
    * [Acessing the Rails console](#rails-console)
  * [Updating the containers](#updating)
  * [Configure your web server](#web-server)
  * [Final tip!](#final-tip)



## Why dockerize my Rails application? <a name="why"></a>

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 <a name="starting"></a>

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 <a name="the-setup"></a>

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qygntibgo44f700ylsvt.png)

To follow this tutorial, I assume you already have your Rails application working and running. If not, you can use [the tutorial application](https://github.com/shayani/dockerizing_rails_app) for your tests.

Follows a screenshot of the application running.

![Application screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2e949aqahdzbr6q3fe2q.png)


## Let's start! <a name="lets-start"></a>

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 <a name="config-database"></a>

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.

```yaml
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
```

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 😉

```yaml
host: <%= ENV['DB_HOST'] || 'localhost' %>
username: <%= ENV['DB_USERNAME'] || 'postgres' %>
password: <%= ENV['DB_PASSWORD'] || 'postgres' %>
```

><small>⚠️ Notice that we set default values for everything.</small>

### The `Dockerfile` <a name="the-dockerfile"></a>

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.

```dockerfile
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"]
```

> <small>ℹ️ More information of `Dockerfile` can be found [on this link](https://docs.docker.com/engine/reference/builder/).</small>

### The `.dockerignore` file <a name="the-dockerignore"></a>

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/
```

### Building the container <a name="building"></a>

Now it's time to build our application container.

> <small>⚠️ **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. </small>

```bash
docker build . --tag fshayani/dockerizing_rails_application:latest
```

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

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

```
docker push fshayani/dockerizing_rails_application:latest
```

## Going production! <a name="going-production"></a>

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`.

> <small>ℹ️ 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.</small>

### The Docker Compose file <a name="docker-compose"></a>

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

```yaml
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

```

### Starting up our application <a name="starting-up"></a>

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

> <small>⚠️ **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`
> </small>

```bash
docker compose up
```
and open your browser on http://localhost:3000 😄 You will see your application running!

> <small>ℹ️ **TIP:** Add the flag `-d` to the command to run it on the background.</small>

### Accessing Rails console <a name="rails-console"></a>

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

```bash
docker compose exec app bundle exec rails console
```

## Updating the containers <a name="updating"></a>

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 <a name="web-server"></a>

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! <a name="final-tip"></a>

There a application called [Watchtower](https://containrrr.dev/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 ;)