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
volumes:
garage-api-sync:
external: true
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
- Docker Sync documentation - https://docker-sync.readthedocs.io/en/latest/index.html
Originally published at https://chrisshennan.com/blog/4-steps-to-improve-laravel-docker-performance-issues