Rafi
Posted on November 24, 2020
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
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
Let's add has_many
macro in Author
model
# app/models/author.rb
class Author < ApplicationRecord
has_many :articles
end
And populate DB with some seed data. Install faker first
bundle add faker
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
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
Let's prefix our routes with api
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.
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
#!/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.
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.
# 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
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.
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
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
docker-compose up
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"
},
...
]
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
Posted on November 24, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.