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.
- About CSTS
- Dependencies
- Refactoring
- Tests
- Debugging
- Continous Integration
- Continous Delivery
- Final Notes
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.
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.
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 | Fast, nimble PDF writer for Ruby. | |
prawn-table | 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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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!
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.
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.
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.
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.
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 Dockerfile
s. 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.
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.
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: ********