DEV Community

Cover image for 4 steps to improve Laravel + Docker performance issues
Chris Shennan
Chris Shennan

Posted on • Updated on • Originally published at chrisshennan.com

4 steps to improve Laravel + Docker performance issues

Ok, so the title is a little misleading. I was attempting to improve the performance of a Laravel application that was running inside docker but the improvements I made would work with other frameworks i.e. Symfony, CodeIgniter or even your own bespoke application. In this particular case, these changes resulted in a performance gain of almost 90%.

The performance improvements that I introduced were (I'll cover each of these in a bit more detail below).

  • Checking for DNS issues
  • Installing PHP Opcache
  • Configuring Nginx to handle OPTIONS requests
  • Installing docker-sync

Checking for DNS issues

DNS resolution issues can greatly affect the performance of your application and can leave you trying to debug applications for performance issues when your application is working fine. In my case, I traced the DNS resolution issues down to the use of a .test domain extension, i.e. example.domain.test. To try and identify the root cause, I created some sample entries in the /etc/hosts file to test with i.e.

127.0.0.1   example.domain.test
127.0.0.1   example.domain.xyz
Enter fullscreen mode Exit fullscreen mode

With these set up, localhost, example.domain.test and example.domain.xyz will all resolve to 127.0.0.1. I was suspecting performance issues with example.domain.test and I set up example.domain.xyz as an alternative mapping to see if the issue was isolated to the .test extension or also affected the .xyz one.

Next, I spun up a standard nginx docker container so there was some content being served for our test cURL request to receive.

docker run --rm -p 8081:80 nginx
Enter fullscreen mode Exit fullscreen mode

With this running, we can make a request to http://localhost:8081 or http://example.domain.test:8081 or http://example.domain.xyz:8081) and we should be presented with the default "Welcome to nginx!" page.

Now we can test if there are any DNS resolution issues by timing the requests.

Via localhost

sh-3.2$ /usr/bin/time curl -I  localhost:8081
HTTP/1.1 200 OK
Server: nginx/1.21.4
Date: Fri, 17 Dec 2021 08:32:50 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Tue, 02 Nov 2021 14:49:22 GMT
Connection: keep-alive
ETag: "61814ff2-267"
Accept-Ranges: bytes

        0.01 real         0.00 user         0.00 sys
Enter fullscreen mode Exit fullscreen mode

Via example.domain.test

sh-3.2$ /usr/bin/time curl -I  example.domain.test:8081
HTTP/1.1 200 OK
Server: nginx/1.21.4
Date: Fri, 17 Dec 2021 08:33:17 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Tue, 02 Nov 2021 14:49:22 GMT
Connection: keep-alive
ETag: "61814ff2-267"
Accept-Ranges: bytes

        5.10 real         0.00 user         0.00 sys
Enter fullscreen mode Exit fullscreen mode

Via example.domain.xyz

sh-3.2$ /usr/bin/time curl -I  example.domain.xyz:8081
HTTP/1.1 200 OK
Server: nginx/1.21.4
Date: Fri, 17 Dec 2021 08:33:42 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Tue, 02 Nov 2021 14:49:22 GMT
Connection: keep-alive
ETag: "61814ff2-267"
Accept-Ranges: bytes

        0.05 real         0.00 user         0.00 sys
Enter fullscreen mode Exit fullscreen mode

In my case, I was running this on Mac OSX and localhost and example.domain.xyz returns in a few milliseconds but example.domain.test has a 5-second delay. This turned out to be due to attempts to resolve the hostname via IPV6 and I'd only defined the IPV4 mapping in /etc/hosts file. Once I added the IPV6 mapping into my /etc/hosts file, like below,

127.0.0.1   example.domain.test
127.0.0.1   example.domain.xyz

::1 example.domain.test
Enter fullscreen mode Exit fullscreen mode

I could then see that the cURL request to example.domain.test were resolving quickly as expected.

sh-3.2$ /usr/bin/time curl -I  example.domain.test:8081
HTTP/1.1 200 OK
Server: nginx/1.21.4
Date: Fri, 17 Dec 2021 08:37:11 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Tue, 02 Nov 2021 14:49:22 GMT
Connection: keep-alive
ETag: "61814ff2-267"
Accept-Ranges: bytes

        0.01 real         0.00 user         0.00 sys
Enter fullscreen mode Exit fullscreen mode

Note: If your application is an API that is accessed via an AJAX request, you could be affected by this delay twice, once for the preflight (OPTIONS) request and again for the GET / POST request, so this change could save you around 10 seconds overall per AJAX request.

Installing PHP Opcache

The PHP docker image doesn't have opcache enabled by default. You can add this to your Dockerfile in a similar way to below

FROM php:7.4-fpm

RUN apt-get update && apt-get install opcache -y
Enter fullscreen mode Exit fullscreen mode

Unless you have specific needs, just enabling opcache should be enough

Configuring Nginx to handle OPTIONS requests

If your application is being accessed via an AJAX request then there are 2 requests that are being made. An OPTIONS (CORS) request (to see if the action is allowed) and the GET / POST / PATCH etc request (the actual action) and if your application is handling the OPTIONS request then you have a delay as a result of passing the request over to PHP and PHP booting up the framework etc to process the request.

In a development environment, you might be happy to leave your application open to all requests, in which case we can configure Nginx to handle the OPTIONS request directly and cut out your application which will speed up the request. To achieve this we need to add the following into your nginx configuration

# for OPTIONS return these headers and HTTP 200 status
if ($request_method = OPTIONS) {
    add_header Access-Control-Allow-Methods "*";
    add_header Access-Control-Allow-Headers "*";
    add_header Access-Control-Allow-Origin "*";
    return 200;
}
Enter fullscreen mode Exit fullscreen mode

For example, in our garage API case, a full server block for development might look like this.

server {
    server_tokens off;

    listen 80;
    listen [::]:80;

    root /var/www/garage-api/public;
    index index.php index.html index.htm;

    location / {
        try_files $uri $uri/ /index.php$is_args$args;
        gzip_static on;

        # for OPTIONS return these headers and HTTP 200 status
        if ($request_method = OPTIONS) {
            add_header Access-Control-Allow-Methods "*";
            add_header Access-Control-Allow-Headers "*";
            add_header Access-Control-Allow-Origin "*";
            return 200;
        }
    }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass garage-api:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
}
Enter fullscreen mode Exit fullscreen mode

Installing docker-sync

Install docker-sync using the following command

sudo gem install docker-sync
Enter fullscreen mode Exit fullscreen mode

Configure

docker-sync.yml

version: "2"
syncs:
  garage-api-sync:
    src: './application'
    sync_userid: 33
    sync_groupid: 33
Enter fullscreen mode Exit fullscreen mode

docker-compose.yml

version: '3.7'

services:
  garage-api:
    volume:
      garage-api-sync:/app:nocopy

  garage-nginx:
    image: nginx
Enter fullscreen mode Exit fullscreen mode

Spinning up

docker-sync start
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d
Enter fullscreen mode Exit fullscreen mode

Summary

Although I was looking to boost the performance of my Laravel application running inside docker, these changes are not related to Laravel and so could be applied to any other framework or PHP application but these 4 changes did have a significant impact on the performance of my application.

I focused on one particularly heavy area of the application in which had a frontend application was making 18 API calls (so 9 preflight requests + 9 GET requests) and originally this took around 32 seconds to complete all the API calls. After the changes had been put in place this dropped to around 3.5 seconds resulting in a performance boost of almost 90%.

References

Top comments (3)

Collapse
 
cavo789 profile image
Christophe Avonture

Nice article; thanks. I didn't know docker-sync but, as far I understand, this is only required for Mac users (OSX). Can you confirm ? I'm working under Windows / WSL2 and I don't have any sync issues. In my case, I suppose, docker-sync will not improve things; correct ?

Collapse
 
chrisshennan profile image
Chris Shennan

I'm aware OSX and Windows had performance issues with docker in the past and docker sync was developed in order to help remove some of those issues for OSX & Windows users.

Some of those earlier performance issues have been improved over the years as docker has improved, and as for me, I'm developing on OSX but until recently I didn't have a big enough issue with performance to require me to do anything.

If the responses you are getting from your docker project are already fairly speedy then you might not get a massive improvement by enabling docker-sync, but this might change depending on what type of project you are working on i.e. a single, self-contained application or having multiple applications running if you are developing and integration micro-services.

Collapse
 
cavo789 profile image
Christophe Avonture

Hello

I'm running a PHP codebase (big one) and a web server, redis cache, pgadmin interface and some others in my docker-compose.yml file. I've four applications like that (PHP and Apache for the others). This is a very big application splitted into smaller blocks.

As far I can see, there is no sync issues at all using Docker and Windows/WSL.

I've started to read documentations about docker-sync yesterday, I think I didn't need that one, I think like you said, Docker has made a lot of improvements for this.

Have a nice day.