Joeri Verdeyen bio photo

Joeri Verdeyen

full-stack web-developer, cyclist, mountainbiker, Nespresso lover, Strava pusher.

Twitter LinkedIn Instagram Github Stackoverflow Last.fm Strava

How I develop in PHP with CoreOS and Docker

CoreOs Logo

Notice!!

Since Docker and all tools provided are moving fast, you shouldn’t be reading this post. We’ve written a new post about PHP development and Docker at Yappa. You can read it here: http://tech.yappa.be/docker-php-development

I’ve been using the Vagrant provisioned-with-Ansible-setup for a while now. But for the last month(s) I’ve been playing around with things like: Docker, boot2docker, CoreOS, etcd, .. I managed to setup a fast and easy way to develop my PHP applications. Symfony2 is my preferred weapon of choice, so I’ll explain how I’m developing a Symfony2 app.

The software stack

Most of these applications are installed with an automated Mac installation/configuration manager I’ve created a while ago. MacPlan, built on top of Ansible.

You can also download and install these applications manually or use Homebrew and Homebrew Cask to install them.

brew install vagrant
brew cask install virtualbox
brew install docker
brew install docker-compose

Vagrant and CoreOS

I’m working on Mac, so I need a virtual machine to run Docker server. I used to run boot2docker as a Docker host, but I recently switched to CoreOs because it feels faster. I didn’t benchmarked it yet.

Configure coreos-vagrant

git clone git@github.com:coreos/coreos-vagrant.git
cd coreos-vagrant
cp config.rb.sample config.rb
cp user-data.sample user-data

Edit config.rb, uncomment and edit the following lines.

$expose_docker_tcp=2375 # expose docker to other hosts then localhost
..
$share_home=true # mount your Mac home dir in the vagrant box
..
$vm_memory = 2048
$vm_cpus = 4
..
$forwarded_ports = { 2375 => 2375 } # use localhost as docker ip on your Mac

Once these things are done, you can start the CoreOS Vagrant box.

vagrant up

Vagrant will ask for sudo permission to change the /etc/hosts file on your Mac.

...
==> core-01: adding to (/etc/hosts) : 172.17.8.101  core-01  # VAGRANT: fc39e1aea8341bd5d302603e416aa3de (core-01) / 53f5faee-f39b-486e-a004-a6db956eaab1
Password:
==> core-01: Setting hostname...
==> core-01: Configuring and enabling network interfaces...
==> core-01: Exporting NFS shared folders...
==> core-01: Preparing to edit /etc/exports. Administrator privileges will be required...
==> core-01: Mounting NFS shared folders...
==> core-01: Machine already provisioned. Run `vagrant provision` or use the `--provision`
==> core-01: to force provisioning. Provisioners marked to run always will still run.

You can test the docker daemon (already) by running:

$ vagrant ssh -c "docker ps"
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
Connection to 127.0.0.1 closed.

Configure Docker client on Mac

The Docker client on your Mac don’t know where to look for Docker server (yet).

$ docker ps
FATA[0000] Get http:///var/run/docker.sock/v1.18/containers/json: dial unix /var/run/docker.sock: no such file or directory. Are you trying to connect to a TLS-enabled daemon without TLS?

The easiest way is to add to following line at the bottom of ~/.bash_profile. Or update your dotfiles in MacPlan or personal dotfiles management repository.

echo "export DOCKER_HOST=tcp://localhost:2375" >> ~/.bash_profile

Open a new terminal or export the variable in your current terminal.

export DOCKER_HOST=tcp://localhost:2375

At this moment, Docker client will have access to Docker server on your virtual Machine with CoreOS.

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

No containers running yet.

Docker compose

I’m always starting with (almost) the same docker-compose.yml file. This files describes the docker services I want to use.

CoreOs Logo

In short the following services are used:

  • A data container
  • An Nginx webserver on port 80
  • PHP-FPM listening in port 9000
  • A MySQL server
  • Mailcatcher

Data Container

I’m using this to get some volumes shared accros different containers (PHP, Nginx, ..) without file permission issues. Also the vendor folder will be symlinked to a volume on the docker containers, this speeds up the io.

Nginx

A standard webserver setup with a minimal amount of setup. I’m using a pre-built image, yappabe/docker-nginx.

PHP-FPM

A PHP-FPM instance waiting on a TCP socket, port 9000 to process the PHP files. Also a pre-built image, yappabe/docker-php. It’s possible to use an older versions of PHP, by using a tag on the docker image.

php:
    image: yappabe/php:5.4

The following tags are possible:

  • 5.6
  • 5.4
  • 5.3
  • default will be the latest built: 5.6

MySQL

A normal Mysql server, listening on port 3306. You can define the default credentials and pre-create a database. You can also leave these settings empty for random credentials and no database on creation.

Run the container

Starting form my default setup you can download the docker-compose.yml file and run the containers. Open a new terminal window.

composer create-project --no-install symfony/framework-standard-edition projectX
cd projectX
curl -O https://gist.githubusercontent.com/jverdeyen/850bc70015eff9f7ef35/raw/f7819f762ea7a3ddb9873f44557ed040728e8100/docker-compose.yml
docker-compose up

Docker will start pulling the images, but this will only be done once. After that, you shoud see an output log of the started containers.

Creating projectx_app_1...
Creating projectx_mailcatcher_1...
Creating projectx_mysql_1...
Pulling image tutum/mysql:latest...
..

mysql_1       | 150706  7:51:03 [Note] Event Scheduler: Loaded 0 events
mysql_1       | 150706  7:51:03 [Note] /usr/sbin/mysqld: ready for connections.
mysql_1       | Version: '5.5.43-0ubuntu0.14.04.1'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  (Ubuntu)

Open a new terminal window to check of all containers are running.

$ docker ps
CONTAINER ID        IMAGE                        COMMAND                CREATED             STATUS              PORTS                                            NAMES
4d228350470f        yappabe/nginx:latest         "/run.sh"              3 minutes ago       Up 2 minutes        0.0.0.0:80->80/tcp, 443/tcp                      projectx_nginx_1
a9f1580cfd84        yappabe/php:latest           "/usr/sbin/php5-fpm    3 minutes ago       Up 2 minutes        9000/tcp                                         projectx_php_1
337246de94d2        tutum/mysql:latest           "/run.sh"              3 minutes ago       Up 2 minutes        0.0.0.0:3306->3306/tcp                           projectx_mysql_1
7ac07efc9df1        yappabe/mailcatcher:latest   "mailcatcher --smtp-   3 minutes ago       Up 3 minutes        0.0.0.0:1025->1025/tcp, 0.0.0.0:1080->1080/tcp   projectx_mailcatcher_1
91a1ff1f1d90        yappabe/data:latest          "/bin/sh"              3 minutes ago       Up 3 minutes                                                         projectx_app_1

Install vendors

Now we need to install our vendors.

docker exec -i -t projectx_php_1 bash -c 'cd /var/www/app/ && ln -sf /vendor /var/www/app/vendor && composer install'

Fill in the correct credentials for MySQL when asked. Fill in “mysql” as database host as localhost will not work. This is because of the Docker Compose magic, it links all the containers together and updates their hosts file accordingly.

Clearing the Symfony2 Cache is also easy.

docker exec -i -t projectx_php_1 bash -c '/var/www/app/app/console cache:clear'

Now you can visit your Symfony2 application at http://172.17.8.101, and start developing.

Since app_dev.php has an internal check on localhost, you can’t visit the page. An easy fix is to remove the following lines from app_dev.php.

// This check prevents access to debug front controllers that are deployed by accident to production servers.
// Feel free to remove this, extend it, or make something more sophisticated.
if (isset($_SERVER['HTTP_CLIENT_IP'])
    || isset($_SERVER['HTTP_X_FORWARDED_FOR'])
    || !(in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', 'fe80::1', '::1')) || php_sapi_name() === 'cli-server')
) {
    header('HTTP/1.0 403 Forbidden');
    exit('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.');
}

Extra: Improve performance

There are some quick tips to improve the speed of a Symfony2 application while developing.

  • Use /dev/shm as your cache and logs location
  • Finetune the nfs mount command in the coreos-vagrant Vagrantfile

Next?

I’ve also written something about automagic discover docker containers with dnsdock and CoreOs.

Setting up a DNS service discovery with SkyDNS or DNSDock

Thanks for reading

Feel free to leave a comment if you have remarks or like this post