But Docker can also be a very useful tool for local development, especially when it comes to making repeatable builds and environments faster and easier.
Docker is under very active development, and the best way to get started seems to change every couple of months. As of this writing (2015-09-28), the best way to get set up on OS X seems to be via Docker Toolbox (which can also be downloaded using Homebrew Cask:
brew cask install dockertoolbox.)
Docker Toolbox automates the setup of the Docker runtime and several supporting tools. On OS X, this means ensuring that VirtualBox is installed, installing
docker-machine and using it to create a VirtualBox
boot2docker1 VM to host the Docker daemon.
Once Docker Toolbox is installed, we should have access to the Docker Quickstart Terminal. This application simply opens OS X’s Terminal.app with a wrapper script, running a few checks and setting up environment variables so that the
docker commands know how to find the Docker daemon running on the VM.
Rather than jumping over to the Docker Quickstart Terminal every time I want to do something with Docker locally, I’ve simply added the following to my Bash configuration so that it sets up the correct environment with each new shell session:
# Connect docker client to Docker Toolbox's boot2docker VM # (A docker-machine created VirtualBox VM called 'default') eval $(docker-machine env default)
After sourcing this file or starting a new shell, we should have some environment variables set for Docker:
$ env | grep DOCKER DOCKER_HOST=tcp://192.168.99.100:2376 DOCKER_MACHINE_NAME=default DOCKER_TLS_VERIFY=1 DOCKER_CERT_PATH=/Users/english/.docker/machine/machines/default
Now, I can run
docker ps and other Docker commands from any Bash shell and connect to the Docker daemon running on the
default Virtual Machine.
Testing It Out
Let’s say we’ve been developing a Rails app on OS X, and we’d like to test it on Linux so we can document its dependencies in preparation for a production deployment. We could use a tool like Vagrant to spin up a Linux VM from a known basebox, but if we’re splitting our time between several projects, we may not want to deal with the overhead of downloading and running several full VMs on our development workstation.
Since Docker uses containers, we only need one VM running Linux (or none if our workstation is already running Linux, but here, we’re assuming development on OS X). Container images can be much smaller than full VM images, and they can also be spun up much faster since the VM that hosts them only has to boot once.
Let’s start an Ubuntu container and see if we can get our application running inside of it.
docker run --rm -ti ubuntu:14.04 bash
This will pull down the official Ubuntu 14.04 Docker image from the
_/ubuntu Docker Hub repo (with the
14.04 tag) if we don’t already have it it locally. It will create a new container using that image (in the
default VM provided by Docker Toolbox), then run the command
bash within that container, attaching it to our terminal in interactive mode (
-ti). After running, it will remove the container (
--rm), not saving any modifications that we may have made to it.
Once all of the layers2 of the image are pulled down, we should see something like this:
By default, we’re logged in to the running container as
8d6cb95178f4 is the container’s ID. We can use this later to operate on the container. If we poke around a bit, we’ll see that we’re in a minimal Ubuntu Linux environment. Let’s exit and try something more advanced.
This time, let’s attach a volume containing the source code for our application. This will let us access that directory from within the running container. WARNING: This is not a copy! Modifications will also be made to the directory on our workstation.
From our application’s source directory, try this:
docker --rm -v $PWD:/src ubuntu bash
-v $PWD:/src. This will mount the current working directory from our host as a volume at
/src in the container3.
We should now be able change to that directory and poke around. We should be able to see files from our app’s source repository and working within the container, create files that show up on our workstation.
root@9c0c1fc48459:/# cd /src/ root@9c0c1fc48459:/src# ls Gemfile Gemfile.lock Guardfile LICENSE README.md README.nitrous.md Rakefile app bin config config.ru db features lib log public script spec vendor root@9c0c1fc48459:/src# cat config.ru # This file is used by Rack-based servers to start the application. require ::File.expand_path('../config/environment', __FILE__) run SampleApp::Application root@9c0c1fc48459:/src# ls Gemfile Guardfile README.md Rakefile bin config.ru features lib public spec Gemfile.lock LICENSE README.nitrous.md app config db fromdocker.txt log script vendor root@9c0c1fc48459:/src# touch fromdocker.txt root@9c0c1fc48459:/src# exit vonnegut:sample_app_rails_4 english$ ls Gemfile LICENSE Rakefile config features log spec Gemfile.lock README.md app config.ru fromdocker.txt public vendor Guardfile README.nitrous.md bin db lib script
We now have the basic tools for doing local development with Docker. We’ve pulled images from Docker Hub, we’ve run local containers based on those images, and we’ve mounted volumes into these containers allowing us to interact with files on our host workstation.
1. The name “boot2docker” has been used to refer to both the minimal VM for hosting a Docker daemon and the package that was a predecessor to the Docker Toolbox. Here, we refer to the minimal VM with the Docker daemon installed. The Boot2Docker package has been superceded by the Docker Toolbox as the preferred means of installing Docker on OS X. ↩
2. Docker images are built up in layers using union filesystems with copy on write (CoW) semantics. This allows some lower layers to be shared between different images. Pulling down an image pulls down all of the image’s layers. If we omitted the
--rm from our
docker run commands and made modifications to the container’s filesystem, we could save a new image with a new layer added on top of all the previous layers. For more on Docker’s union filesytems, see Jérôme Petazzoni’s “Deep Dive into Docker Storage Drivers”. ↩
3. One of the things that Docker Toolbox simplifies for us is mounting filesystems into the boot2docker VM. By default, it sets up a file sharing mount of
/Users on the boot2docker VM. This allows
-v in our
docker run commands to mount individual directories into individual containers. If you need to mount something outside of
/Users, you will need to manually set up the file sharing in VirtualBox to support it. ↩