4 steps to improve Laravel + Docker performance issues

22nd December 2021

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

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

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

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

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

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

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

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

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;
}

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;
    }
}

Installing docker-sync

Install docker-sync using the following command

sudo gem install docker-sync

Configure

docker-sync.yml

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

docker-compose.yml

version: '3.7'

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

  garage-nginx:
    image: nginx

Spinning up

docker-sync start
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d

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

Liked this article?

Subscribe to my newsletter and get the ramblings of a wannabe #indiehacker straight in your inbox once a month. Ramblings will cover a variety of topics including:-

Web Development, DevOps, Startups, Bootstrapping, #buildinpublic, SEO, opinion and personal experience.

I won't send you spam and you can unsubscribe at any time.