Deploy a Ruby on Rails API only application in Docker with PostgreSQL
Tags: docker, rails, ruby, api
Published: 2020-11-24 19:36:53

---
title: Deploy a Ruby on Rails API only application in Docker with PostgreSQL
published: true
description: 
tags: docker, rails, ruby, api
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/47qjegyxjsp8uq0oemkg.png
---

Project Link: https://github.com/Joker666/rails-api-docker

[Docker](https://www.docker.com/) allows packaging an application or service with all its dependencies into a single image which then can be hosted into different platforms like [Docker Hub](https://hub.docker.com/) or Github Container Registry. These images can pulled and shared with teammates for easy development or deployed to production with container orchestration tools like Kubernetes.

When we look at the current state of development, there are endless installation instructions on how to install and configure an application as well as all its dependencies. And even then it doesn't work, the Ruby version doesn't match or some dependency's version upgrade broke the installation. It is where Docker comes in handy, the image created once, can run in any platform as long as Docker is installed. Today we are going to deploy a Ruby on Rails application in Docker.

## Preparation
We will need a few tools installed in the system to get started with Rails development

- Ruby 2.7
- Rails 6.0
- Docker

With these installed, let's generate our API only project

```bash
rails new docker-rails \
  --database=postgresql \
  --skip-action-mailbox \
  --skip-action-text \
  --skip-spring -T \
  --skip-turbolinks \
  --api
```

Since we are creating API only project, we are skipping the installation of few Rails web specific tools like `action-text` or `turbolinks`. And we are making sure `--api` flag is there to create an API only Rails project.

## Making APIs
We are going to make two APIs. Authors and articles
```ruby
rails g resource Author name:string --no-test-framework
rails g resource Article title:string body:text author:references --no-test-framework
```
Let's add `has_many` macro in `Author` model
```ruby
# app/models/author.rb
class Author < ApplicationRecord
    has_many :articles
end
```
And populate DB with some seed data. Install faker first
```bash
bundle add faker
```
Then do `bundle install` and then update `seeds` file with
```ruby
# db/seeds.rb
require 'faker'

Author.delete_all
Article.delete_all

10.times do
    Author.create(name: Faker::Book.unique.author)
end

50.times do
    Article.create({
        title: Faker::Book.title,
        body: Faker::Lorem.paragraphs(number: rand(5..7)),
        author: Author.limit(1).order('RANDOM()').first # sql random
    })
end
```
Now, we do not have PostgreSQL running, so we cannot run the migrations or seed data. We would do that when we deploy in docker. Now lets update the controller files
```ruby
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
    before_action :find_article, only: :show
    def index
        @articles = Article.all
        render json: @articles
    end

    def show
        render json: @article
    end

    private
        def find_article
            @article = Article.find(params[:id])
        end
end

# app/controllers/author_controller.rb
class AuthorsController < ApplicationController
    before_action :find_author, only: :show
    def index
        @authors = Author.all
        render json: @authors
    end

    def show
        render json: @author
    end

    private
        def find_author
            @author = Author.find(params[:id])
        end
end
```
Let's prefix our routes with `api`
```ruby
Rails.application.routes.draw do
    scope :api do
        resources :articles
        resources :authors
    end
end
```
We are all set now to hit some endpoints.

## Enter Docker
To build a docker image we have to write a `Dockerfile`. What is a `Dockerfile` through? `Dockerfile` is where all the dependencies are bundled and additional commands or steps are written to be executed before building the image. There are built in images for Ruby. We will start with an image of Ruby 2.7. Let's write the `Dockerfile` first and then we will explain what is happening there.

```Dockerfile
FROM ruby:2.7

RUN apt-get update -qq && apt-get install -y postgresql-client
# throw errors if Gemfile has been modified since Gemfile.lock
RUN bundle config --global frozen 1

WORKDIR /app

COPY Gemfile Gemfile.lock ./
RUN bundle install

COPY . .

ENTRYPOINT ["./entrypoint.sh"]
EXPOSE 3000

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]
```

So we start from Ruby 2.7 pre-built image and then install PostgreSQL client into the system, this is a Debian based system so we use `apt-get`. Next, we freeze the bundle config which will actually help us maintain consistency over the dockerized system and host system. If `Gemfile` was modified but we did not run `bundle install`, this is where it will throw an error

Now we set our working directory inside the docker system to be `/app`. We copy over the `Gemfile` and `Gemfile.lock` over to the docker's `app` directory and run `bundle install` inside docker. After `bundle install` finishes we copy over all the files from our host system to the docker system.

After that, we execute a shell script which we will come back shortly and then we expose port 3000 and start the rails server binding it to 0.0.0.0. 

The `entrypoint.sh` file
```bash
#!/bin/bash

set -e

if [ -f tmp/pids/server.pid ]; then
    rm tmp/pids/server.pid
fi

exec "$@"
```
This helps fix a Rails-specific issue that prevents the server from restarting when a certain server.pid file pre-exists, this needs to be run on every docker start.

We are done with our docker file. Now let's build it.

```bash
docker build -t rails-docker .
```
This builds the image pulling all the dependencies and saves it with a tag `rails-docker:latest`

Now, we can run this image, but that won't necessarily help us since we need PostgreSQL running as well. We could use a local installation of the DB but here we will run the DB inside docker.

## Environment Setup
Let's add `.env` file the root of the directory
```
DBHOST=localhost
DBUSER=postgres
DBPASS=password
```
This is essentially our DB environment variables which we will overwrite with docker.
Now, let's update the `database.yml` file inside config so that it the Rails app can read from the environment variables to connect to PostgreSQL.
```ruby
# config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  <<: *default
  database: docker_rails_development
  username: <%= ENV['DBUSER'] %>
  password: <%= ENV['DBPASS'] %>
  host: <%= ENV['DBHOST'] %>
```
## Docker-Compose
Docker compose is a handy way to write all the docker images as services dependent on each other and running inside one internal network where they can talk to each other. We are going to run PostgreSQL as a service in docker-compose along with the image of Rails API we just built

```yml
version: '3.8'

services:
    web:
        build: .
        image: rails-docker
        restart: "no"
        environment:
            - DBHOST=postgresql
            - DBUSER=postgres
            - DBPASS=password
        ports:
            - 3000:3000
        depends_on:
            - postgresql

    postgresql:
        image: postgres
        restart: "no"
        ports:
            - 5432:5432
        environment:
            POSTGRES_DB: docker_rails_development
            POSTGRES_USER: postgres
            POSTGRES_PASSWORD: password
        volumes:
            - postgresdb:/var/lib/postgresql/data/

volumes:
    postgresdb:
```

So there are two services here. In the `postgresql` service, we are using the official `postgresql` image and passing some values for environment variables and exposing the internal `5432` port to the host machine. We add a docker volume with it so that it stores data there and data can survive a restart.

The `web` service, runs the image we just built for the API and depends on `postgresql` service. That means the `postgresql` service needs to be up and running first for `web` service to start running. This is cool. Since we specified the `POSTGRES_DB` environment in the `postgresql` service, if the database doesn't exist when running the PostGreSQL server for the first time, it will create the database. Great, now let's run the services.

```bash
docker-compose -f docker-compose.yml up --build
```

This will build the images first if they are not built already and then run them. We would see that both images are running in the console. Now let's do our migration and seeding.

Stop the services with `ctrl+c` and run
```bash
docker-compose run web rails db:migrate
docker-compose run web rails db:seed
```
This will run the rails commands from inside the `web` service container. We now have data populated.

Now let's run the services again with
```bash
docker-compose up
```

Let's hit some endpoints to check
```bash
curl localhost:3000/api/authors

[
   {
      "id":1,
      "name":"Lakendra Bergnaum",
      "created_at":"2020-11-24T13:25:29.507Z",
      "updated_at":"2020-11-24T13:25:29.507Z"
   },
   ...
]
```

Sure it fetches the authors from the API and prints in the console. And if hit the `articles` endpoint it would print the articles as well.

## Conclusion
That was a lot to grasp if you are starting with docker for the first time. But we covered how we can deploy a Rails API only app in docker along with PostgreSQL. This is a good starting point to build something awesome.

## Resouce
- https://dev.to/forksofpower/build-a-json-api-spec-compliant-api-with-rails-6-and-fastjsonapi-b19
- https://docs.docker.com/compose/rails/