In this guide we run through how to install Docker, set everything up for easy configuration in the future, and run Ruby on Rails web application smoothly. Includes all code snippets required!

Remember Docker? The Quick and Easy Alternative to Virtualization for Developers and Non-Developers Alike we covered in our previous blog post? For those unfamiliar with the subject, Docker is a containerization management tool that can help with isolated development and deployment of software on any machine with any configuration, offering a clever, resource efficient alternative to spinning up traditional virtual machines. Docker allows for portable software development that can benefit all project stakeholders, including developers, managers, devops, and even customers, since Docker ensures all containers have the same configuration, independently from the host environment they are run on.

In this week’s update, we put our knowledge into practice by showing you in this Docker tutorial - creating isolated development environments, learning to setup Docker from the very beginning with a fresh Docker install, through to your first Docker run with your web application.

Installing Docker on Mac

Did you know that Docker was created with Linux namespaces in mind - that it even used LXC in its early days? What this means is that won’t run natively on OS X or Windows.

Docker for Mac (official version)

Even though there are Docker for Mac and Docker for Windows official versions, these both run Linux Alpine in a Virtual Machine (VM) under the hood, which effectively works as a Docker host VM, and they aren’t ideal for the following reasons:

  • Slow file sharing from host to container - the built in mechanism for file sharing is slower than the default Virtual Box solution, however Docker developers are working very hard to improve this situation (this GitHub issue is the best place to keep up to date on the situation)
  • The VM disk image has memory issues resulting in it taking up way too much space - by default it uses 50GB as a sparse image. For every file created, it eats up disk space - meaning, for example, if you keep creating and deleting Docker images of 2GB, it will take up 2GB, then 4GB, then 6GB… up to 50GB. Even if you remove the Docker image, it won’t give the space back to the host - plus if you reset the disk image to free up space you’ll lose your Docker images and volumes! You can check the GitHub issue here.

docker-machine with xhyve

An alternative to the official Docker for Mac solution which partially combats these issues is using docker-machine with VirtualBox (or possibly Parallels, VMware or xhyve) to install Docker. nfs file sharing is easily fixed here & there’s a disk image size trick to use with VirtualBox. We have used docker-machine with the xhyve driver in the past, however we had problems with upgrades (from time to time docker-machine didn’t want to boot for unknown reason) and file permission issues (we had to create a user with id 501 and group with id 21 to match the Mac user; the issue is that, in Linux, a group with id 21 is already reserved for dialout group to allow access to the serial ports via files in /dev).

Docker for Mac with nfs file sharing

For this reason, we switched to Docker for Mac with nfs file sharing. This handy install gives the same performance as the xhyve driver configuration. Unfortunately, this required us to simply accept the known VM disk image space issue mentioned earlier, even though our file sharing issue is addressed.

3 tips for choosing which Mac installation is best for you:

  • If you plan to run a couple of Docker VMs, then go with docker-machine, as Docker for Mac can only run a single VM under the hood.
  • Keep in mind that if you use Docker for Mac you also get docker-compose (“a tool for defining and running multi-container Docker applications”) in the package.
  • If you want to leverage the full power of Docker then you may like to use Linux since Docker was built for Linux - which we will cover in the next section.

Installing Docker on Linux

Docker was designed to run on Linux, so installation on this OS is trivial. Head over to the official installation instructions for Docker for Ubuntu to set yourself up. That’s it, really.

Preparing your application for iso(lation)

This section assumes that you already have a Ruby on Rails web application that you would like to containerize.

Docker setup

We will start off with a basic Dockerfile that should be sufficient for most Ruby on Rails web apps:

# Dockerfile.dev

# Base image 
FROM ruby:2.4.0-slim

# Install project dependencies
RUN apt-get update -qq && \
    apt-get install -y --no-install-recommends \
                        gcc \
                        g++ \
                        make \
                        patch \
                        git \
                        ssh \
                        libpq-dev \
                        postgresql-client-9.5 \
                        pkg-config \
                        ruby-dev \
                        zlib1g-dev \
                        liblzma-dev

# Add Postgres repo 
RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ jessie-pgdg main' >> /etc/apt/sources.list.d/pgdg.list && \
    curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -

# Install Postgres client 9.5
RUN apt-get update -qq && \
    apt-get install -y --no-install-recommends postgresql-client-9.5

# Ensure we don't check host for github.com when cloning gem repos
RUN mkdir /root/.ssh && \
    chmod 700 /root/.ssh && \
    echo "Host github.com\n\tStrictHostKeyChecking no\n" >> /root/.ssh/config

# Create working directory 
RUN mkdir /app
# and switch there
WORKDIR /app

# Config for bundle
ENV BUNDLE_APP_CONFIG=/app/.bundle \
    BUNDLE_GEMFILE=/app/Gemfile \
    BUNDLE_JOBS=2 \
    BUNDLE_PATH=/app/vendor/bundle

# Add our app files to the image
ADD . /app

# Install gems
bundle install

Even though we run bundle install in our Dockerfile (just in case we would like to provide a complete app image to someone eg. designer or copywriter), we will also keep gem files locally and mount them on run.

Gems are kept locally for 2 reasons:

  • ability to browse a gem’s code using a text editor on the host machine
  • installing new gems won’t require us to rebuild the whole image

docker-compose setup

We also need to provide info about our stack for docker-compose, which is used for orchestrating the whole application stack:

# docker-compose.dev.yml
version: '2'
services:
  web:
    image: myapp
    volumes:
      - .:/app
    links:
      - postgres
      - redis
    ports:
      - "3000"
    environment:
      - VIRTUAL_HOST=myapp.dev
    command: bundle exec rails s -b 0.0.0.0
  sidekiq:
    image: myapp
    volumes:
      - .:/app
    links:
      - postgres
      - redis
    command: bundle exec sidekiq
  postgres:
    image: postgres:9.5
    ports:
      - "5432"
    volumes:
      - postgres-data:/var/lib/postgres
  redis:
    image: redis:latest
    volumes:
      - redis-data:/data

volumes:
  postgres-data:
    driver: local
  redis-data:
    driver: local

We use Dockerfile.dev and docker-compose.dev.yml files to emphasize that they have been created for development purposes.

nginx-proxy setup

We also run nginx-proxy in another Docker container, for easy mapping between local domains and different Docker web applications. It works automatically - if we point myapp.dev to localhost (for docker-machine you need to use docker-machine ip), in the /etc/hosts file, then we will be able to access the app in the browser under this name. This is thanks to the VIRTUAL_HOST environment variable in the docker-compose file.

Running nginx-proxy is as simple as this: docker run -d -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock:ro jwilder/nginx-proxy

As luck would have it, it’s basically a fire and forget command. The container will be started automatically, every time your start Docker app / daemon.

Getting things up and running

Ok, now we need to build our app image:

docker build -f Dockerfile.dev -t myapp .

After that we can install gems for our app (locally):

docker-compose -f docker-compose.dev.yml run web bundle install

When it’s finished, we can start the whole stack:

docker-compose -f docker-compose.dev.yml up

When we update our code locally (using an editor on the host machine), changes will be propagated automatically thanks to local volume mapping (.:/app).

Helpful aliases for Docker

Typing docker-compose... every time we do things is cumbersome, even with tab complete from the command line (as it cycles through docker tools). We like to instead have a few bash aliases and functions to help work with Docker’ised apps:

# Aliases

# Docker
alias dbd=docker-build
alias drun=docker-compose-run
alias dup="docker-compose -f docker-compose.dev.yml up --remove-orphans"
alias ddown="docker-compose -f docker-compose.dev.yml stop"
alias dcl=docker-cleanup

# Docker & bundler
alias dbi="drun bundle install"
alias dbe="drun bundle exec"
alias dbet="drun -e RAILS_ENV=test bundle exec"

# Functions

# Build docker image
# docker-build $image-name
docker-build() {
  project_name=$1

  docker build -f Dockerfile.dev -t ${project_name} .
}

# Cleanup stopped container and unused images
docker-cleanup() {
  docker ps -a | grep "Exit\|Created" | awk '{print $1}' | xargs docker rm
  docker images | grep "none" | awk '{print $3}' | xargs docker rmi
}

# Run command against docker web container defined in
# docker-compose.dev.yml file.
#
# Pass -u argument to run as user with current user id
# Pass -e arguments for env variables, ie:
#   docker-compose-run -e RAILS_ENV test bundle exec rake db:migrate
docker-compose-run() {
  cmd=""
  var=""

  # Parse -e options
  while [[ $# > 0 ]]
  do
  i="$1"
  case $i in
    -e*|--environment*)
      var="${var} -e $2"
      shift
      ;;
    -u*)
      var="${var} -u $(id -u)"
      ;;
    *)
      cmd="${cmd} $1"
      ;;
  esac
  shift
  done

  # Trim whitespaces
  cmd=echo "${cmd}" | sed 's/^ *//' | sed 's/ *$//'
  var=echo "${var}" | sed 's/^ *//' | sed 's/ *$//'

  cmd="docker-compose -f docker-compose.dev.yml run --service-ports --rm ${var} web ${cmd}"
  echo "Running: ${cmd}"
  eval $cmd
}

With all these helpers configured, your Docker workflow can look as simple as this:

  • dbi - install all gems
  • dup - start whole stack
  • dbe rspec - run all rspec tests
  • dbe rake db:migrate - migrate the database
  • dbe rake db:drop db:create db:migrate - re-create database for test environment (btw. this could also be done with aliases!)
  • ddown - stop the whole stack (in case of errors occurring)
  • dcl - to clean stopped containers and unused images

Summary

We love Docker! We use it during the whole web app development cycle: from development to running the app on production servers. Want to leverage Docker for your app development or existing infrastructure? iRonin team are experts in setting up Docker and guiding businesses through how to use it effectively. If you’d like to incorporate Docker into your development cycle then make sure to reach out and have a chat with us - we’re always happy to help.