Docker PHP development flow
Our trip to a fun, but still imperfect development environment.
The less technical part
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.
Install Homebrew, Brew Cask and Git
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
Install Docker and Docker Compose
brew install docker brew install docker-compose
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.
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:
To activate the change, you need to reload your terminal, or execute the following line to reload it on the fly:
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
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:
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.
Restarting your mac can fix the dns resolver.
Reload the vagrant container.
Flush DNS cache
sudo dscacheutil -flushcache;sudo killall -HUP mDNSResponder
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.