Migrating our PHP applications to Docker without sacrificing performance
At We Make Waves, we’re continuously improving our cloud-based infrastructure and development workspaces. Most recently, we’ve migrated all of our projects from running on bare metal to Docker. Our motivation for doing so was to take advantage of:
- Isolation: We can run multiple applications with different dependencies on the same machine. E.g. Run different versions of Node/PHP per application. Applications won’t interact with each other, also allowing us to parallelise our integration tests on the same machine.
- Security: Applications run in their own isolated environment, and so a vulnerability in one won’t affect others. Docker also encourages you to only run and expose necessary programs.
- Reproducibility: We compile versions of our applications as images, meaning we can identically reproduce what would run in production anywhere else before.
- Service-orientation: Our applications could be split into smaller, horizontally scalable micro-services that interact with each other for better resource efficiency.
- Rapid Deployment: A container orchestrator starts and stops Docker containers on instances, instead of waiting for new instances to start. Speeding up rolling deployments from 15 minutes to less than 60 seconds (dependent on health-checks).
- Standardisation: Allows us to run officially maintained versions of common services such as MySQL, Redis, RabbitMQ easily.
For some of our projects, we use PHP that currently runs on bare metal EC2 instances with Ubuntu Linux. By moving to Docker, we don’t want to sacrifice performance for the advantages that Docker brings. So to ensure we don’t sacrifice performance, we’ve benchmarked our existing bare metal applications with siege which we can then compare against any PHP Docker solution.
All benchmarks have been run on a machine with the following specification:
- Ubuntu Linux 17.10 x64
- php-fpm 7.2
- nginx 1.12.2
- Intel Core i7–7567U @ 3.5Ghz CPU
- 16GB DDR4 RAM
- Samsung 850 EVO M.2 256GB
We’ve used a skeleton Symfony 4 application with the lucky numbers route added as the target benchmark.
Running this on bare metal with PHP-FPM + NGINX gives us the following results:
Solution 1: Official PHP-FPM + NGINX
In the official PHP repository on Docker Hub there are a few variants, including php-cli, php-fpm and php-apache. Naturally, this results in us first opting to try out php-fpm with an nginx container from the official nginx repository. This was very quick and simple to configure, only requiring an nginx configuration filealongside the docker-compose configuration. On the down-side, we found deploying this type of configuration quite difficult as we’d need a third data container for the nginx and php-fpm containers to share source code from. This made deployment configuration more complex and too granular for our service oriented architecture.
From a service-oriented viewpoint, we’d expect to have one container per purpose. A small Symfony app or WordPress CMS should only require one container to run the app, and a separate database container where necessary. Keeping the database container separate allows us to use external database services such as Amazon’s RDS in deployment and local docker databases in development.
Solution 2: PHP w/ Apache
With this in mind, we tried the official php-apache image that offers an Apache2 web server with mod-php. This only requires a single service in the docker-compose configuration and Apache2 configuration, but also required a separate Dockerfile to enable mod_rewrite.
Solution 3: Custom PHP-FPM w/ NGINX
We’re a lot more comfortable configuring php-fpm with nginx, and need the ability to easily add php modules and run database migrations on start, so we created our own image from the Ubuntu Xenial base. This features s6 overlay to supervise the php-fpm and nginx processes as well as executing scripts on start. The source for this is available here.
We ran the same benchmark on all of these Docker solutions. The results are shown in the table below.
From the results, it’s clear that our custom Docker PHP-FPM w/ NGINX container greatly outperformed the official PHP images by ~6x, with close performance to our current bare metal setup. We use images like this in all of our PHP Docker deployments as the small performance loss from bare metal is within a small enough tolerance for us to be happy and is offset by the advantages listed at the start of this post.