PHP / Server Setup

CentOS/RHEL: Faster PHP CLI and Cron Jobs

by , , revisited on


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

Ingredients of a sane and fast PHP setup

1. A website has a website user

You surely created a separate PHP-FPM pool user for your website.
You use that same user to set up your cron job with.

2. File-based PHP OPcache

Thanks to the file-based OPcache, we can run faster PHP cron jobs and significantly reduce the CPU usage.

The problems

A website typically needs many cron jobs.
Example of an ugly cron job table for a website

@daily /path/to/php -d memory_limit=-1 -d opcache.file_cache_only=1 -d opcache.enable_cli=1 -d opcache.file_cache=/home/username/.cache/opcache /path/to/cron.php
@daily /path/to/php -d memory_limit=-1 -d opcache.file_cache_only=1 -d opcache.enable_cli=1 -d opcache.file_cache=/home/username/.cache/opcache /path/to/cron-other.php

Hardcoding file-based OPcache directory

We hardcoded OPcache directory, e.g. /home/username/.cache/opcache in every cron job…
And while running PHP in CLI, we also have to specify it in order to benefit from faster PHP.

Hardcoding memory_limit setting

You absolutely want a sane memory_limit applied to all your PHP-FPM pools. This allows you to find faulty scripts easily which unnecessarily eat up your RAM.
But you typically want unlimited or much larger memory allocation towards your PHP runs on the CLI. That means having to provide -d memory_limit=-1 flag, for unlimited case, every time. Not good also.

Hardcoding PHP version

Furthermore, different sites may run different PHP versions via different PHP-FPM pools.
One site may be using PHP 8.0 binary under /opt/remi....
The other, system’s default PHP 7.2 binary at /usr/bin/php.
Running things in CLI means having to remember which site uses which PHP version… And having to hardcode PHP binary paths in cron jobs or while running PHP in CLI… Bad.

So overall, you can see from there, there’s a lot of repetitive hardcoding. Moreover, when you want to invoke something interactively, it is quite common to simply type php which is prone to invoking the wrong version of PHP interpreter and not making benefit from file-based OPcache.

But there’s an easy fix.

Setting up great PHP CLI and cron jobs

Now it’s time for some elegant and consistent setup where we reduce hardcoding to the minimum and make everything clean and nice.

For each PHP version, we can create a launcher with the necessary settings. A launcher is actually a bash wrapper around actual php. It will invoke the correct actual php binary with the correct/necessary arguments.

Create directory /opt/php-cli/system/bin and php file inside it with contents:

#!/bin/bash
/usr/bin/php -d memory_limit=-1 -d opcache.enable_cli=1 -d opcache.file_cache=$HOME/.cache/opcache "$@"

Noticed anything special? We hardcoded all our paths in this binary. And this is the only time we ever hardcode anything, per PHP version.
The "$@" ensures that we pass whichever arguments our wrapper accepts, into an actual php invocation.

Make it executable. Now symlink /opt/php-cli/system/bin/php to ~/.local/bin/php of every user that will run the PHP version that is default for the system.
Log in with a website user, and run:

ln -s /opt/php-cli/system/bin/php ~/.local/bin/php

For every non-default version, e.g. if you have a 7.4 software collection from Remi, create another launcher, e.g. /opt/php-cli/7.4/bin/php would have these contents:

#!/bin/bash
/opt/remi/php74/root/usr/bin/php -d memory_limit=-1 -d opcache.enable_cli=1 -d opcache.file_cache=$HOME/.cache/opcache "$@"

And similarly symlink /opt/php-cli/7.4/bin/php to ~/.local/bin/php of the users who run PHP using this version of interpreter.

Now our cron jobs have to ensure that ~/.local/bin is in cron’s PATH by adding PATH=$HOME/.local/bin:/usr/bin:/bin.
This allows us to simply put php in every cron entry for the site:

PATH=/home/foo/.local/bin:/usr/bin:/bin
@daily php /path/to/cron.php
@daily php /path/to/cron-other.php

Note that we must use a literal PATH value for user foo, because the environment variables specification in crontab does not allow using variables in the assigned value.

While we reached our goal of reducing duplication, we have a hardcoded username in a crontab. Not a problem if you use Ansible for setting up servers.
Otherwise, see the next section.

If you want to avoid hardcoding the username in crontab and reduce hardcoding of PHP version …

Simply set the path to a launcher’s bin directory instead of ~/.local/bin, e.g.:

PATH=/opt/php-cli/system/bin:/usr/bin:/bin

CLI

Now, as for the CLI, ensure that ~/.local/bin is in your website user’s PATH and has priority over other paths.

Simply ensure that there’s no PATH setting in your ~/.bash_profile and the following is in your ~/.bashrc:

# User specific environment
if ! [[ "$PATH" =~ "$HOME/.local/bin:$HOME/bin:" ]]; then
    PATH="$HOME/.local/bin:$HOME/bin:$PATH"
fi
export PATH

For a detailed explanation about it, see CentOS/RHEL: where is my PATH?.

This way of setting ensures that ~/.local/bin is on the right-side from $PATH, and thus, /usr/bin/php would be looked up first.

After changes to your ~/.bashrc, you may wish to log out and back into the server in order to update your PATH now. This is preferred over running source ~/.bashrc because that might create duplicates in the PATH.

Now typing just php magically becomes that faster and less error-prone.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: