Scale PHP application across multiple NginX Docker nodes

April 20, 2018

Greetings to Docker specialists and regular visitors of our blog! What should you do if your project has grown and single server can’t handle the load? What should be considered during scaling process? And how to correctly scale your high volume website?

I’ll try to disclose this topic in details and show you the best ways to solve such problems.

So, let’s imagine that your project is developed on PHP and it was dockerized before. You’re using single node for your whole stack and remote MySQL server as a database server. You’ve launched a marketing campaign today and huge wave of visitors has flown to your website, but something is wrong. Surprisingly everything began slowing down and users are experiencing bad experience. Or it can be even worse: your website started to throw errors and just stopped opening at all. Am I talking about something familiar? So let’s go!

Ideally you should use applications in Docker when they’re serverless, in other words when they’re not dependent on permanent files or data writing to the disk, so we’re able to easily launch them on any available server. But in most cases the situation is different, so let’s review the example of usage of Wordpress inside of Docker (not the best option), but it will work to provide you with clear example of how to correctly use scaling for PHP.

Let’s begin from the review of the scaling schema of PHP Docker high available cluster:

Scale PHP application across multiple NginX Docker nodes

NFS storage - we’ll use it as a shared storage for our Wordpress. We’ll carry out wp-content directory there, where plugins and static content (i.e images) is stored. Of course the best way would be to store images in some kind of CDN, but that’s totally another story.

HAproxy Load Balancer - will be used for redirecting users to the correspondent container and distribute the load accordingly.

Code container - we’ll keep Worpdress core here and run Nginx with PHP-FPM.

Cron container - this is a container with your code, but it will be used only for cron tasks execution. I’d like to draw your attention that Wordpress has several options of cron executions. First one is usage of standard CRON function, when task is executed when client appeals to the website, and second option is usage of system cron. If we’re talking about high load usage of system crons is better option, as it will reduce the load on the servers.

High Available Cluster Info

Here’s what was used in order to get started:

Two servers with Ubuntu 16.04 in Digital Ocean thedockerexperts-wp-01 & thedockerexperts-wp-02

Test domain wp.thedockerexperts.com

Installed and configured Rancher rancher.example.com

Private registry

Remote MySQL Server

Docker-ce Installation

Before the start we need to check the compatibility of Docker versions and your Rancher. When the article was written I used Rancher 1.6 and versions were checked via https://rancher.com/docs/rancher/v1.6/en/hosts/

I installed docker-ce 17.03.x-ce on all hosts:

curl https://releases.rancher.com/install-docker/17.03.sh | sh

NFS server setup

I’ve chosen thedockerexperts-wp-01 to be the NFS server:

apt-get install nfs-kernel-server

Now we’re creating directory www-data with permissions that will be shared between hosts:

mkdir -p /srv/nfs-server/wp.thedockerexperts.com/shared/wp-content
chown -R www-data:www-data /srv/nfs-server/wp.thedockerexperts.com/shared/wp-content

To limit the accesses from trusted hosts let’s edit file /etc/exports and add this string:

/srv/nfs-server/wp.thedockerexperts.com/shared 167.99.251.41(rw,sync,no_root_squash,fsid=0) 46.101.159.236(rw,sync,no_root_squash,fsid=0) 127.0.0.1(rw,sync,no_root_squash,fsid=0)

You need to change the data according to your IP addresses and dirs.

IMPORTANT! I suggest to use internal network with private IPs for NFS and also cover all of that with firewall for security reasons.

service nfs-kernel-server restart

Now we’re setting up NFS server on all servers:

apt-get install nfs-common

To check let’s mount the dir /srv/nfs-server/wp.thedockerexperts.com/shared with previously added content of wp-content:

mount -t nfs -o proto=tcp,port=2049 <nfs-server-IP>:/ /mnt

ls -la /mnt/
total 12
drwxr-xr-x  3 root     root     4096 Apr 22 19:45 .
drwxr-xr-x 23 root     root     4096 Apr 22 19:41 ..
drwxr-xr-x  4 www-data www-data 4096 Apr 22 19:48 wp-content

Build and publish Docker images

To ease the process our company has prepare ready-to-go Docker image with NginX and PHP-FPM, which is available on Docker Hub. We’ll use it for building images of cronjob container and container with your code.

Directories structure:

├── cron
│   ├── conf
│   │   ├── cron
│   │   │   └── crontab
│   │   └── supervisor
│   │       └── supervisord.conf
│   └── Dockerfile
└── lemp7
    ├── conf
    │   └── nginx
    │       └── wp.thedockerexperts.com
    ├── Dockerfile
    └── public_html
        ├── index.php
        ├── license.txt
        ├── readme.html
        ├── wp-activate.php
        ├── wp-admin
        │   ├── about.php
        │   ├── admin-ajax.php
        │   ├── admin-footer.php
        │   ├── admin-functions.php
        │   ├── admin-header.php
        │   ├── admin.php
        │   ├── admin-post.php
        │   ├── async-upload.php
        │   ├── comment.php
        │   ├── credits.php
        │   ├── css
        │   │   ├── about.css
        │   │   ├── about.min.css
        │   │   ├── about-rtl.css
        │   │   ├── about-rtl.min.css
        │   │   ├── admin-menu.css
        │   │   ├── admin-menu.min.css
        │   │   ├── admin-menu-rtl.css
        │   │   ├── admin-menu-rtl.min.css
        │   │   ├── code-editor.css
        │   │   ├── code-editor.min.css
        │   │   ├── code-editor-rtl.css
        │   │   ├── code-editor-rtl.min.css
        │   │   ├── color-picker.css
        │   │   ├── color-picker.min.css
        │   │   ├── color-picker-rtl.css
        │   │   ├── color-picker-rtl.min.css
        │   │   ├── colors
--More--

Disabling of standard wp-cron:

To disable standard wp-cron and use system one we need to add the next string to wp-config.php file:

define('DISABLE_WP_CRON', true);

NginX Docker Image

We put code of your app to NginX Docker container with PHP-FPM & excluding wp-content and called it CODEBASE:

Content of lemp7/Dockerfile:

FROM dockerexperts/nginx-php7

COPY conf/nginx/wp.thedockerexperts.com /etc/nginx/sites-enabled/

RUN mkdir -p /var/www/wp.thedockerexperts.com

COPY public_html/ /var/www/wp.thedockerexperts.com/public_html/

RUN chown -R www-data:www-data /var/www/wp.thedockerexperts.com/public_html

To configure NginX we use simple config lemp7/conf/nginx/wp.thedockerexperts.com:

upstream php {
        server 127.0.0.1:9000;
}

server {
        ## Your website name goes here.
        server_name wp.thedockerexperts.com;
        ## Your only path reference.
        root /var/www/wp.thedockerexperts.com/public_html;
        ## This should be in your http block and if it is, it's not needed here.
        index index.php;

        location = /favicon.ico {
                log_not_found off;
                access_log off;
        }

        location = /robots.txt {
                allow all;
                log_not_found off;
                access_log off;
        }

        location / {
                # This is cool because no php is touched for static content.
                # include the "?$args" part so non-default permalinks doesn't break when using query string
                try_files $uri $uri/ /index.php?$args;
        }

        location ~ \.php$ {
                #NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
                include fastcgi_params;
                fastcgi_intercept_errors on;
                fastcgi_pass php;
        }

        location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
                expires max;
                log_not_found off;
        }
}

Don’t forget that you need to change paths in Dockerfile as well as NginX configuration file according to your project’s specs.

Image building

cd lemp7
ls -la
total 20
drwxr-xr-x 4 root root 4096 Apr 22 19:58 .
drwxr-xr-x 4 root root 4096 Apr 22 19:51 ..
drwxr-xr-x 3 root root 4096 Apr 22 19:51 conf
-rw-r--r-- 1 root root  286 Apr 22 19:58 Dockerfile
drwxr-xr-x 4 root root 4096 Apr 22 19:57 public_html
docker build .

Sending build context to Docker daemon  24.33MB
Successfully built 16c0c7fef405

Image publishing

docker tag 16c0c7fef405 <your-private-registry-url>/lemp7:wptest
docker push <your-private-registry-url>/lemp7:wptest

CRON image

CRON container will use files of CODEBASE image, so all PHP modules dependencies are followed and your code is in place.

Content of cron/Dockerfile:

FROM <your-private-registry-url>/lemp7:wptest

COPY conf/supervisor/supervisord.conf /etc/supervisor/supervisord.conf
COPY conf/cron/crontab /tmp/
RUN crontab /tmp/crontab

Content of cron/conf/supervisor/supervisord.conf:

[supervisord]
nodaemon = true
logfile = /var/log/supervisord.log
logfile_maxbytes = 10MB
pidfile = /var/run/supervisord.pid

[program:rsyslog]
command=rsyslogd -n
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

[program:cron]
command = /usr/sbin/cron -f
autostart = true
autorestart = true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

[program:cron_log]
command = tail -F /var/log/syslog
stdout_events_enabled=true
stderr_events_enabled=true
stdout_logfile_maxbytes = 10MB
stdout_logfile_backups = 0

[eventlistener:stdout]
command = supervisor_stdout
buffer_size = 100
events = PROCESS_LOG
result_handler = supervisor_stdout:event_handler

Content of cron/conf/cron/crontab:

*/10 * * * * cd /var/www/wp.thedockerexperts.com/public_html; php wp-cron.php > /dev/null 2>&1

Image building

cd cron/
docker build .

Successfully built 828412c9b982

Image publishing

docker tag 828412c9b982 <your-private-registry-url>/lemp7:wpcron
docker push <your-private-registry-url>/lemp7:wpcron

Rancher

Create Base Environment

Click on the button Add environment in envs. management, which can be found by following the link in your Rancher: https://rancher.example.com/settings/env

Add Rancher Environment

Let’s enter the new environment and start addind hosts.

Rancher Environment Menu

In your environment we’ll see a notification that before container launch we need to add hosts with compatible Docker versions. You’re able to check them by following the link.

Add Hosts

Infrastructure -> Add Host

We copy the content of the 5th point and add it to the console of our servers. When installation process of rancher-agent is over, you’ll have to see similar data, as it’s shown on the below screenshot:

Successfully added hosts to rancher

Connect Private Registry

Infrastructure -> Registry -> Add registry

Add private registry to rancher

Stacks and services

Now when everything is ready, we can start adding services to Rancher.

NFS Rancher

Go to environment -> Catalog - let’s use the search to find NFS.

Click on View Details and fill in the fields:

NFS Server: <nfs-server-IP>

Export Base Directory: /

On Remove: Retain

We’ll see the next picture after the launch:

Successfully added nfs service

Now we’re adding volume. Let’s proceed in Infrastructure -> Storage -> Add Volume:

Name: wp-content

MyScalableWP stack

We need to add stack, where we’ll launch our services. Keep in mind that wp-content needs to be connected from NFS.

Add new stack to rancher
Service configuration
Add core service to rancher
Volumes configuration
Mount nfs volume to service container
MySQL External Service

Let’s add MySQL as external service for convenience:

Mysql as external service

Load balancing

We need to schedule Load Balancer to 2 hosts, where we add balancer=true labels. This menu is available while editing hosts.

Load Balancer service
Load balancer in rancher

DNS records

nslookup wp.thedockerexperts.com
Server:		67.207.67.3
Address:	67.207.67.3#53

Non-authoritative answer:
Name:	wp.thedockerexperts.com
Address: 167.99.251.41
Name:	wp.thedockerexperts.com
Address: 46.101.159.236

CRON service

To deploy cron container we need to add new service:

Cron service in rancher
Volumes
Cron service volumes

Testing

Let’s try to install W3 Total Cache WP plugin:

Test installation of the WP plugin

We see that plugin was installed in shared dir:

[email protected]:~/mywp/lemp7# ls -la /srv/nfs-server/wp.thedockerexperts.com/shared/wp-content/plugins/
total 52
drwxr-xr-x 4 www-data www-data  4096 Apr 22 20:40 .
drwxr-xr-x 5 www-data www-data  4096 Apr 22 20:40 ..
drwxr-xr-x 4 www-data www-data  4096 Apr  3 20:19 akismet
-rw-r--r-- 1 www-data www-data  2230 Mar 17 20:27 hello.php
-rw-r--r-- 1 www-data www-data    28 Jun  5  2014 index.php
drwxr-xr-x 9 www-data www-data 28672 Apr 22 20:40 w3-total-cache

Let’s try to add one more host.

Don’t forget that we need to update /etc/exports file on the NFS server and add new host.

Cluster scaling with test host

Conslusions

In this blog post I tried to show you how you can use Docker for scaling of your PHP project in high available cluster. Wordpress was used just for example’s clear visibility. However something is not considered in this article and you should always remember that, when you’re ready to scale your project.

  1. NFS is a bottleneck in this case. And for HA infrastructure it doesn’t fit really well, because if NFS server goes down your whole project will be down. Also the speed of NFS doesn’t always fit for production mode - consider CDN to serve static content. Alternative for NFS is GLusterFS, it’s more appropriate for HA but the data transfer speed won’t be really high if we’re comparing it with regular local disks.

  2. Take into consideration where your sessions are stored. In Worpdress case it’s not actual, but with other PHP projects you’ll need to put them to shared storage. In my opinion the best option here would be Redis or database.

  3. Don’t forget about the size of your docker images. Always try to cut your image, so the time spent for its build and deploy is reduced.

I’ll be really glad to see any kind of comments, questions and suggestions!

comments powered by Disqus