September 29, 2015
One of our designers wanted to make some changes to our job offer page. At first it seemed trivial - just change some HTML inside a rails app. Then we realised that we would need to setup the whole development environment on his Mac. With the help of homebrew this didn't sound like a challenge at all. But then it came to my mind that this might be the perfect case for setting up a development environment using docker and docker-compose (formerly fig).
Docker does not have native Mac OS X support yet, but there are already tools that allow running docker inside a virtual machine with the feeling of having it installed locally. After struggling with boot2docker, kitematic and vagrant I finally found a setup that just works using docker-machine.
This setup is using homebrew and Virtual Box, but you can install docker binaries in any way you like, as well as use VMWare instead.
This may sound obvious, but I've spent enough time fighting with older versions of docker and docker-compose that it's worth mentioning here. Simply run
docker-compose (fig) is a simple utility for running a set of containers defined in a YAML file. It removes the pain of managing separate containers for app, database and supporting services and lets you focus on your app development. Head over to the great documentation for more details. We will use it to define our app dependencies as well as the rails app container itself.
brew install docker-compose
docker-machine allows for quick provisioning of docker-ready nodes, from local virtual machines (Virtual Box, VMWare) to cloud servers (EC2, DigitalOcean, Rackspace and more). This is a single point of management for multiple local or remote environments from one terminal.
brew install docker-machine
After installing all docker packages you should be able to access the following commands. Please pay attention to the version numbers, there is a significant chance that earlier versions of those tools won't work.
λ docker --version Docker version 1.6.0, build 4749651 λ docker-compose --version docker-compose 1.2.0 λ docker-machine --version docker-machine version 0.2.0 (HEAD)
Since we want to create a local development environment, we will use Virtual Box. You can download the installed package from Virtual Box Downloads page
docker-machine virtual machine drivers are based on boot2docker. Invoking the following command will setup a new Virtual Box VM using boot2docker image.
docker-machine create -d virtualbox docker-vm
You can check the status of your new vm using:
λ docker-machine ls NAME ACTIVE DRIVER STATE URL SWARM docker-vm * virtualbox Running tcp://192.168.99.100:2376
As you can see above, our new machine has an IP address
192.168.99.100. This address is required to tell the docker command line tool where the docker daemon is running. Fortunately, we don't need to remember it.
docker-machine comes with a handy command called
λ docker-machine env docker-vm export DOCKER_TLS_VERIFY=1 export DOCKER_CERT_PATH="/Users/teamon/.docker/machine/machines/docker-vm" export DOCKER_HOST=tcp://192.168.99.100:2376
ENV variables tell docker CLI the location of the desired host you want to operate on. In order to make your life easier, add this line to your
.bashrc file to have the environment setup correctly in every new terminal:
# .zshrc/.bashrc eval "$(docker-machine env docker-vm)"
While this step is not necessary, it might be easier to use an easy-to-remember name instead of an IP address. Edit your
/etc/hosts file and add this line at the very bottom:
NOTE: Your IP address might be different. You can always check it using
docker-machine ip command
And it's done — now you have a fully functional docker environment running on OS X!
Now to the most exciting part.
You MUST put your application directory under the
/Users directory. If you don't, the file sharing between OS X file system and Virtual Box VM will not work. Symlinking won't work either.
docker-compose requires at least two files to be added to your app's directory:
The minimal content of your
Dockerfile should contain something like this:
FROM ruby:2.2.0 RUN apt-get update -qq && apt-get install -y build-essential nodejs RUN mkdir /app WORKDIR /tmp COPY Gemfile Gemfile COPY Gemfile.lock Gemfile.lock RUN bundle install WORKDIR /app CMD ["rails", "server", "-b", "0.0.0.0"]
Our app requires two companion services: mysql and redis. We can easily define those using a friendly YAML syntax:
web: build: . volumes: - .:/app ports: - "3000:3000" links: - db - redis db: image: library/mysql:5.6.22 environment: MYSQL_ROOT_PASSWORD: password # required by mysql image redis: image: redis
We also need to configure the
config/database.yml file to point to our mysql container which is accessible under
development: adapter: mysql2 encoding: utf8 host: db # <--- database: app_development username: root password: password # <--- same as MYSQL_ROOT_PASSWORD above
Connecting to redis also needs to be tweaked a little - now the redis URL will be
redis://redis:6379/0 (using the
Now we can build our app and start everything together with a single command:
You should see output similar to foreman's, with three differently colored log files. And if everything went fine, you should be able to see your app at http://docker:3000 (or http://192.168.99.100:3000 if you haven't edited
It might be useful to have a separate
database.docker.yml file and a
bin/docker-setup script to set everything up automatically after a fresh cloning of the repository.
# bin/docker-setup #!/bin/bash set -e cp -f config/database.docker.yml config/database.yml cp -f config/application.docker.yml config/application.yml # build app image docker-compose build # start mysql and redis in background docker-compose start db docker-compose start redis # setup database docker-compose run web rake db:create db:migrate # ensure all containers are up and running docker-compose up
I can't think of a better summary than this quote from one of the Head of our designers, Paweł:
Design can seldom be validated without actually looking at what you made. And when what you made is code, it's getting harder by the minute. One can deploy after every little change which is counter productive or wait until those changes accumulate which isn't fast. Docker helps with that. I'm happy now.