DEV Community

Cover image for Deploy a Ruby on Rails API only application in Docker with PostgreSQL
Rafi
Rafi

Posted on

Deploy a Ruby on Rails API only application in Docker with PostgreSQL

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

Docker 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 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

rails new docker-rails \
  --database=postgresql \
  --skip-action-mailbox \
  --skip-action-text \
  --skip-spring -T \
  --skip-turbolinks \
  --api
Enter fullscreen mode Exit fullscreen mode

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

rails g resource Author name:string --no-test-framework
rails g resource Article title:string body:text author:references --no-test-framework
Enter fullscreen mode Exit fullscreen mode

Let's add has_many macro in Author model

# app/models/author.rb
class Author < ApplicationRecord
    has_many :articles
end
Enter fullscreen mode Exit fullscreen mode

And populate DB with some seed data. Install faker first

bundle add faker
Enter fullscreen mode Exit fullscreen mode

Then do bundle install and then update seeds file with

# 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
Enter fullscreen mode Exit fullscreen mode

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

# 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
Enter fullscreen mode Exit fullscreen mode

Let's prefix our routes with api

Rails.application.routes.draw do
    scope :api do
        resources :articles
        resources :authors
    end
end
Enter fullscreen mode Exit fullscreen mode

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.

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"]
Enter fullscreen mode Exit fullscreen mode

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

#!/bin/bash

set -e

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

exec "$@"
Enter fullscreen mode Exit fullscreen mode

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.

docker build -t rails-docker .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

# 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'] %>
Enter fullscreen mode Exit fullscreen mode

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

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:
Enter fullscreen mode Exit fullscreen mode

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.

docker-compose -f docker-compose.yml up --build
Enter fullscreen mode Exit fullscreen mode

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

docker-compose run web rails db:migrate
docker-compose run web rails db:seed
Enter fullscreen mode Exit fullscreen mode

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

docker-compose up
Enter fullscreen mode Exit fullscreen mode

Let's hit some endpoints to check

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"
   },
   ...
]
Enter fullscreen mode Exit fullscreen mode

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

Top comments (2)

Collapse
 
filippomassarelli profile image
Filippo Massarelli

Edit:
Make sure to run

docker-compose run web rails db:create
Enter fullscreen mode Exit fullscreen mode

Befor migrating and seeding your DB

Thanks for this, helped me a lot!

Collapse
 
jaguiarinfo profile image
Julio Aguiar

Thanks a lot!
Two adds to Ubuntu:

  1. The Docker need run your entrypoint.sh as a script:
    chmod +x entrypoint.sh

  2. If 5432 port is blocked, you need:
    sudo ss -lptn 'sport = :5432'
    and kill