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.