Today I was working on a php development environment using docker on MacOSX (you might run into the same problems on windows, too!). The osxfs in Docker for Mac is painfully slow and the company behind docker is actually aware of that. At #77 they track the issue at docker-for-mac right now. At the time of writing the ticket is still open.

If you happen to use symfony as PHP framework or in particular composer as package manager for php projects, you end up with a very big directory called vendor .

In symfony standard edition I am counting 10259 files and folders.

For this blog post I put a vanilla symfony 3.2.7 project at DracoBlue/symfony-composer-docker-performance-test. With this commit I added a docker-compose.yml and a nginx.conf to run app.php on http://app.locahost.me:8080 and app_dev.php on http://dev.locahost.me:8080 .

If you run a quick benchmark against app_dev.php:

$ ab -n 1000 -c 16 http://dev.localtest.me:8080/ Concurrency Level: 16 Time taken for tests: 319.303 seconds Complete requests: 1000 Failed requests: 0 Requests per second: 3.13 [#/sec] (mean) Time per request: 5108.845 [ms] (mean) Time per request: 319.303 [ms] (mean, across all concurrent requests) Transfer rate: 131.33 [Kbytes/sec] received

you end up with response times for a vanilla symfony project of round about 319ms.

The app.php:

$ ab -n 1000 -c 16 http://app.localtest.me:8080/ Concurrency Level: 16 Time taken for tests: 86.140 seconds Complete requests: 1000 Failed requests: 0 Requests per second: 11.61 [#/sec] (mean) Time per request: 1378.234 [ms] (mean) Time per request: 86.140 [ms] (mean, across all concurrent requests) Transfer rate: 54.50 [Kbytes/sec] received

looks better (86ms), but is not very convenient for development.

Let's tune this with a simple trick down to 46ms (-85%) for app_dev.php and down to 9ms (-88%) for app.php.

The solution

Given that in case of the php-fpm container the vendor directory can be considered readonly, why might solve it with an always up to date in-docker data volume for just the vendor directory.

With this commit I replaced:

php-fpm : image : exozet/php-fpm:7.1.2 volumes : - ./:/usr/src/app

with:

php-fpm : image : exozet/php-fpm:7.1.2 volumes_from : - lsyncd lsyncd : image : dracoblue/lsyncd environment : - SOURCES=/mnt/vendor - DESTINATIONS=/usr/src/app/vendor - EXCLUDES=.svn:.git:.docker volumes : - ./:/usr/src/app - ./vendor:/mnt/vendor - /usr/src/app/vendor

in the docker-compose.yml .

Now I ran the benchmarks again. First in dev mode with app_dev.php:

$ ab -n 1000 -c 16 http://dev.localtest.me:8080/ Concurrency Level: 16 Time taken for tests: 46.269 seconds Complete requests: 1000 Failed requests: 0 Requests per second: 21.61 [#/sec] (mean) Time per request: 740.303 [ms] (mean) Time per request: 46.269 [ms] (mean, across all concurrent requests) Transfer rate: 906.28 [Kbytes/sec] received

Down from 319ms/request to 46ms/request!

Afterwards the app.php:

$ ab -n 1000 -c 16 http://app.localtest.me:8080/ Concurrency Level: 16 Time taken for tests: 9.688 seconds Complete requests: 1000 Failed requests: 0 Requests per second: 103.22 [#/sec] (mean) Time per request: 155.002 [ms] (mean) Time per request: 9.688 [ms] (mean, across all concurrent requests) Transfer rate: 484.57 [Kbytes/sec] received

Down from 86ms/request to 9ms/request!

How does it work?

The /usr/src/app is mounted from the host system (e.g. by docker with osxfs) into php-cli and the php-fpm container.

The relevant filesystem looks like this:

/usr/src/app contains all data + mounted from host with osxfs

After the modification, the subfolder /usr/src/app/vendor is over-mounted as docker volume (with no reference to the host). This way the data volume is docker only and very fast.

But at the beginning the vendor folder is empty:

/usr/src/app contains all data (except vendor!) + mounted from host with osxfs /usr/src/app/vendor contains no data + is not mounted from host

By adding an additional lsyncd container with:

lsyncd : image : dracoblue/lsyncd environment : - SOURCES=/mnt/vendor - DESTINATIONS=/usr/src/app/vendor - EXCLUDES=.svn:.git:.docker volumes : - ./:/usr/src/app - ./vendor:/mnt/vendor - /usr/src/app/vendor

and using

php-fpm: image: exozet/php-fpm:7.1.2 volumes_from: - lsyncd

to inherit the volumes from lsyncd for the php-fpm container sets up a sync from /mnt/vendor to /usr/src/app/vendor .

We have now this setup:

/mnt/vendor is mounted from host with osxfs /usr/src/app contains all data (except vendor!) + mounted from host with osxfs /usr/src/app/vendor receives incremental updates from /mnt/vendor + is not mounted from host

Whenever a file is changed on the host in the vendor folder, the lsyncd will sync the files from /mnt/vendor to /usr/src/app/vendor . I tested this on my local box and had a lag of max. 1 second.

If you need two way sync, take a look at docker-sync by Eugen Mayer instead!

Drawbacks

One-Way-Sync: Whenever you mount the /usr/src/app/vendor folder like this, it won't be writeable by the container. So please don't mount it on your php-cli container and use it just for php-fpm containers. Diskspace: The vendor directory exists two times, so you use double amount of hdd space Initial-Sync: The lsyncd is started with init configured, so there will be an initial sync whenever you boot up the container (between 5 seconds and 2 minute, depends on your project and hard disk speed).

References

To end up with this solution, I read lots of posts in dockers forum, blogs or on stackoverflow. Additionally I tried most of the existing solutions. That's why I want to share them here, so you might have a starting point of the lsyncd' volume is not sufficient for your usecase.

Update: Other solution in docker >=14.04.0-ce: Suffix the volume with :cached

(Updated 2017/04/07 14:16 GMT+2)

With Version 17.04.0-ce (currently in edge, not yet stable! Thanks to eyeohno for the info) there will be a cached flag for volumes. See 17.04.0-ce release notes for further information.

So you can use:

php-fpm : image : exozet/php-fpm:7.1.2 volumes : - ./:/usr/src/app:cached

then.

Results on the same hardware (app_dev.php):

$ ab -n 1000 -c 16 http://dev.localtest.me:8080/ Concurrency Level: 16 Time taken for tests: 84.148 seconds Complete requests: 1000 Failed requests: 0 Total transferred: 42939000 bytes HTML transferred: 42614000 bytes Requests per second: 11.88 [#/sec] (mean) Time per request: 1346.369 [ms] (mean) Time per request: 84.148 [ms] (mean, across all concurrent requests) Transfer rate: 498.32 [Kbytes/sec] received

and app.php

$ ab -n 1000 -c 16 http://app.localtest.me:8080/ Concurrency Level: 16 Time taken for tests: 21.034 seconds Complete requests: 1000 Failed requests: 0 Total transferred: 4807000 bytes HTML transferred: 4572000 bytes Requests per second: 47.54 [#/sec] (mean) Time per request: 336.546 [ms] (mean) Time per request: 21.034 [ms] (mean, across all concurrent requests) Transfer rate: 223.18 [Kbytes/sec] received

So we end up with stats like these:

file | docker | docker | docker | lsyncd ad-hoc | 17.03.0-ce | 17.04.0-ce | 17.04.0-ce | container | | | :cached) | ------------------------------------------------------------------------ app_dev.php | 319ms/req | 312ms/req | 84ms/req | 46ms/req app.php | 86ms/req | 83ms/req | 21ms/req | 9ms/req