Using Docker and Vagrant on Mac OS X with a Ruby on Rails application

Mon, Nov 11, 2013 4-minute read

I have been following the development of Docker for the past few months. Docker is an open-source project to easily create lightweight, portable, self-sufficient containers from any application. A few great tutorials on how to use it already came up, so I decided to give it a try for a side project and share my experiences. All the files discussed in this post can be found at this GitHub repository

The goals:

  1. Separate the application into individual containers that communicate with one another. Just like functions and classes in your code, containers should do one thing and do that well. In my case I have the following containers:

    • PostgreSQL container

    • Redis container

    • Rails container, featuring RVM, Ruby 2.0.0-p247, Nginx and Passenger

  2. Separate the application code from the containers. I wanted to be able to develop my project on OS X (my Vim / Tmux setup is currently OS X dependant and I don’t want to mirror it on Ubuntu).

I had to choose between using VirtualBox or VMware Fusion for virtualization, since at the moment Docker doesn’t support OS X. At first I tried VMware Fusion, as I couldn’t get Vagrant to work with OS X. I wanted to host my code on OS X, mount it with VMware Fusion on a Ubuntu guest, and use it inside a Docker container. That went well, but I had troubles with the shared folder syncing between OS X and Ubuntu. On occasions I would edit a file on OS X, and the change wouldn’t sync with the Ubuntu guest system. It turned out other people also had similar issues with VMware Fusion, so I decided to switch back to Vagrant with VirtualBox provider.

For me Vagrant wouldn’t boot up at all initially. I was constantly getting the same error message - “VM failed to remain in the running state”. The fix turned out to be adding a static DNS (8.8.8.8 / 8.8.4.4) to my OS X network adapter.

Having done that I had a stable Vagrant/Ubuntu/Precise virtual machine.

With a little help from the folks at RelateIQ, I added a provisioning script, which automatically installs docker and the required kernel on the VM. At that point I had a running Ubuntu VM using Vagrant, with Docker installed on it.

Next I prepared Dockerfiles for the PostgreSQL and Redis containers.

The final step was to setup the Rails container, linking it to PostgreSQL and Redis. As of Docker 0.6.5 one can link containers together, allowing them to communicate with each other. This basically creates a secure tunnel from the parent to the linked container for a given port and exports the IP and port as environmental variables.

# Dockerfile - Rails container

FROM ubuntu:precise

RUN apt-get -y install build-essential openssl libreadline6 libreadline6-dev curl git-core zlib1g zlib1g-dev libssl-dev libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt-dev autoconf libc6-dev ncurses-dev automake libtool bison subversion pkg-config libpq5 libpq-dev build-essential git-core curl libcurl4-gnutls-dev python-software-properties libffi-dev libgdbm-dev vim

RUN curl -L https://get.rvm.io | bash -s stable

RUN echo 'source /usr/local/rvm/scripts/rvm' >> /etc/bash.bashrc

ENV PATH /usr/local/rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/rvm/gems/ruby-2.0.0-p247/bin:/opt/nginx/sbin

RUN /bin/bash -l -c 'rvm install 2.0.0-p247'
CMD /bin/bash -l -c 'rvm use 2.0.0-p247 --default'

RUN gem install passenger
RUN passenger-install-nginx-module --auto-download --auto --prefix=/opt/nginx

RUN mkdir -p /var/log/nginx

RUN gem install bundler --pre

ADD nginx.conf /opt/nginx/conf/nginx.conf

ENV PATH /usr/local/rvm/gems/ruby-2.0.0-p247@global/bin:/usr/local/rvm/rubies/ruby-2.0.0-p247/bin:/usr/local/rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/nginx/sbin

CMD /bin/bash

In order to link the PostgreSQL and Redis containers with the Rails container, I execute the following:

PGSQL=$(sudo docker run -p 5432:5432 -d -name pgsql project/pgsql /usr/bin/start_pgsql.sh YOURPASSWORD)
REDIS=$(sudo docker run -p 6379:6379 -d -name redis project/redis)

sudo docker run -i -privileged -p 80:80 -name rails -v /vagrant/rails/src:/var/www/project -link pgsql:pgsql -link redis:redis -t project/rails /bin/bash

At this point I prefer to launch the Rails container with /bin/bash and manually do bundle install and foreman start -e .env.development to launch my application. Since I don’t want to install all the Gemfile dependencies when I launch the container, once it is finished, I commit a new version for future use. If a code change requires a new Gem, I run bundle update manually and re-commit the container.

Next, I would like to automate this setup a bit further resulting into a setup similar to RelateIQ’s development environment. Additionally, when starting the containers, data and log volumes need to be mounted, so that data can be persisted if a container is rebuilt/destroyed.

Comments and ideas on how to improve this setup are very welcome.