Deploying a Performant PHP Application on Kubernetes with Rancher

Deploying a Performant PHP Application on Kubernetes with Rancher

Karl Hughes
Karl Hughes
Gray Calendar Icon Published: June 24, 2020
Gray Calendar Icon Updated: December 2, 2020
Read our free white paper: How to Build a Kubernetes Strategy

Introduction

PHP is one of the most popular programming languages on the web. It powers many widely used content management systems like WordPress and Drupal, and provides the backbone for modern server-side frameworks like Laravel and Symfony.

Despite its popularity, PHP has a bit of a reputation for being slow and hard to maintain. It has gotten better in recent years, but there are two features that high-performance PHP applications will likely need: OPcache and PHP FastCGI Process Manager (PHP-FPM).

In this blog post, you’ll see how to deploy a PHP application on Kubernetes with custom OPcache and PHP-FPM configurations to improve performance. You’ll use Rancher to deploy a PHP application using custom environment variables to dynamically configure OPcache and PHP-FPM. We’ll show you how to build PHP-FPM configuration options into your Docker image and adjust them using environment variables in your containers.

Performance in PHP

First, it’s helpful to understand how web requests are handled in a PHP application.

PHP is typically run alongside a web server that handles the requests and sends them through to the PHP application. You can use PHP-FPM or mod_PHP to run your application, but this walkthrough will use PHP-FPM because of the performance benefits and NGINX because it’s the most common web server used with PHP-FPM.

Image 01 Architecture of PHP/NGINX application

Once you’ve eliminated slow database queries, handled blocking TCP requests, and sorted out any poorly written code, you can make two crucial optimizations to your PHP deployment: enabling OPcache and adjusting your PHP-FPM settings.

OPcache

OPcache speeds up your PHP application by storing scripts in memory the first time they’re called. Subsequent requests will then be loaded from memory rather than the filesystem, which may give you a 74% speed boost.

OPcache offers several settings that you can adjust to improve the performance and reliability of your application. In this tutorial, you’ll see how to set up a PHP Docker image that allows you to adjust the memory limits for OPcache, the number of cached files and the cache re-validation frequency.

PHP-FPM

PHP-FPM (FastCGI Process Manager) starts one or more processes to run your PHP application. Unlike mod_PHP (which bundles PHP as an Apache module), PHP-FPM gives you granular control over the number of processes your server (or container in this case) will run and how they should be started and stopped.

Finding an ideal PHP-FPM configuration is highly dependent on your application, the number of requests it serves and the memory and CPU limits in the container. I recommend reading Hayden James’ article on the topic and then testing out several different configurations under a load testing environment.

Deploying a PHP Application on Kubernetes

Prerequisites

Before you continue with this tutorial, you should have the following:

All the code used in this walkthrough is available on Github, or you can follow the steps below to build the application from scratch.

The PHP Application

The application you’ll use is a single PHP file that displays the current date. Create a new file and name it index.php:

<?php
echo 'The current date is ' . date('F jS, Y');

Creating the Dockerfile and Configuration Files

There are many PHP Docker images available on DockerHub, but none of them include an easy way to modify your OPcache or PHP-FPM configurations using environment variables. The advantage to using environment variables is that you won’t have to rebuild your PHP image every time you want to tweak your PHP-FPM or OPcache settings. This allows you to quickly tune your application to improve performance.

First, create a new file called opcache.ini. You’ll copy this file into your PHP image and add default values for each environment variable in the Dockerfile:

# See https://www.php.net/manual/en/opcache.configuration.php for all available configuration options.
[opcache]
opcache.enable=${PHP_OPCACHE_ENABLE}
opcache.memory_consumption=${PHP_OPCACHE_MEMORY_CONSUMPTION}
opcache.max_accelerated_files=${PHP_OPCACHE_MAX_ACCELERATED_FILES}
opcache.revalidate_freq=${PHP_OPCACHE_REVALIDATE_FREQUENCY}
opcache.validate_timestamps=${PHP_OPCACHE_VALIDATE_TIMESTAMPS}

Next, create another new file called www.conf. This file will store the PHP-FPM configuration options that you will be able to update via environment variables:

; See https://www.php.net/manual/en/install.fpm.configuration.php for all available configuration options

; Required user, group, and port options
[www]
user = www-data
group = www-data
listen = 127.0.0.1:9000

; Process manager options
pm = ${PHP_FPM_PM}
pm.max_children = ${PHP_FPM_MAX_CHILDREN}
pm.start_servers = ${PHP_FPM_START_SERVERS}
pm.min_spare_servers = ${PHP_FPM_MIN_SPARE_SERVERS}
pm.max_spare_servers = ${PHP_FPM_MAX_SPARE_SERVERS}
pm.max_requests = ${PHP_FPM_MAX_REQUESTS}

You need to copy these files into your Docker image and set default environment variable values, so create a new Dockerfile in the root of your project. Add the following steps:

FROM php:7.4-fpm

# OPcache defaults
ENV PHP_OPCACHE_ENABLE="1"
ENV PHP_OPCACHE_MEMORY_CONSUMPTION="128"
ENV PHP_OPCACHE_MAX_ACCELERATED_FILES="10000"
ENV PHP_OPCACHE_REVALIDATE_FREQUENCY="0"
ENV PHP_OPCACHE_VALIDATE_TIMESTAMPS="0"

# Install opcache and add the configuration file
RUN docker-php-ext-install opcache
ADD opcache.ini "$PHP_INI_DIR/conf.d/opcache.ini"

# PHP-FPM defaults
ENV PHP_FPM_PM="dynamic"
ENV PHP_FPM_MAX_CHILDREN="5"
ENV PHP_FPM_START_SERVERS="2"
ENV PHP_FPM_MIN_SPARE_SERVERS="1"
ENV PHP_FPM_MAX_SPARE_SERVERS="2"
ENV PHP_FPM_MAX_REQUESTS="1000"

# Copy the PHP-FPM configuration file
COPY ./www.conf /usr/local/etc/php-fpm.d/www.conf

# Copy the PHP application file
COPY ./index.php /var/www/public/index.php
RUN chown -R www-data:www-data /var/www/public

This Dockerfile copies the OPCache config, PHP-FPM config, and PHP application files into the image and ensures the var/www/public directory that contains your PHP code is owned by the PHP-FPM user. The ENV declarations set default PHP_OPCACHE_... and PHP_FPM_... environment variables, but you can override them any time you run this image. That will make performance tuning much easier on a live deployment.

Building and Pushing to DockerHub

At this point, you’ve got a single-file PHP application, an OPcache configuration file, a PHP-FPM configuration file, and a Dockerfile in your project. You can now build your Docker image:

docker build -t <YOUR_USERNAME>/php-fpm .

Next, push the image to Docker Hub:

docker push <YOUR_USERNAME>/php-fpm

Deploying a PHP-FPM Workload

Now that your custom PHP-FPM image is available on Docker Hub, you can deploy it as part of a Workload on your Kubernetes cluster. Using the Rancher UI, create a new deployment, name it php-fpm, and use <YOUR_USERNAME>/php-fpm as the Docker image. You can modify any of the PHP_OPCACHE_... and PHP_FPM_... environment variables used in the Dockerfile above.

Image 02 Deploying a PHP-FPM Workload via the Rancher UI

Before setting up an Nginx Workload to serve the PHP-FPM deployment, check that your PHP-FPM and OPcache settings were properly added to the container. From the Rancher UI, click on the three dots next to your PHP deployment and then click “Execute Shell”:

Image 03 Execute shell from Rancher UI

To check that the OPcache module is enabled, type php-fpm -i. This outputs the entire PHP .ini configuration. Look through it for the section on OPcache where you should see something like this (with any values you changed reflected):

...
opcache.blacklist_filename => no value => no value
opcache.consistency_checks => 0 => 0
opcache.dups_fix => Off => Off
opcache.enable => On => On
opcache.enable_cli => Off => Off
opcache.enable_file_override => Off => Off
opcache.error_log => no value => no value
opcache.file_cache => no value => no value
opcache.file_cache_consistency_checks => 1 => 1
opcache.file_cache_only => 0 => 0
opcache.file_update_protection => 2 => 2
opcache.force_restart_timeout => 180 => 180
opcache.huge_code_pages => Off => Off
opcache.interned_strings_buffer => 8 => 8
opcache.lockfile_path => /tmp => /tmp
opcache.log_verbosity_level => 1 => 1
opcache.max_accelerated_files => 10000 => 10000
opcache.max_file_size => 0 => 0
opcache.max_wasted_percentage => 5 => 5
opcache.memory_consumption => 256 => 256
opcache.opt_debug_level => 0 => 0
opcache.optimization_level => 0x7FFEBFFF => 0x7FFEBFFF
opcache.preferred_memory_model => no value => no value
opcache.preload => no value => no value
opcache.preload_user => no value => no value
opcache.protect_memory => 0 => 0
opcache.restrict_api => no value => no value
opcache.revalidate_freq => 0 => 0
opcache.revalidate_path => Off => Off
opcache.save_comments => 1 => 1
opcache.use_cwd => On => On
opcache.validate_permission => Off => Off
opcache.validate_root => Off => Off
opcache.validate_timestamps => Off => Off
...

Whenever you redeploy your PHP-FPM Workload, PHP-FPM will restart and reset the OPcache, so you don’t typically have to worry about resetting OPcache when running PHP-FPM on Kubernetes. If you do want to refresh the cache manually, the easiest way is to redeploy the Workload from the Rancher UI.

To make sure the PHP-FPM configuration changes worked, type php-fpm -tt in the shell. You should see a list of all the PHP-FPM options including a section with the process manager updates you added to the www.conf file and set using environment variables:

NOTICE:  pm = dynamic
NOTICE:  pm.max_children = 10
NOTICE:  pm.start_servers = 2
NOTICE:  pm.min_spare_servers = 1
NOTICE:  pm.max_spare_servers = 2
NOTICE:  pm.process_idle_timeout = 10
NOTICE:  pm.max_requests = 1000

Deploying an Nginx Workload

Right now you have a PHP-FPM Workload but no web server to access it. There are a number of NGINX Docker images you can use to serve your PHP application, but I typically use this NGINX image, which allows you to use one image for any number of PHP-FPM Workloads by using an environment variable.

Create a new Workload in the Rancher UI on the same cluster as your PHP-FPM Workload. Name it nginx, use the Docker image shiphp/nginx-env, map port 80 on the container to an open port on your cluster, and add the environment variable NGINX_HOST=php-fpm:

Image 04 Creating an NGINX Workload in the Rancher UI

If you named your PHP-FPM Workload something besides php-fpm or use want to serve a second Workload, you can use the NGINX_HOST environment variable to connect to it. This also allows you to run multiple PHP-FPM and Nginx workloads on the same cluster.

Once your Nginx Workload is available, click the link to the port where it’s hosted to open up the web application. You should see the current date generated by your PHP script:

Image 05 The final PHP current date application deployed on Kubernetes

Conclusion and Next Steps

Now that you have a PHP-FPM Workload deployed to your Kubernetes cluster, you can start the real work of performance tuning. Fortunately, updating the PHP-FPM and OPcache settings is now as simple as changing the environment variables and redeploying your Workload. This will allow you to try new settings and get much faster feedback than you would by rebuilding your Docker image whenever you want to test a new setting.

Getting the best performance out of a web application is an iterative process, but hopefully, the Kubernetes deployment in this tutorial will help you build more performant PHP applications. If you have questions, feel free to reach out to me on Twitter.

Additional Resources

Read our free white paper: How to Build a Kubernetes Strategy
Karl Hughes
github
Karl Hughes
Technology Team Lead
Karl is a technology team lead and entrepreneur. He is the founder of Draft.dev, and in his free time, he runs the CFP Land newsletter for tech conference speakers.
Get started with Rancher