Our trip to a fun, but still imperfect development environment.

The less technical part

The problem

During a regular work day we work on several PHP projects. Sometimes new projects, but also legacy code which still require earlier versions of PHP. We all work on Macbooks and want to switch quickly and easily between projects. The project requirements vary, the PHP version may be different, or additional services may be required (such as Redis, Elasticsearch, ..).

Unable to mimic the production environment without spending countless hours installing packages on a virtual box for each project.

The roads we've travelled

We started from a local development server, which had the issue that only one PHP version was available. A result of this was that we were less likely to use newer PHP versions as it might break projects wth legacy code. We mounted project files using smbd/nfs/sshfs, which is pretty unstable and more often than not caused issues with git and tools such as gulp.

An alternative was a set of remote development servers but that would be pricey, unmaintainable and if those go down.. we'd all be idle, doing nothing.

There was also LAMP, but we discarded that option pretty quickly, environments would be too diverse.

The next most logical step to us was vagrant. We'd set it up with a set of ansible playbooks and it worked pretty good for some time, but there were issues with high memory usage and building took ages. We did look into packer, but the image sizes would grow too large, and space is somewhat limited.

The not-(yet)-so-holy-grail: Docker

All that glitters is not gold, as we struggled our way to an almost perfect development environment.

What we achieved:

Different versions for packages (PHP, Mysql, ..) per project

Our code is cloned on our Macbook SSD

Add/Remove extra services with a blink of an eye

Easily simulate production environments

Low memory footprint

Start development on a cloned project within seconds

Access projects locally as: http://www.project.docker/

Bleeding edge, yeey!

The technical part

Let's get down to business, in the next parts we'll show you how we're using docker in our development process.

Requirements

Install Requirements

Install Xcode

xcode-select --install

Install Homebrew, Brew Cask and Git

Install Homebrew

ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Install Brew Cask

brew install caskroom/cask/brew-cask

Install git with brew

brew install git

Install Vagrant and Virtualbox

brew cask install vagrant brew cask install virtualbox vagrant plugin install vagrant-disksize

Install Docker and Docker Compose

brew install docker brew install docker-compose

Vagrant up

Clone our Vagrant setup in your favorite development directory.

git clone https://github.com/yappabe/vagrant-docker.git cd vagrant-docker

Start the Vagrant box. This can initially take a while.

vagrant up

You can check the docker daemon in the Vagrant box.

$ vagrant ssh -c 'docker ps' CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES d370b4d9e575 tonistiigi/dnsdock "/go/bin/dnsdock"

You should see a dnsdock container running. If not, something might have gone wrong.

Now we should have:

a Vagrant box with a running docker daemon

a dnsdock container which will serve as our development dns-server to make sure that http://www.project.docker will (for example) point to the correct Apache or Nginx container

the Vagrantfile mounted the current user folder in the Vagrant box. /Users/user is mounted as /Users/user in the Vagrant box

the Vagrantfile forwarded the Docker api port (2375) to localhost

Accessing the Docker daemon

The fastest (and easiest) way to access your docker containers is from your command line. However, there's some steps that should be taken before you're actually able to.

When you'll try to run a docker command, you'll get the following:

$ docker ps Cannot connect to the Docker daemon. Is the docker daemon running on this host?

The reason for this error is because the $DOCKER_HOST variable isn't set yet (or incorrectly). Just set it using the following command:

DOCKER_HOST="tcp://localhost:2375" docker ps

It's good practice to add this to your dotfiles so add it to .bash_profile:

export DOCKER_HOST=tcp://localhost:2375

To activate the change, you need to reload your terminal, or execute the following line to reload it on the fly:

source ~/.bash_profile

Congratulations, you're now able to access the Docker daemon from the terminal. But we're not done yet!

We still need to do the following:

Create development containers (Nginx/PHP-FPM/Mysql/..)

Resolve http://www.project.docker to the webserver container

Create development containers

This is the fun part. Defining the services we need. We use Docker Compose to define our default set of services.

Create a test project

Create a new folder in your home folder and add a Hello World file.

mkdir ~/Development/hello-world cd ~/Development/hello-world echo '<?php echo "Hello world!";' > index.php

Add a Docker Compose configuration in a new file named docker-compose.yml.

app: image: busybox volumes: - .:/var/www/app tty: true nginx: image: yappabe/nginx links: - php volumes_from: - app environment: DOCUMENT_ROOT: /var/www/app/ INDEX_FILE: index.php PHP_FPM_SOCKET: php:9000 DNSDOCK_ALIAS: www.project.docker php: image: yappabe/php:7.0 working_dir: /var/www/app volumes_from: - app

Start containers

We are now set to startup our containers.

cd ~/Development/hello-world docker-compose up

You will see some pulling, downloading and extracting.

Creating helloworld_app_1... Pulling php (yappabe/php:7.0)... 7.0: Pulling from yappabe/php 69d893a34f64: Pull complete Digest: sha256:0e49326a8360853f2291db375322c65bc2c26a8923b9fd6c640dd5774097be3d Status: Downloaded newer image for yappabe/php:7.0 Creating helloworld_php_1... Creating helloworld_nginx_1... Attaching to helloworld_app_1, helloworld_php_1, helloworld_nginx_1 php_1 | [12-Nov-2015 18:04:30] NOTICE: fpm is running, pid 1 php_1 | [12-Nov-2015 18:04:30] NOTICE: ready to handle connections

Now we have:

A PHP-FPM container listening on port 9000

A Nginx container listening on port 80

A data container sharing data between containers and mounting local volumes

But, we still can't access http://www.project.docker.

Setup dns resolving

We need to tell our Mac to look at the dnsdock container to resolve the www.project.docker domain to the Nginx container.

Add a resolver config

sudo vim /etc/resolver/docker

Add the following line:

nameserver 172.17.8.101

This will tell our Mac to resolve all *.docker domains to the 172.17.8.101host, which is our Vagrant box. Since dnsdock forwards port :53 it well be accessed.

Flush your dns cache afterwards. If dns resolving isn't working properly, this might be the solution.

sudo dscacheutil -flushcache;sudo killall -HUP mDNSResponder

Add a route to the containers

We also need to tell our mac to route all our container IP request to the Vagrantbox. Containers are in the 172.18.0.0 range.

Notice: When you've the vagrant-triggers plugin installed, this step isn't required anymore. Our new Vagrantfile will add routes by default. This commit will take care of it.

sudo route -n add -net 172.18.0.0 172.17.8.101

Warning: Mac OS X will purge routes when you restart. Remember to add this on every reboot or create service for this command.

Access the webserver

Now you should be able to access the webserver on http://www.project.docker and see the Hello world! message.

Not working?

Restarting your mac can fix the dns resolver.

Reload the vagrant container.

vagrant reload

Flush DNS cache

sudo dscacheutil -flushcache;sudo killall -HUP mDNSResponder

Wrap-up

We've used and tried a lot of different methods but for now this is the most stable solution. The demo is a very basic example on how you can leverage from Docker containers. In a future blog post we will explain a bit more about our Symfony2 development and extra services.