Skip to content

Instantly share code, notes, and snippets.

@moeabdol
Last active July 25, 2017 18:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save moeabdol/c0ff4ebff92ea3be11d29912a83510db to your computer and use it in GitHub Desktop.
Save moeabdol/c0ff4ebff92ea3be11d29912a83510db to your computer and use it in GitHub Desktop.
CSTS Showcase

About This Document

In this document I will showcase code excerpts extracted from a toy project of mine CSTS. The purpose of this document is to demonstrate coding styles, techniques, and best practices I follow when developing Rails applications.

Content

About CSTS

Customer Support Ticketing System (CSTS) is a Ruby on Rails 5 project. Its was developed in less than a week as part of a coding challenge to demonstrate my coding abilities in Ruby, and Ruby on Rails.

CSTS is a simple customer support ticketing portal. The system maintains 2 models (User, Ticket). A user object can have one of three main roles (Customer, Agent, Admin). Following is the Entity Relationship Diagram ERD depicting the relationship between the User model and the Ticket model.


Entity Relationship Diagram

A ticket is a simple object that contains text explaining customer complaints, suggestions, feature requests, etc. A ticket is either resolved or unresolved. Only a customer can create tickets, and only agents can resolve them.

A customer can issue many tickets, and an agent can resolve many as well; hence, the many to many relationship between a user and a ticket. A customer can only view his/her tickets, while agents and admins can view all tickets in the system.

An admin user is responsible for managing all objects within the system. An admin; therefore, can edit and delete any user and/or ticket from the system. An admin is also responsible for assigning user roles. When a new user signs up, the system automatically assigns a 'Customer' role to the new user. Only an admin can change the user role from a Customer to an Agent.

Once a user has been assigned an 'Agent' role, he/she can then can start resolving tickets within the system. Admins and agent can also list and search all tickets in the system. The system implements authentication using the Devise gem and authorization using the Pundit gem.

Admins and agenst can export PDF files for a specific agent's resolved tickets within the last 30 days. This feature is important for higher management to measure agents' performance.

Dependencies

CSTS is a Rails 5.1 application that stores data in a MySQL backend database. All gem dependencies are listed in the Gemfile. The following table lists the most important gems used in the application.

Gem Function Description
devise Authentication Devise is a flexible authentication solution for Rails based on Warden.
pundit Authorization Minimal authorization through OO design and pure Ruby classes.
simple_form Forms Forms made easy for Rails! It's tied to a simple DSL, with no opinion on markup.
kaminari Pagination A Scope & Engine based, clean, powerful, customizable and sophisticated paginator for Ruby webapps.
prawn PDF Fast, nimble PDF writer for Ruby.
prawn-table PDF Provides support for tables in Prawn.
rspec-rails Testing rspec-rails is a testing framework for Rails 3.x, 4.x and 5.0.
guard-rspec Testing Guard::RSpec allows to automatically & intelligently launch specs when files are modified.
guard-livereload Development LiveReload guard allows to automatically reload your browser when 'view' files are modified.
shoulda-matchers Testing Shoulda Matchers provides RSpec- and Minitest-compatible one-liners that test common Rails functionality.
factory_girl_rails Testing factory_girl is a fixtures replacement with a straightforward definition syntax, support for multiple build strategies (saved instances, unsaved instances, attribute hashes, and stubbed objects), and support for multiple factories for the same class (user, admin_user, and so on), including factory inheritance.
capybara Testing Acceptance test framework for web applications.
capybara-webkit Testing A Capybara driver for headless WebKit to test JavaScript web apps.
pry-rails Debugging This is a small gem which causes rails console to open Pry.
pry-byebug Debugging Step-by-step debugging and stack navigation in Pry
better_errors Debugging Better Errors replaces the standard Rails error page with a much better and more useful error page.
  • Note that capybara-webkit depends on Qt 5 which can be installed locally using apt-get on ubuntu machines or Homebrew on OSX.

Refactoring

I always try to abide by the SOLID principles whenever I write code. I have miniaml comments in my code as I strongly believe in self-documenting code. My classes, methods, and variables have descriptive names explaining their purpose and functionality. I try to make my classes not exceed 100 lines of code, if it does however, that might be an indication that I'm violating the single responsibility principle. I also try to follow Sandi Metz's Squint Test. That is, if you lean back on your chair and squint your eyes, you'll hopefully see a uniform flow of code where methods flow harmoniously from top to bottom and methods are short, sweet, and pointy. This simple visual test guarantees that each method does one thing and one thing only.

Enough with the fancy acronyms and let's dive straight into the code. In the following 4 sub-sections, I will cherry-pick pieces of code to demonstrate the best practices I follow.

Controllers

Controllers are the glue between models and views in the MVC architecture pattern. Knowing where a controller's responsibility starts and where it ends is an essential part to building DRY controllers.

Let's look at the TicketsController index action.

def index
  @tickets = Ticket.search(params[:search], params[:page])
  authorize @tickets
end

It might seem like a regular index action; however, if you look closely you'll notice the Ticket.search method which is not an ActiveRecord api method provided by Rails.

The search class method is a method I implemented in the Ticket model class.

def self.search(search, page)
  if search
    where('content LIKE ?', "%#{search}%").order('created_at DESC').page(page)
  else
    order('created_at DESC').page(page)
  end
end

To make my point clear, consider the original implementation of the index action here.

def index
  if params[:search]
    @tickets = Ticket.where('content LIKE ?', "%#{params[:search]}%").order('created_at DESC').page(page)
  else
    @tickets = Ticket.All.order('created_at DESC').page(page)
  end
  authorize @tickets
end

Calling an ActiveRecord query; such as, Ticket.where is not part of the controller's responsibility. For this reason, I have extracted the search functionality and implemented it in its proper place, the Ticket model class, where all data related responsibilities should live. By implementing this simple refactoring we now have a drier TicketsController.

Models

We all love how Rails makes our lives as developers easier. Rails is well known for its helper methods that are shipped with Rails and are available out of the box. Consider the following example.

class Post < ActiveRecord::Base
  has_many :comments, dependent: :destroy
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

Once we inherit from ActiveRecord::Base we now have a plethora of methods available for us to use to our hearts content. Some of these methods are association methods; such as, has_many and belongs_to which are demonstrated in the code snippet above. Using these association methods tells Rails what kind of relationship(s) objects of these classes have. As you can see has_many takes an optional hash parameter { dependent: :destroy }. This tells Rails to make sure to destroy all Comment objects that belong to the owining Post object if the Post object is destroyed.

This is a very convenient option to have. However, this is not the case with has_and_belongs_to_many aka habtm association method which we use in our CSTS app to associate User and Ticket model objects.

class User < ApplicationRecord
  has_and_belongs_to_many :tickets
end

class Ticket < ApplicationRecord
  has_and_belongs_to_many :users
end

According to Rails API Doc habtm doesn't support the dependent: :destroy option. This means that if we destroy a User object that has_and_belongs_to_many: tickets we might end up having many dangling tickets that have no reference to any users in our database. This will overtime accumelate and result in many unreferenced and unecessary tickets that the system keeps maintaining. Moreover, this will eventually lead to performance issues as our database grows in size.

One way to solve this problem is not to use habtm at all, but instead use the has_many :through association. This will allow us to use dependent: :destroy Another way to solve this issue is to use another convenience method that Rails provides, namely the before_destroy hook. Take a look at the User model now.

class User < ApplicationRecord
  has_and_belongs_to_many :tickets

  before_destroy do
    tickets.each { |ticket| ticket.destroy }
  end
end

This ensures that whenever we destroy a User we make sure we destroy all associated Tickets as well, just like our good old friend dependent: :destroy. So instead of fighting the framework, we use another helper method that Rails provides.

Views

ERB views allow developers to embed Ruby code directly inside HTML templates. Rails provides all kinds of view helper methods to simplify writing views. For example, link_to and image_tag are one-liner helpers to add links and images to your erb views.

<%= link_to "Tickets", tickets_path %>
<%= image_tag "moeabdol.jpg", alt: "moeabdol" %>

A lot of Rails developers out there confuse erb templates for places where they can write logic. Consider the following example where we try to determine a css class dynamically inside an erb view.

<% if ticket.status.eql?("resolved") %>
  <tr class="success">
    <td>
      <%= ticket.content %>
    </td>
  </tr>
<% else %>
  <tr class="danger">
    <td>
      <%= ticket.content %>
    </td>
  </tr>
<% end %>

Although this is completely valid, it's considered a bad approach to write code directly inside views. Views are responsible mainly for the presentation of our pages, hence the name "Views". Views should be dumb, and we should always try to minimize the amount of logic we write in them. The reason for that being is that if we keep doing so, we will end up having a lot of business logic living inside view templates which defies the purpose of the MVC architecture. Another reason is that it is now harder to test this dynamic css class functionality.

Following on Rails's "Convention Over Configuration" phillosiphy, we should extract this functionality into a view helper just like how Rails implements link_to and image_tag. Consider this final implementation of the Tickets index view.

<tr class="<%= ticket_status_class(ticket) %>">
  <td>
    <%= ticket.content %>
  </td>
</tr>

We have extracted the dynamic css class logic from our view, and instead implemented a view helper method ticket_status_class.

def ticket_status_class(ticket)
  ticket.status.eql?("resolved") ? "success" : "danger"
end

Now the view has to know nothing about how this logic works, and all its concerned about is presenting the data. An added benefit to doing so is that our view now is drier, and we can easily test this functionality. Also, we can now share this logic with other views who want to present tickets according to the same logic.

Policies

Securing a web application is a common problem web developers have to solve. Rails already does a great job enforcing security defaults in newly created projects. Things like Cross-Site-Request-Forgery are handlerd automatically by Rails.

While Rails sets these defaults for you, it leaves you the freedom of deciding who uses your application (Authentication), and what they are allowed to do (Authorization). The Devise gem tries to answer the question of who is allowed into your application, and the Pundit gem answer the what is the user allowed to do.

There are many techniques and/or 3rd party gems out there that implement Rails authorization. Pundit however, is in my personal opinion the best authorization gem out there. It totally isolates the authorization problem from the rest of the code, and adds another layer to the MVC architecture, the policy layer.

Policy Directory

It uses pure Ruby classes to define policies. As you might remember, in CSTS only users with agent role are allowed to resolve tickets. Let's take a look at the TicketPolicy resolve? policy.

def resolve?
  user.agent? and ticket.status == 'unresolved'
end

The resolve? policy only returns true if the currently logged in user is an agent and the ticket is still unresolved.

This example might not demonstrate best practices and techniques in writing Ruby code; however, I thought it might be good to mention this early on, as we are going to revisit policies again in the Policy Tests section.

Tests

Testing Rails application has never been so much fun until the introduction of RSpec testing framework. RSpec is a Behaviour Driven Testing framework that has a human-like Domain Specific Language (DSL). It almost reads like english, and is very easy to understand even by non-developers. Although Rails ships with its own testing framwok MiniTest, I find RSpec to be much more expressive and intuitive.

Testing has always been a best practice that I follow and preach. I have proposed test driven development to some of my previous employers, and slowly but surely they grew to understand its purpose and need. In the following 4 sub-sections, we will look at how I TDD/BDD my Rails applications end-to-end.

Controller Tests

To understand how to test controllers properly, we have to understand its role in Rails MVC architecture. When a request is made by a user, Rails router will route the request to the proper controller. The controller then investigates the request's REST verb (GET, POST, PUT, PATCH, DELETE). Along with the RESTful verb the controller also looks at the request endpoint to identify which action it should envoke.

Take for example a GET request to https://csts.com/tickets. The TicketsController now identify that it's a GET request to the tickets index action.

def index
  @tickets = Ticket.search(params[:search], params[:page])
  authorize @tickets
end

The index action assigns @tickets instance variable that is used in the associated app/views/tickets/index.html.erb view.

To fully test this controller's action, we simulate a GET request to the tickets#index action. We then test that the request is successful. Then we confirm that @tickets variable is assigned. And finally, we make sure that the index.html.erb template is rendered. You can take a look at the entire spec suite here.

RSpec.describe TicketsController, type: :controller do
  describe "GET #index" do
    before { get :index }

    it "returns http success" do
      expect(response).to have_http_status(:success)
    end

    it 'assigns @tickets' do
      expect(assigns(:tickets)).to eq([])
    end

    it 'renders index template' do
      expect(response).to render_template(:index)
    end
  end
end

This is a straight-forward example of how to test conrollers. Let's take a look at another slightly more complicated action, namely the resolve action in TicketsController.

def resolve
  @ticket.update(status: 'resolved')
  @ticket.users << current_user
  redirect_to @ticket
end

This is a custom action that I have created for agents to resolve tickets. The action updates the status of the Ticket from unresolved to resolved. The resolve action also makes sure that the agent resolving this ticket is now a User of that ticket. Then we finally redirect to the ticket's show view. Lets take a look at the route configured for this action.

resources :tickets do
  member do
    get 'resolve'
  end
end

The route is a simple GET request hitting the resolve action. Let's now take a look at the test.

describe 'GET #resolve' do
  let(:ticket) { create(:ticket, status: 'unresolved') }

  before(:example) do
    agent = create(:user, role: 'agent')
    sign_in(agent)
    get :resolve, params: { id: ticket }
  end

  it 'returns http redirect' do
    expect(response).to have_http_status(:redirect)
  end

  it 'assigns @ticket' do
    expect(assigns(:ticket)).to eq(ticket)
  end

  it 'redirects to show page' do
    expect(response).to redirect_to(ticket)
  end
end

We first use FactoryGirl to create a fake ticket in the test database with an unresolved status. We then create a fake user with an agent role and sign_in as this new agent. Next we simulate a GET request to the resolve action. The first spec confirms that the response is a redirect or a HTTP 303 since the resolve action is redirecting to the ticket show page. The second spec confirms that @ticket variable was assigned to the fake ticket we just created. Finally, we make sure that the application redirects to the ticket show page.

Model Tests

Remember from earlier our talk on how controllers shouldn't be responsible for data relevant tasks. And how we refactored out TicketsController#index action and moved the search code to the Ticket model. Let's take a look at the search class method again.

def self.search(search, page)
  if search
    where('content LIKE ?', "%#{search}%").order('created_at DESC').page(page)
  else
    order('created_at DESC').page(page)
  end
end

The search method first determines if a search keyword was passed. If that was the case, it will then run a where query and grab all records that contain the search keyword. Now let's take a look at the spec.

RSpec.describe Ticket, type: :model do
  context 'search' do
    let(:ticket1) { create(:ticket, content: 'first ticket.') }
    let(:ticket2) { create(:ticket, content: 'second ticket.') }

    it 'should return all matching records' do
      expect(Ticket.search('ticket', 1)).to match([ticket1, ticket2])
    end

    it 'should return specific matching record' do
      expect(Ticket.search('first', 1)).to match([ticket1])
      expect(Ticket.search('second', 1)).to match([ticket2])
    end
  end
end

The spec is pretty straight-forward. We create 2 fake tickets containing content that we will use as search keywords. Our first spec searches for the 'ticket' keyword which is common to both tickets. In result, both records are retrieved. Our second spec has two expectations to fullfil. We search for specific keywords 'first' and 'second'. The first expectation makes sure that we only retrieve the first record while the second expectation makes sure we retrieve the second. With that we have fully TDD'd our search method.

Integration Tests

There are two schools of thought when it comes to testing views in Rails. Some developers insist on spec'ing out their views leaving no stone unturned. Others say that testing views is unecessary since views don't necessarly contain business logic and are subject to frequent redesigns all the time.

I tend to agree with the second group, but with a minor additon. It might be futile to test how pages look, or how HTML elements are structured, but it's absolutely necessary to test how users interact with the front-facing application. This is where integration tests fall into play.

Simulating user interactions and scenarios are important on so many levels. First and foremost, it confirms that the entire system is tested end-to-end ensuring that all components are communicating properly and harmoniously from front to back and vice versa. Secondly, integration tests are a proof of work that can be shown to higher management proving that certain requirements and user stories are fullfiled.

Let's pick a user story and see how RSpec and Capybara allow us to write integration tests that are easy to read and follow. As we mentioned earlier in this document, an agent should be able to search for tickets using a search keyword. Let's look at how this search tickets feature is spec'd.

feature 'search tickets' do
  let!(:ticket1) { create(:ticket, content: 'first ticket.') }
  let!(:ticket2) { create(:ticket, content: 'second ticket.') }

  context 'agent' do
    before(:example) do
      signin(create(:agent))
      visit tickets_path
    end

    scenario 'can find all tickets' do
      fill_in 'search-box', with: 'ticket'
      click_on 'search-button'
      expect(page).to have_selector('td', text: 'first ticket.')
      expect(page).to have_selector('td', text: 'second ticket.')
    end

    scenario 'can find specific ticket' do
      fill_in 'search-box', with: 'first'
      click_on 'search-button'
      expect(page).to have_selector('td', text: 'first ticket.')
      expect(page).not_to have_selector('td', text: 'second ticket.')
    end
  end
end

We first create 2 fake tickets containing content that we'll search for. Then we log into the system as an Agent user. We then visit the tickets index page where the search box is available. In the first scenario we fill_in the search-box with the ticket keyword which is common to both records we created earlier. Now we click_on the search-button to initiate a search, and we expect the page to have two table rows containing two table data element with the first and the second tickets respectively.

In the second scenario, we login again as an Agent and revisit the tickets_path fill_in the search-box with the keyword first and click_on the search-button. Now this time, the page should only contain the first ticket and NOT the second.

Please note that I usually use Cucumber as part of my testing arsenal, but since CSTS was intended to be a quick effort I left it out. Just for this time!

Policy Tests

The reason I dedicated this section to Pundit policy tests, is to showcase how awesome Pundit's testing DSL is. Consider the same example we mentioned earlier in the Policies section where users are allowed to resolve tickets only and only if they are Agents and the ticket is unresolved.

def resolve?
  user.agent? and ticket.status == 'unresolved'
end

Let's now see how we test this TicketPolicy.

describe TicketPolicy do
  subject { described_class }

  context 'agent' do
    let(:agent) { create(:agent) }
    let(:ticket) { create(:ticket, status: 'unresolved') }

    permissions :resolve? do
      it 'grants access if ticket is unresolved' do
        expect(subject).to permit(agent, ticket)
      end

      it 'denies access if ticket is resolved' do
        resolved_ticket = create(:ticket, status: 'resolved')
        expect(subject).not_to permit(agent, resolved_ticket)
      end
    end
  end
end

In the context of an agent, we see that resolve? action permissions has 2 scenarios. First when the ticket is unresolved the agent is permitted to access the resolve action.

In the second scenario, the agent is not permitted to resolve the ticket since the ticket is already resolved.

Let's also look at another context for the same resolve policy. This time with a mere Customer.

describe TicketPolicy do
  subject { described_class }

  context 'customer' do
    let(:customer) { create(:customer) }
    let(:ticket) { create(:ticket) }

    permissions :resolve? do
      it 'denies access' do
        expect(subject).not_to permit(customer, ticket)
      end
    end
  end
end

This time the system denies the customer regardless of ticket status, and this spec ensures this behavior.

Debugging

Going through the test development cycle of Red-Green-Refactor eliminates any need for debugging. However, every once in a while a nasty bug slips through the cracks. In such situations I always come ready with my suite of debugging tools. In Ruby and Rails applications I always use Pry as an alternative to irb. Pry is an excellent tool for quickly trying out pieces of code to proof-concept an idea. It also provides syntax coloring that makes code much more pleasant to read in the terminal.

In Rails applications I use pry-rails, so whenever I type rails console in the terminal I drop into a pry shell. I also use pry-byebug to investigate code whenever things go wrong. Usually I just write binding.pry in my code as a breakpoint and then step into the code and investigate variables and execution flow.

def index
  binding.pry
  @tickets = Ticket.search(params[:search], params[:page])
  authorize @tickets
end

Once the application breaks at the breakpoint where you insterted binding.pry you can then use n, c, s keys to next, continue,or step-in the code. Along with Pry I also use better_errors gem to show better and more meaningful error pages in Rails.

better_errors gem

I also use simple puts statements to investigate my variables. This might sound like a beginner's approach to debugging, but you'll be amazed to know that some of the best developers out there like Martin Fowler use this classic technique as their first approach to debugging.

I rarely find myself in a situation where I don't really know what's going on with my code. Thanks to TDD for making it hard to introduce logical errors. Syntactic errors on the other hand, are easily and quickly caught by my editor's parser and lint tools.

I also use rubocop to enforce some coding standards and styles. Whenever I deviate from Ruby's best coding styles like for example not double indenting method that are private rubocop will generate a warning reminding me that methods under private or protected should be double indented.

Double Indent Under Private

Continous Integration

Github integrates well with Travis-CI; therefore, building and testing my code on Travis was as easy as including a simple .travis.yml file in my project directory.

language:
  - ruby
rvm:
  - 2.4.0
services:
  - mysql
before_script:
  - export DISPLAY=:99.0
  - sh -e /etc/init.d/xvfb start
before_install:
  - cp config/database.travis.yml config/database.yml
  - . $HOME/.nvm/nvm.sh
  - nvm install stable
  - nvm use stable
  - npm install
  - npm install -g webpack yarn
  - yarn add bootstrap jquery
script:
  - export RAILS_ENV=test
  - bundle exec rails db:create db:migrate
  - bundle exec rails db:test:prepare
  - bundle exec rspec spec
addons:
  code_climate:
    repo_token: secret_tocken:)

There is nothing extraordinary in this file. The file contains instructions for Travis on how to prepare the build server making sure all dependencies are there. Then running bundle to install application gems, and prepare the database, and run the specs.

Everytime I push a new change to my Github repository, a new build is initiated automatically this time with the newly added code. This guarantees a workflow where bugs and errors are caught as early as possible, especially when many developers are working on the same project. Although this might seem like an overkill for CSTS as I was the only developer working on it, but I always like to follow best practices.

You can check my build log here.

Travis-CI

Continous Delivery

I have been a fan of Docker since its early days. The idea of containarizing my app into a unit of artifact that can be shipped and distribured easily is an awesome feat. With docker it's easy to deploy thousands of application instances to the cloud with one simple command.

Although CSTS is a simple demo app, I decided to containarize it using docker just in case I needed to showcase my knowledge of docker. In my project directory you can find two Dockerfiles. The first one to dockerize my app and the second to dockerize the latest image of NGINX. Following is the application Dockerfile.

# Base image:
FROM ruby:2.4.1

# Install dependencies
RUN apt-get update -yqq && apt-get --no-install-recommends install -yqq build-essential libpq-dev nodejs

# Set an environment variable where the Rails app is installed to inside of Docker image:
ENV RAILS_ROOT=/var/www/customer_support_portal
ENV RAILS_ENV=production
RUN mkdir -p $RAILS_ROOT

# Set working directory, where the commands will be ran:
WORKDIR $RAILS_ROOT

# Gems:
COPY Gemfile Gemfile
COPY Gemfile.lock Gemfile.lock
RUN gem install bundler
RUN bundle check || bundle install --without development test -j4

COPY config/puma.rb config/puma.rb

# Copy the main application.
COPY . .

RUN exec rails assets:precompile RAILS_ENV=production
RUN exec rails db:create
RUN exec rails db:migrate

EXPOSE 3000

# The default command that gets run will be to start the Puma server.
CMD bundle exec puma -C config/puma.rb

The file is pretty much self-explainatory. We first download the Ruby 2.4.1 image. Then we install application dependenceis; such as, NodeJS. We copy the Gemfile and run bundle to install application gems. We copy puma's config file then precompile assets for production. We also create and migrate the database, and finally expose port 3000 and start the application server puma.

The second Dockerfile-nginx creates another container for NGINX.

# Base image:
FROM nginx:latest

# Install dependencies
RUN apt-get update -yqq && apt-get -yqq install apache2-utils

# establish where Nginx should look for files
ENV RAILS_ROOT /var/www/customer_support_portal

# Set our working directory inside the image
WORKDIR $RAILS_ROOT

# create log directory
RUN mkdir log

# copy over static assets
COPY public public/

# Copy Nginx config template
COPY config/nginx.conf /tmp/customer_support_portal.nginx

# substitute variable references in the Nginx config template for real values from the environment
# put the final config in its place
RUN envsubst '$RAILS_ROOT' < /tmp/customer_support_portal.nginx > /etc/nginx/conf.d/default.conf
#RUN rm -rf /etc/nginx/sites-available/default
#ADD config/nginx.conf /etc/nginx/sites-enabled/nginx.conf

EXPOSE 80

# Use the "exec" form of CMD so Nginx shuts down gracefully on SIGTERM (i.e. `docker stop`)
CMD [ "nginx", "-g", "daemon off;" ]

We download the latest NGINX image, install some dependencies, and tell NGINX where it should serve files from. We copy NGINX's config file to its proper place and expose port 80 then run NGINX in daemon mode.

Finally let's look at docker-compose.yml file which is used by the docker-compose tool to build both images and link them.

version: '2'

services:
  app:
    build: .
    command: bundle exec puma -C config/puma.rb
    volumes:
      - /var/www/customer_support_portal
    expose:
      - "3000"

  web:
    build:
      context: .
      dockerfile: Dockerfile-nginx
    links:
      - app
    ports:
      - "80:80"

When we run docker-compose up the build process will start, and we'll end up with 2 images csts_app and csts_web that I then push to my docker registery. Finally, I log into my personal VPS and pull these images and run them. This entire process can be automated so that whenever I push code to Github it will run Travis-CI and once the build is successful docker images will be built and pushed to the server. However, I haven't automated this process but it's easy to do.

Using docker can help implement a true micro-services platform, where we can separate the moving parts (code) from data (database) from static assets (js and css) and disect the system into distributable parts that communicate together. In my simple app it is easy to scale csts to fit in a 1000 server architecture.

VPS

This is how my application sits in my VPS now. If we move the MySQL database to a separate server, then we can deploy our application on many servers and make them all point to the same database instance. We can take this one step further by securing our app instances in a private cloud and having a dedicated public NGINX server running in a DMZ acting as a load-balancer.

The possiblities of different architecture schemata are endless. Docker gives us this ability of separating concerns into containers.

Final Notes

I hope this gist gives a glimpse of my coding, testing, and devops skills. I truely love what I do, and I'm hungry to learn and grow.

I already have a running dockerized version of CSTS on one of my VPSs here. Feel free to play with the app. You can login using the following credintials. Thanks :)

email: ********
password: ********
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment