Ruby on Rails Development Life-cycle on Docker Containers

Before the containers have been integrated with the applications, the RoR application’s deployments are managed either manually or maintaining more handy tool such as Capistrano. Either way, there are a couple of required procedures needs to be applied on every new change set of the source code wanted to be deployed as a version.

Administration point of view, RoR applications are threated as file based applications, similar to PHP.  Unlike Java or Go, there is not one binary/archived deployable artifact. Therefore, every single change set contains several files and directories which leads the deployment ( directly or indirectly ) a process between SCM and the destination servers. Capistrano handles this quite well, especially In case of any deployment error, there is this automated rollback capability which tries to keep all the target nodes in the cluster on the same version.

A typical deployment procedure includes procedures as follows :

    • Source code delivery from the private SCM to the target servers (pulling operation)
    • Dependency management for the libraries defined in the Gemfile.
    • Applying necessary RAKE instruction for database operations ( rake db:seed or similiar )
    • Finally, reloading the web server in order to serve the new version.

Therefore the dependency management is handled on the target server, right after the deployment. The RoR applications are modular in development perspective; building the logic of application’s required dependencies ( gems ) by defining them in a file called Gemfile. Developers keep the Gemfile in the root of the repository in SCM. After the dependencies are defined, one needs to run bundle install command by targeting the Gemfile to download all the dependencies from central dependency servers.

Huge GEM_HOMEs Complicates Portability

Modularity comes at a cost. Dependencies in size could be huge in size which causes the obstacle when it comes to the portability of a container. A regular project could contain more than 400 MB dependencies in size ! Therefore the Docker integration should be soft enough for developers to manage and feasible for operations to handle big dependency files.

The solution provided in this article is to use a Dockerfile which has an environmental configuration which sets the GEM_HOME variable at a custom location. This is the location where the defined gems are stored in the environment. If there is a gem defined in the Gemfile which already exists in this path means no need to go & fetch again via network. Without this dependency caching mechanism, It takes minutes to fetch the dependencies comparing to deploying and running the application in seconds. This leads to the conclusion that the dependency caching is a necessary improvement.

One way is to store all the dependencies in the image itself and run the Nginx to get server up. However (as I have mentioned earlier), in this way, the Docker image becomes too big for shipment. The other option is to use a docker volume which gives high level abstraction of data management inside containers.

docker create -v /ruby_gems/2.2.1 –name dummyrorapp_gems busybox

Creating the volume as above, can be achieved via a lightweight image like busybox since this will be a passive instance. Then, this instance needs to be provided to the applications container as follows :

docker run -d –name=${APP_NAME} –volumes-from ${APP_NAME}_gems –link db:db –memory 1G –memory-swap 2G –memory-swappiness=0 –restart=unless-stopped ${REGISTRY}/${APPNAME}:latest

Actually, this is setup can be found a few places on the web provided as a solution, like this on healthcareblocks. However, making the volume optional with no additional configuration is the tricky. Withing this solution, the instance will run no matter what; If the volume provided (even if the volume is empty) the gems will be downloaded and stored within the volume, otherwise the gems are stored in the application container.

The rake instructions before the nginx command is for database operations like seed and migration. The worker process manager for ruby code in the backend is chosen as passenger. The fully functional Dockerfile stated as follows :

FROM debian:wheezy
ADD /etc/apt/sources.list

RUN apt-get update \
    && apt-get upgrade -y \
    && apt-get install gnupg build-essential curl procps apt-transport-https ca-certificates libcurl4-openssl-dev libmysqlclient-dev xvfb imagemagick git -y

RUN echo "deb wheezy main" >> /etc/apt/sources.list \
  && curl -sSL | gpg --import -                                                   \
  && apt-key adv --keyserver hkp:// --recv-keys 561F9B9CAC40B2F7                     \
  && curl -sSL | bash -s stable                                                      

ENV PATH $PATH:/usr/local/rvm/bin
RUN rvm install ruby-2.2.1

ENV PATH /usr/local/rvm/gems/ruby-2.2.1/bin:/usr/local/rvm/gems/ruby-2.2.1@global/bin:/usr/local/rvm/rubies/ruby-2.2.1/bin:$PATH

# Setup Passenger + Node + Nginx Couple
RUN curl --fail -ssL -o setup-nodejs && bash setup-nodejs            \      
  && apt-get update                                                                                        \
  && apt-get install -y nginx-extras passenger nodejs                                                       

ENV GEM_HOME /ruby_gems/2.2.1
ENV GEM_PATH $GEM_HOME:/usr/local/rvm/gems/ruby-2.2.1@global
ENV MY_RUBY_HOME /ruby_gems/2.2.1



ENV APP_HOME /opt/app/ruby/
RUN mkdir -p $APP_HOME/tmp && chmod 777 -R $APP_HOME/tmp $APP_HOME/config/*yml
CMD mkdir -p $GEM_HOME                  \
  && gem install bundler                \
  && bundle install --jobs 4 --retry 3  \
 && nginx