The Ultimate Development Environment: Moving from Vagrant to Docker for Mac!

Nicola Kabar
6 min readJul 20, 2016

--

Disclaimer: I work at Docker but I wasn’t asked to write this piece. I simply was looking for alternative development environments and wanted to share how I used Docker for Mac to create my dev environment.

A couple of years ago, a friend of mine introduced me to Vagrant. I was impressed by how easy it was to set-up a local dev environment with the OS flavor of my choice all running locally on my Mac using VirtualBox. I used Vagrant daily to tinker around, write and test code, replicate customer issues, and even run Docker!

Over time, I started hitting some issues with my Vagrant dev environment. To list a few:

Driver Issues: I was having many issues with Virtualbox. Not saying that this is a Vagrant issue. But my experience with Vagrant was relying on Virtualbox’s stability when creating or removing VMs. Simple things like closing my laptop would put my Vagrant machine in a weird state that can only be resolved with a reboot.
Networking: Guest VM port forwarding wasn’t trivial. I needed to pre-define which ports need to be forwarded to the guest VM. Changes can’t take effect without restarting the VMs.
File Syncing: Similar to networking, synced directories needed to be pre-defined. Changes can’t take effect without restarting the VMs.

and last but not least:

Speed: No matter how small my Vagrant box was, it took minutes to boot and get set up (given that Virtualbox wouldn’t act up). This is an important factor. Imagine if you only needed to change forwarded ports or alter provisioning scripts, you’d need to reboot and waste atleast a couple of minutes.

These issues made me think: What if I can bring the power of Docker containers to my dev environment? I wanted to quickly spin up different dev environments in a repeatable way. I wanted to ensure I can easily share folders from my Mac and have them automatically sync. I wanted to reach whatever services I’m running quickly and easily from my preferred browser. I did not want to waste resources just to stand up a development VM nor did I want to install anything locally on my Mac with `brew`.

Luckily, Docker developed a native Docker for Mac app ( and Docker for Windows too!) that was launched as a public beta during DockerCon 2016. Key thing to note is that Docker for Mac is deeply integrated with OS X native virtualization(Hypervisor Framework), networking and file system.

So last week, I started looking into using Docker for Mac to spin up my local dev environment in a Docker container. The first step is to define the Dockerfile that the dev container is created from. The second step is to define the host directory that gets mounted into the dev container at runtime. Combined, the container and its packages with the mounted directories make up my dev environment.

Here’s what I defined in my Dockerfile:

Base OS: I’m a big fan of Ubuntu and used it for many years. So I wanted to stick with Ubuntu as my dev OS. This translates to having `ubuntu:wily` as the base Docker image.
Packages: I then added the required packages. This was an iterative process. As I needed additional packages or different version of packages, I simply updated the Dockerfile and re-built the image. There was no need to uninstall any packages or worry about current versions installed.
Docker and Compose Clients: I wanted to use Docker client and Docker compose from within the dev container by mounting the Docker unix socket.
Go Environment: I’m learning Go so I wanted to have a proper Go setup in my dev environment. Today, i’m using version 1.6.2, but once I want to try the another version I can easily change the version and rebuild the image.
Users and Working Directory: I’m using `root` as the user since i also have root privileges on my Mac. I’m also use `/root` as the home dir.
Entrypoint/Command: Defined bash as an entrypoint for my container.

As a result, here’s how my Dockerfile ended up like ( you can find it here too!)

FROM ubuntu:wily
MAINTAINER Nicola Kabar <nicolaka@gmail.com>
# Installing some packages
RUN apt-get update -y \
&& apt-get install no-install-recommends -y \
build-essential \
vim \
git \
curl \
python-dev \
python-pip \
cmake \
wget \
sudo \
iputils-ping \
ansible \
ssh \
&& rm -rf /var/lib/apt/lists/*
# Installing Docker Client and Docker ComposeRUN curl -Ssl https://get.docker.com | sh
RUN pip install docker-compose
# Installing + Setting Up GO EnvironmentENV GOLANG_VERSION 1.6.2
ENV GOLANG_DOWNLOAD_URL https://golang.org/dl/go$GOLANG_VERSION.linux-amd64.tar.gz
ENV GOLANG_DOWNLOAD_SHA256 e40c36ae71756198478624ed1bb4ce17597b3c19d243f3f0899bb5740d56212a
RUN curl -fsSL “$GOLANG_DOWNLOAD_URL” -o golang.tar.gz \
&& echo “$GOLANG_DOWNLOAD_SHA256 golang.tar.gz” | sha256sum -c — \
&& sudo tar -C /usr/local -xzf golang.tar.gz \
&& rm golang.tar.gz
# Setting up GOPATH. For me, i’m using $HOME/code/go
ENV HOME /root
ENV GOPATH $HOME/code/go
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
# Setting WORKDIR and USER
USER root
WORKDIR /root
# Sample Bash Profile that will be overwritten if your mounted dir has a .profile
ADD bash_profile .profile
# Running Bash
CMD [“/bin/bash”, “-l”]

Next, I had to define the host directory that I wanted to mount to the container at runtime. My projects’ source-code, docker config, vim profiles, ssh configs and keys, git configs are better kept outside the Docker image. My thought was that these are either too sensitive to commit inside an image or that I wanted to use them from my Mac (e.g having same vim profiles on Mac and within the container). That’s why I have created a `dev` directory on my Mac that I mount as a home directory when I launch my dev container. This used to be my `config.vm.synced_folder` in Vagrantfile. This is how my `dev` directory looks like :

~/dev$ ls -a

.gitconfig
.bash_history
.local
.cache
.pip
.ansible
.docker
.profile
.viminfo
.aws
.ssh
code

Once I build the image from the Dockerfile, I run the container and I mount the ~/dev directory into `/root` ( default home dir for the root user) and the Docker socket. Then, I can start developing from within the container!

$ docker run -it --rm --hostname devcon -v /var/run/docker.sock:/var/run/docker.sock -v ~/dev:/root nicolaka/devcon:latestroot @ ~
[2] 🐳 → which go
/usr/local/go/bin/go
root @ ~
[3] 🐳 → echo $GOPATH
/root/code/go
root @ ~
[6] 🐳 → docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
722c2bb82740 nicolaka/devcon:latest “/bin/bash -l” 17 minutes ago Up 17 minutes kickass_babbage
root @ ~
[36] 🐳 → docker version
Client:
Version: 1.11.2
API version: 1.23
Go version: go1.5.4
Git commit: b9f10c9
Built: Wed Jun 1 21:54:25 2016
OS/Arch: linux/amd64
Server:
Version: 1.12.0-rc4
API version: 1.24
Go version: go1.6.2
Git commit: e4a0dbc
Built: Wed Jul 13 03:28:51 2016
OS/Arch: linux/amd64
Experimental: true
root @ ~
[33] 🐳 → docker-compose — version
docker-compose version 1.7.1, build 6c29830
root @ ~
[34] 🐳 → which git
/usr/bin/git

You can try running the container yourself on your Docker for Mac, simply run:

$ docker run -it --rm --hostname devcon -v /var/run/docker.sock:/var/run/docker.sock nicolaka/devcon:latest

When it comes to networking, If I need to expose any ports that I’d like to reach via a browser on my Mac using `localhost` , I can do port mapping when I launch the container. If the container’s already launched, then I can quit it and relaunch with the correct port configs. This is a sub-second delay as opposed to couple of minutes compared to Vagrant.

My `dev` Mac directory is already synced to the dev container’s home directory. All my code resides on my Mac so I can easily quit the container and fire up a new one without worrying about losing any data on that directories.

Summary: I used the newly-released Docker for Mac to create a local dev environment inside a Docker container. This approach allowed me to quickly spin up a dev environment , use less host resources, iterate faster, test faster, and focus more on building cool things!

--

--

Nicola Kabar

DevOps Enthusiast. SE @ HashiCorp. Previously: Docker, Tigera, Cisco.