Development environments can be quite complex and time consuming to setup. Think about it, what tools do you need to have working for the most basic development environment? A short list for your average PHP developer would likely include a web server (often Apache), a database server (often MySQL) and PHP. Sounds like we are already finished right? Well, if you are installing all this through your operating systems package manager now you have to deal with the defaults your OS provider to set for all your software. This can differ quite widely between Linux distributions and wider still between binary distributions (XAMPP & WAMPP etc.). There are several solutions to these problems. As mentioned above you could use one of the pre-built LAMP bundles. This solution works well most of the time however there are situations where it can really be a problem. For example, if you need to add a PHP extension that isn't supported by your LAMP distribution, adding it can be really painful.
What other solutions exist? Well, as you might have guessed from the title of this post you can use Docker for this!
What is Docker?
Docker is a system that allows you to run one or more instances of an application in there own logically separated environments. You can think of docker as a light weight virtual machine system built for easily reproducible instances. The Docker developers and community provide many pre-configured images to help with this.
How does Docker help me setup my development environment?
Docker allows you to configure easily reproducible images. The images can be distributed via a repository but isn't necessary if you make a Docker build file that does all your setup work for you. Docker also provides ways of allowing Docker instances to interact with real machines as well as other Docker instances.
Build scripts? That doesn’t sound easy and/or fun!
If you are familiar with the build scripts of other build systems (such as Ant for example) you might already be put off. The build script syntax for Docker is fairly intuitive, well documented and allows you to get basic stuff up and running quickly.
The setup we use
Everyone will likely use Docker in their own way, the way that is 'best' for them. What works well for someone won't necessarily work well for someone else. This is often the way with something as complex as software, not to mention if someone wants to use an entirely different tool for the same job (Nginx vs Apache for example).
The setup we use is based around the idea of working in multiple projects potentially simultaneously. There for been able to switch projects quickly is important to keep efficiency as high as possible in an environment of many projects. The way we do this in our environment is the simplest possible, we have all the projects stored in a single directory and the same directory is the webroot for our local server. One project will be located at localhost/project1 and another project will be located at localhost/project2, simple but effective. Docker could be, and is designed to be, used to host a separate instance per project. Most projects for us are hosted in exactly the same server environment so this isn't really necessary in our case.
Implementing our way of working in Docker
To implement our setup in Docker requires a minimum of a single Docker instance to run Apache and PHP. MySQL can be run on the host machine but to make the environment more portable this should also run in it's own instance. Now we are up to two instances. One other tool yet to mention is phpMyAdmin. We rely on this quite a bit for various tasks as you might imagine. This could be run from the same Docker instance that runs all the projects but there are several reasons a separate Docker instance for this works much better. Now we have three instances.
The first instance to get working is MySQL. The Docker community provide a mysql pre-built image that can easily be used for this. One of the issues using any database server in Docker is where your data is actually stored. If you run the MySQL docker instance without any configuring the data will end up being stored in the actual Docker instance image. Storing the data in the instance is bad, docker instances are meant to be disposable, to a degree, and if all your data is in the instance that’s not a good thing. Fortunately, using Docker volumes, you can easily ensure your important data is not stored in the Docker instance. One other thing to consider when running MySQL in Docker, the Docker instance needs to be easily accessible to other Docker instances. To do this you will want to ensure your MySQL Docker instance is named. With all this in mind to get a basic MySQL instance running we end up with the command
docker run --name database -v /var/lib/mysql:/var/lib/mysql mysql:5
The "--name database" option names the instance as "database" not a very original name nor necessarilly good. What if there was another database server? Well, for now this is fine.
"-v /var/lib/mysql:/var/lib/mysql" defines the volume to store the actual database. /var/lib/mysql is the default location MySQL stores data. This command maps /var/lib/mysql on the host machine to /var/lib/mysql in the container. There for causing MySQL to store all data on the host machine.
"mysql:5" tell Docker which image to run and which tag of that image to run. As MySQL 5 is the latest version this could just be "mysql". However, if MySQL 6 where to be released (I'm sure this will be soon) and the Docker community released that as the latest version. Docker would automatically upgrade to the latest version of MySQL which isn't necessarily what you would want. There for adding ":5" tells Docker to only run the latest version of MySQL 5.
If you have been using the same or older version of MySQL on your host machine the Docker instance will probably be able to use the database files that the hosts MySQL instance used. This has the advantage that all your databases and users will be available automatically. You will have to change the permissions on the host machine to allow the Docker instance to access these files correctly. For me the host machine MySQL user's uid was 125, the Docker instance MySQL user's uid was 999. Simple chown -R 999:999 /var/lib/mysql ensures the Docker instance of MySQL has proper access to the files. This is because Docker instance volumes will pass through the permissions from the host machine.
PHP, Apache & PHPMyAdmin in Docker
To setup PHP and Apache is again relatively stright forward but you will almost certainly need to actually make your own image to have the right configuration for your needs. This is primarily because the default Docker images for PHP and Apache have only the basics installed and configured. You will probably want several PHP extensions installed and at least one Apache module enabling. Fortunately making a Docker image is quite easy.
To make a Docker image you will use a build file often referred to as "Dockerfile". Bellow is the Dockerfile we use to build the Apache and PHP Docker image with comments explaining what’s happening.
# Base our image off the community php image with the tag 5-apache
# The default PHP configuration is missing a few important extensions
# Install source packages to allow the extensions to be compiled
RUN apt-get update
RUN apt-get install -y libmcrypt-dev libfreetype6-dev libjpeg62-turbo-dev libpng12-dev libcurl4-gnutls-dev libssl-dev libicu-dev
# Configure gd and intl extensions with any options you need
RUN docker-php-ext-configure gd --with-freetype-dir --with-jpeg-dir --with-png-dir
RUN docker-php-ext-configure intl
# Install the extensions
RUN docker-php-ext-install mysql mysqli pdo pdo_mysql mcrypt gd intl
# Install xdebug
RUN yes | pecl install xdebug
# Copy the xdebug configuration that is in the same directory as this Dockerfile to the images file system
COPY xdebug.ini /usr/local/etc/php/conf.d/
# Enable mod_rewrite
RUN a2enmod rewrite
# Replace apache config with out modified one
COPY apache2.conf /etc/apache2/
This Docker file is pretty small but does a lot of things. The two main commands you are likely to need when configuring some thing is RUN and COPY. As you might guess RUN simply runs a command as root in the Docker instance. COPY copies a file or directory to a path inside the Docker instance. This allows you to install needed packages and replace any files you need modified very easily.
To actually get Docker to run the build script you would use this command:
docker build -t php5xdebug .
This command creates a new image with the tag php5xdebug. This is a bit of a better name than the previous one for MySQL but still has a few problems. What if I wanted a new instance running PHP 5.2 for some reason?
The Dockerfile for phpMyAdmin is similar but lacks xdebug and is running on PHP 7. The previous instance was running PHP 5 as we have a lot of legacy code that isn't compatible with PHP 7. You could easily make another instance for hosting projects that is running PHP 7. The only thing you really need to do is remove mysql from the list of installed extensions as PHP 7 no longer supports the mysql extension.
RUN apt-get update
RUN apt-get install -y libmcrypt-dev libfreetype6-dev libjpeg62-turbo-dev libpng12-dev libcurl4-gnutls-dev libssl-dev
RUN docker-php-ext-configure gd --with-freetype-dir --with-jpeg-dir --with-png-dir
RUN docker-php-ext-install mysqli pdo pdo_mysql mcrypt gd
COPY phpMyAdmin/ /var/www/html/
Remember I mentioned that running phpMyAdmin in it's own instance has it's advantages? Well, now you are no longer running xdebug and can have a totally different configuration much more easily than if you where running a single Apache instance on your host machine. To make managing phpMyAdmin simple I just had a copy in the same directory as the Dockerfile. This copy of phpMyAdmin is then copied into the Docker image.
There is supposed to be only one Dockerfile in a directory. The idea is that each project should have it's own Dockerfile. In this instance this isn't exactly how we are using it. however, having the separate directories is nice as the different assets the different images need are stored separately.
So we have built our images. How do we run them? In a similar way to MySQL.
docker run -v /opt/projects:/var/www/html --link database -p 80:80 php5xdebug
"-v /opt/projects:/var/www/html" Like with MySQL we are again using a volume from the host machine, this time as the webroot for the instance.
"--link database" tells Docker that this instance should have network access to another Docker instance named "database".
"-p 80:80" tells Docker to pass through port 80 on the host machine to port 80 on the Docker instance. Thus allowing normal access to the web server as if it was running on the host machine.
docker run --link database -p 8080:80 phpmyadmin
Pretty much the same as the the command for starting the regular Apache and PHP instance but on a different port and without a volume since phpMyAdmin was copied into the image when it was built.
At this point your pretty much done... except you need to reconfigure any existing projects to use the new database sever. Fortunately Docker provides name resolution for named instances. So the host name "database" can be used.
One other thing to consider. Passing the network ports thought to the host machine makes them network accessible. If you don't want them to be network accessible you can use the Docker instance's IP address to access them instead. Alternatively you could configure a firewall to block them.