3 ways to get Docker for Mac faster on your Symfony app.
UPDATE (2017-05-05): article has been updated for docker-sync 0.4.
Docker is an amazing tool to set up your whole development platform locally (and elsewhere too). As you may know, it has many benefits, including:
- Same, versioned and deterministic configuration accross environments and developers.
- Ease of use (adding any technical stack to the project is very easy, with just a few lines of configuration).
- Much better performance than a VM, at least on Linux (that's the point of this article).
- Easy deployment.
However, you probably have noticed that running a Symfony app with Docker for Mac is very slow, almost unusable however fast is your Mac. This article will explain several ways to make your Symfony runnable on Docker for Mac.
We are first going to set up a simple Symfony project to evaluate how the exposed solutions perform. If you already have a Symfony project, you can skip directly to the solutions.
The performance tests have been measured on a 2016 MacBook Pro and are intended to show the variations depending on the solutions.
Demo project setup.
The example project will set up a very simple Symfony app with a typical Docker setup with the following containers:
- Nginx webserver.
- PHP 7.1.
- A simple container for the Composer binary.
The Symfony app will be stored in an app folder and the project file structure will look like this:
π symfony-docker-test
π app
π docker
π php
π Dockerfile
π php.ini
π nginx
π app.conf
π docker-compose.yml
Setup the Symfony project
In the symfony-docker-test folder, run:
symfony new app
This needs the symfony
command to be installed on your host, as described on the Symfony installation doc.
Define containers
Edit the docker-compose.yml file at the root of the project:
version: '2'
services:
php:
build: ./docker/php/
environment:
TIMEZONE: Europe/Paris
volumes:
- ./docker/php/php.ini:/usr/local/etc/php/php.ini:ro
- ./app:/var/www/app
working_dir: /var/www/app
webserver:
image: nginx:1.11
depends_on:
- php
volumes_from:
- php
volumes:
- ./docker/nginx/app.conf:/etc/nginx/conf.d/default.conf:ro
ports:
- 8080:80
composer:
image: composer:1.4
volumes_from:
- php
working_dir: /var/www/app
Setup the PHP container
Add the following Dockerfile file to the docker/php folder:
FROM php:7.1-fpm
# Install recommended extensions for Symfony
RUN apt-get update && apt-get install -y \
libicu-dev \
&& docker-php-ext-install \
intl \
opcache \
&& docker-php-ext-enable \
intl \
opcache
# Permission fix
RUN usermod -u 1000 www-data
Add PHP configuration to the docker/php/php.ini file:
date.timezone = ${TIMEZONE}
short_open_tag = Off
log_errors = On
error_reporting = E_ALL
display_errors = Off
error_log = /proc/self/fd/2
memory_limit = 256M
; Optimizations for Symfony, as documented on http://symfony.com/doc/current/performance.html
opcache.max_accelerated_files = 20000
realpath_cache_size = 4096K
realpath_cache_ttl = 600
Setup the Vhost for Nginx
Edit the docker/nginx/app.conf
file:
upstream php-upstream {
server php:9000;
}
server {
root /var/www/app/web;
listen 80;
server_tokens off;
location / {
try_files $uri @rewriteapp;
}
location @rewriteapp {
rewrite ^(.*)$ /app.php/$1 last;
}
location ~ ^/(app|app_dev|app_test|config)\.php(/|$) {
fastcgi_pass php-upstream;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTPS off;
}
}
Run the project
Now that the project has been set up, simply run:
docker-compose up -d
The app should now be available at http://127.0.0.1:8080 and you should see this kind of page:
So... everything is fine, expect one thing: the app is very slow for now π©
Let's run a simple benchmark for the prod and dev environments (you will mostly use the later):
ab -n 100 -r http://127.0.0.1:8080/
ab -n 100 -r http://127.0.0.1:8080/app_dev.php
Benchmark results:
- Prod: 17 seconds
- Dev: 129 seconds (ouch!)
Solution 1: Move vendors out of the shared directory
After several tests (as performed here: https://github.com/michaelperrin/docker-symfony-test), it turned out that the bottleneck that slows down the application is the sharing of the vendor dir, that has a lot of files in it.
One efficient solution is to separate the vendor dir and make it only available in the container and not on the host.
Surprisingly, not sharing the cache
directory doesn't result in a big win, but it can be a second step of performance tuning.
To do so, edit your composer.json
file in the app folder and add the config-dir
parameter in the config
entry:
{
...
"config": {
...
"vendor-dir": "/app-vendor"
}
}
Edit the app/autoload.php file in the app folder and change this line:
$loader = require __DIR__.'/../vendor/autoload.php';
to
$loader = require '/app-vendor/autoload.php';
Add the /app-vendor
folder as a volume for the php
container in docker-compose.yml
:
services:
php:
# ...
volumes:
# ...
- /app-vendor
Make sure you have cleared your Symfony cache folder and install composer dependencies again by running this command:
docker-compose run --rm composer install
Benchmark results:
- Prod: 2.8 seconds
- Dev: 16 seconds
Let's see if we can do even better without cache sharing. Edit Symfony's AppKernel.php file to change the getCacheDir
method this way:
class AppKernel
{
// ...
public function getCacheDir()
{
return '/dev/shm/symfony_docker_test/cache/'.$this->environment;
}
}
Benchmark results:
- Prod: 1.2 seconds
- Dev: 5 seconds
Not bad. But beware! The vendor files are now concealed in your container and won't show anymore on the host. You won't be able to debug the vendors, and autocomplete won't be available in your IDE. My advice? Install first the dependencies in the standard directory, and then change again the composer.json file to make them installed in the container. This workaround is not as bad as it sounds if your dependencies don't change often.
Solution 2: Docker sync
Docker-sync (http://docker-sync.io/) is a tool that makes use of rsync to synchronize volumes files between the host and your containers instead of using Docker's osxfs system.
Install docker-sync:
sudo gem install docker-sync
Add a docker-sync.yml
file at the root of the project:
version: '2'
syncs:
app-sync:
src: './app'
Copy the docker-compose.yml
file to docker-compose-dev.yml
file and add these lines to the end:
volumes:
app-sync:
external: true
Use a named volume for your app's code by changing this:
services:
#...
php:
#...
volumes:
# ...
- ./app:/var/www/app
to:
services:
#...
php:
#...
volumes:
# ...
- app-sync:/var/www/app
Add now a Makefile
file at the root directory, that will allow easy start / stop commands whatever the system the project is run on:
OS := $(shell uname)
start_dev:
ifeq ($(OS),Darwin)
docker volume create --name=app-sync
docker-compose -f docker-compose-dev.yml up -d
docker-sync start
else
docker-compose up -d
endif
stop_dev: ## Stop the Docker containers
ifeq ($(OS),Darwin)
docker-compose stop
docker-sync stop
else
docker-compose stop
endif
You can now start your project by running:
make start_dev
This will start your containers and the docker-sync daemon (answer yes to all questions the first time).
Benchmark results:
- Prod: 0.6 seconds
- Dev: 1.2 seconds
That's a big win! Unfortunately, I experience several sync problems at times, with files not being synced from the host to the container, and with some user right issues on some files as well (chmod 777 to the rescue).
Solution 3: Docker's cache system
The Docker team is aware of the slowness of Docker for Mac (see here and here)
The latest beta (aka. "Edge") versions of Docker have introduced some new ways to mount volumes.
If you have downloaded the edge version, simply change the docker-compose.yml
file at the root of the project to add the :cached
option to the volume share:
services:
php:
# ...
volumes:
- # ...
- ./app:/var/www/app:cached
- Prod: 5.1 seconds
- Dev: 15.7 seconds
Not bad, but not as efficient as the other solutions. That can be enough for some projects though. I can't tell for now if there are downsides for this solution, but I will experiment it from now.
Conclusion
Good news! Docker on Mac can be usable for your Symfony project. There is no perfect solution though. Have a look to a summary of all the possibilities below and take your pick!
Dev benchmark | Prod benchmark | Pros | Cons | |
---|---|---|---|---|
Default | 129 seconds | 17 seconds | Easy | Unusable |
Solution 1: Non-shared vendors and cache | 5 seconds | 1.2 seconds | Fast | Vendors are not visible to host |
Solution 2: Docker-sync | 1.2 seconds | 0.6 seconds | Very fast | Use of 3rd-party tool |
Solution 3: Docker's volume cache | 15.7 seconds | 5.1 seconds | Easy solution | Not as fast. Experimental for now. |