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.
Top comments (2)
Edit:
Make sure to run
Befor migrating and seeding your DB
Thanks for this, helped me a lot!
Thanks a lot!
Two adds to Ubuntu:
The Docker need run your entrypoint.sh as a script:
chmod +x entrypoint.sh
If 5432 port is blocked, you need:
sudo ss -lptn 'sport = :5432'
and kill