Automating WordPress Health Checks with WP-CLI doctor Command

Your WordPress or WooCommerce store's health is absolutely critical nowadays both in terms of performance and security. Having a slow site or one that has been defaced does not exactly demonstrate technical proficiency and inspire trust from your visitors. Generally I like to be proactive with performance and security rather than re-active which means we find out about a website health problem before visitors do and fix it ASAP!

In this tutorial we will go over WordPress health check automation using the Linux command line with the WP-CLI doctor command. You can also read about how to use WP-CLI's doctor command to clean the wp_options table of unnecessary bloated autoloaded data.

Install WP-CLI Doctor Command

Installing doctor command from the github repository, note this does require composer is installed on the host.

wp package install wp-cli/doctor-command --allow-root

You should see some output like this showing the doctor command installed

Installing package wp-cli/doctor-command (dev-master)
Updating /root/.wp-cli/packages/composer.json to require the package...
Using Composer to install the package...
---
Loading composer repositories with package information
Updating dependencies
Resolving dependencies through SAT
Dependency resolution completed in 0.137 seconds
Analyzed 5360 packages to resolve dependencies
Analyzed 311678 rules to resolve dependencies
Package operations: 1 install, 0 updates, 0 removals
Installs: wp-cli/doctor-command:dev-master 9656de3
 - Installing wp-cli/doctor-command (dev-master 9656de3)
Writing lock file
Generating autoload files
---
Success: Package installed.

Here is a list of all of the doctor commands

wp doctor list --allow-root

Here they are!

+----------------------------+--------------------------------------------------------------------------------+
| name                       | description                                                                    |
+----------------------------+--------------------------------------------------------------------------------+
| autoload-options-size      | Warns when autoloaded options size exceeds threshold of 900 kb.                |
| constant-savequeries-falsy | Confirms expected state of the SAVEQUERIES constant.                           |
| constant-wp-debug-falsy    | Confirms expected state of the WP_DEBUG constant.                              |
| core-update                | Errors when new WordPress minor release is available; warns for major release. |
| core-verify-checksums      | Verifies WordPress files against published checksums; errors on failure.       |
| cron-count                 | Errors when there's an excess of 50 total cron jobs registered.                |
| cron-duplicates            | Errors when there's an excess of 10 duplicate cron jobs registered.            |
| file-eval                  | Checks files on the filesystem for regex pattern `eval\(.*base64_decode\(.*`.  |
| option-blog-public         | Confirms the expected value of the 'blog_public' option.                       |
| plugin-active-count        | Warns when there are greater than 80 plugins activated.                        |
| plugin-deactivated         | Warns when greater than 40% of plugins are deactivated.                        |
| plugin-update              | Warns when there are plugin updates available.                                 |
| theme-update               | Warns when there are theme updates available.                                  |
| cache-flush                | Detects the number of occurrences of the `wp_cache_flush()` function.          |
| php-in-upload              | Warns when a PHP file is present in the Uploads folder.                        |
| language-update            | Warns when there are language updates available.                               |
+----------------------------+--------------------------------------------------------------------------------+

We can run all of these health checks or just do a selection of them.

Run All WP-CLI doctor Health Checks

Here is how to run just one WP-CLI doctor check

wp doctor check core-update --allow-root

Here is a sample output showing the WordPress version is current.

+-------------+---------+-------------------------------------+
| name        | status  | message                             |
+-------------+---------+-------------------------------------+
| core-update | success | WordPress is at the latest version. |
+-------------+---------+-------------------------------------+

You can run all of the WP-CLI doctor command health checks with this command

wp doctor check --all --allow-root

In this example you can see that there are some PHP warnings which are a good idea to document and fix afterwards.

Running checks  6  % [============>                                                                                                                                                                                             ] 0:00 / 0:00PHP Warning:  file_get_contents(/var/www/wp-bullet.online/wp-content/db.php): failed to open stream: No such file or directory in /root/.wp-cli/packages/vendor/wp-cli/doctor-command/inc/checks/class-file-contents.php on line 66
Running checks  12 % [=========================>                                                                                                                                                                                ] 0:01 / 0:15PHP Warning:  file_get_contents(/var/www/wp-bullet.online/wp-content/db.php): failed to open stream: No such file or directory in /root/.wp-cli/packages/vendor/wp-cli/doctor-command/inc/checks/class-file-contents.php on line 66
Running checks  100% [==========================================================================================================================================================================================================] 0:12 / 0:10
Error: 4 checks report 'error'.
Warning: count(): Parameter must be an array or an object that implements Countable in /var/www/wp-bullet.online/xhprof/xhprof_lib/utils/favicon_ef3846.ico(104) : eval()'d code(165) : eval()'d code(196) : eval()'d code on line 200
Warning: array_splice() expects parameter 1 to be array, string given in /var/www/wp-bullet.online/xhprof/xhprof_lib/utils/favicon_ef3846.ico(104) : eval()'d code(165) : eval()'d code(196) : eval()'d code on line 206
Warning: Invalid argument supplied for foreach() in /var/www/wp-bullet.online/xhprof/xhprof_lib/utils/favicon_ef3846.ico(104) : eval()'d code(165) : eval()'d code(196) : eval()'d code on line 207
+----------------------------+---------+--------------------------------------------------------------------+
| name                       | status  | message                                                            |
+----------------------------+---------+--------------------------------------------------------------------+
| core-verify-checksums      | error   | WordPress doesn't verify against its checksums.                    |
| file-eval                  | error   | 1 'php' file failed check for 'eval\(.*base64_decode\(.*'.         |
| cache-flush                | warning | Use of wp_cache_flush() detected.                                  |
| autoload-options-size      | success | Autoloaded options size (164.73kb) is less than threshold (900kb). |
| constant-savequeries-falsy | success | Constant 'SAVEQUERIES' is undefined.                               |
| constant-wp-debug-falsy    | error   | Constant 'WP_DEBUG' is defined 'true' but expected to be falsy.    |
| core-update                | success | WordPress is at the latest version.                                |
| cron-count                 | success | Total number of cron jobs is within normal operating expectations. |
| cron-duplicates            | success | All cron job counts are within normal operating expectations.      |
| option-blog-public         | error   | Site is private but expected to be public.                         |
| plugin-active-count        | success | Number of active plugins (2) is less than threshold (80).          |
| plugin-deactivated         | warning | Greater than 40 percent of plugins are deactivated.                |
| plugin-update              | warning | 1 plugin has an update available.                                  |
| theme-update               | warning | 1 theme has an update available.                                   |
| php-in-upload              | warning | PHP files detected in the Uploads folder.                          |
| language-update            | success | Languages are up to date.                                          |
+----------------------------+---------+--------------------------------------------------------------------+

Here is output from the doctor command with some warnings to address like excessive calling of wp_cache_flush(), plugins to update and PHP files in the upload folder which be a potential security risk.

Running checks  100% [========================================================================================================================================================] 0:09 / 0:10
+----------------------------+---------+--------------------------------------------------------------------+
| name                       | status  | message                                                            |
+----------------------------+---------+--------------------------------------------------------------------+
| core-verify-checksums      | success | WordPress verifies against its checksums.                          |
| file-eval                  | success | All 'php' files passed check for 'eval\(.*base64_decode\(.*'.      |
| cache-flush                | warning | Use of wp_cache_flush() detected.                                  |
| autoload-options-size      | success | Autoloaded options size (91.39kb) is less than threshold (900kb).  |
| constant-savequeries-falsy | success | Constant 'SAVEQUERIES' is undefined.                               |
| constant-wp-debug-falsy    | success | Constant 'WP_DEBUG' is defined falsy.                              |
| core-update                | success | WordPress is at the latest version.                                |
| cron-count                 | success | Total number of cron jobs is within normal operating expectations. |
| cron-duplicates            | success | All cron job counts are within normal operating expectations.      |
| option-blog-public         | success | Site is public as expected.                                        |
| plugin-active-count        | success | Number of active plugins (28) is less than threshold (80).         |
| plugin-deactivated         | success | Less than 40 percent of plugins are deactivated.                   |
| plugin-update              | warning | 2 plugins have updates available.                                  |
| theme-update               | success | Themes are up to date.                                             |
| php-in-upload              | warning | PHP files detected in the Uploads folder.                          |
| language-update            | success | Languages are up to date.                                          |
+----------------------------+---------+--------------------------------------------------------------------+

Here is sample output from a site that shows only wp_cache_flush() was detected.

+----------------------------+---------+--------------------------------------------------------------------+
| name                       | status  | message                                                            |
+----------------------------+---------+--------------------------------------------------------------------+
| core-verify-checksums      | success | WordPress verifies against its checksums.                          |
| file-eval                  | success | All 'php' files passed check for 'eval\(.*base64_decode\(.*'.      |
| cache-flush                | warning | Use of wp_cache_flush() detected.                                  |
| autoload-options-size      | success | Autoloaded options size (274.06kb) is less than threshold (900kb). |
| constant-savequeries-falsy | success | Constant 'SAVEQUERIES' is undefined.                               |
| constant-wp-debug-falsy    | success | Constant 'WP_DEBUG' is defined falsy.                              |
| core-update                | success | WordPress is at the latest version.                                |
| cron-count                 | success | Total number of cron jobs is within normal operating expectations. |
| cron-duplicates            | success | All cron job counts are within normal operating expectations.      |
| option-blog-public         | success | Site is public as expected.                                        |
| plugin-active-count        | success | Number of active plugins (52) is less than threshold (80).         |
| plugin-deactivated         | success | Less than 40 percent of plugins are deactivated.                   |
| plugin-update              | success | Plugins are up to date.                                            |
| theme-update               | success | Themes are up to date.                                             |
| php-in-upload              | success | No PHP files found in the Uploads folder.                          |
| language-update            | success | Languages are up to date.                                          |
+----------------------------+---------+--------------------------------------------------------------------+

We can also show only the failures in the next section below

Automation Batch Processing

To only show the errors and warnings from the doctor check add the --spotlight flag.

wp doctor check --all --spotlight --allow-root

Now we only see the errors and warnings

+-----------------------+---------+------------------------------------------------------------+
| name                  | status  | message                                                    |
+-----------------------+---------+------------------------------------------------------------+
| cache-flush           | warning | Use of wp_cache_flush() detected.                          |
| autoload-options-size | warning | Autoloaded options size (1.46mb) exceeds threshold (900kb) |
| cron-count            | error   | Total number of cron jobs exceeds expected threshold.      |
| plugin-update         | warning | 5 plugins have updates available.                          |
| php-in-upload         | warning | PHP files detected in the Uploads folder.                  |
+-----------------------+---------+------------------------------------------------------------+
Error: 1 check reports 'error'.

Here is another sample from another site.

+-----------------------+---------+------------------------------------------------------------+
| name                  | status  | message                                                    |
+-----------------------+---------+------------------------------------------------------------+
| cache-flush           | warning | Use of wp_cache_flush() detected.                          |
| autoload-options-size | warning | Autoloaded options size (1.73mb) exceeds threshold (900kb) |
| option-blog-public    | error   | Site is private but expected to be public.                 |
| plugin-update         | warning | 6 plugins have updates available.                          |
| php-in-upload         | warning | PHP files detected in the Uploads folder.                  |
+-----------------------+---------+------------------------------------------------------------+
Error: 1 check reports 'error'.

Now we can move on to doing selective WP-CLI doctor health checks.

Run Custom List of WP-CLI doctor Health Checks

Here are some of my favorite doctor checks to run on their own.

The autoload options size check is a good one to run (see here for how to clean up the autoloaded data)

wp doctor check autoload-options-size --allow-root

You will get a pretty human readable output about the amount of autoloaded data

+-----------------------+---------+------------------------------------------------------------+
| name                  | status  | message                                                    |
+-----------------------+---------+------------------------------------------------------------+
| autoload-options-size | warning | Autoloaded options size (1.73mb) exceeds threshold (900kb) |
+-----------------------+---------+------------------------------------------------------------+

you can read this post on how to clean the options table

Checking if the SAVEQUERIES constant is defined is important because having it enabled can cause performance issues.

wp doctor check constant-savequeries-falsy --allow-root

You want this undefined unless you are doing some development debugging work.

+----------------------------+---------+--------------------------------------+
| name                       | status  | message                              |
+----------------------------+---------+--------------------------------------+
| constant-savequeries-falsy | success | Constant 'SAVEQUERIES' is undefined. |
+----------------------------+---------+--------------------------------------+

Having WP_DEBUG enabled constantly is not a good idea!

wp doctor check constant-wp-debug-falsy

You want WP_DEBUG disabled unless you are actively debugging an issue.

+-------------------------+---------+---------------------------------------+
| name                    | status  | message                               |
+-------------------------+---------+---------------------------------------+
| constant-wp-debug-falsy | success | Constant 'WP_DEBUG' is defined falsy. |
+-------------------------+---------+---------------------------------------+

Check WordPress core is up to date

wp doctor check core-update --allow-root

It should be the latest version to ensure all security patches are updated.

+-------------+---------+-------------------------------------+
| name        | status  | message                             |
+-------------+---------+-------------------------------------+
| core-update | success | WordPress is at the latest version. |
+-------------+---------+-------------------------------------+

You can also check the WordPress core version with the wp core command

wp core version --allow-root

Output

5.3.2

Check the number of scheduled tasks in wp-cron

wp doctor check cron-count --allow-root

If the nubmer is too high it will complain, this generally means a broken wp-cron which needs to be fixed.

+------------+---------+--------------------------------------------------------------------+
| name       | status  | message                                                            |
+------------+---------+--------------------------------------------------------------------+
| cron-count | success | Total number of cron jobs is within normal operating expectations. |
+------------+---------+--------------------------------------------------------------------+

It is a good idea to check the cron jobs in wp-cron as well

wp cron event list --allow-root

There should be no cronjobs from plugins which have been deleted or deactivated, there should also be no duplicates.

+------------------------------------+---------------------+-----------------------+------------+
| hook                               | next_run_gmt        | next_run_relative     | recurrence |
+------------------------------------+---------------------+-----------------------+------------+
| ao_ccss_queue                      | 2020-01-26 04:55:00 | 3 minutes 51 seconds  | 10 minutes |
| wp_privacy_delete_old_export_files | 2020-01-26 05:37:50 | 46 minutes 41 seconds | 1 hour     |
| prli_cleanup_visitor_locks_worker  | 2020-01-26 05:48:46 | 57 minutes 37 seconds | 1 hour     |
| ao_ccss_maintenance                | 2020-01-26 11:29:17 | 6 hours 38 minutes    | 12 hours   |
| mwp_update_public_keys             | 2020-01-26 12:41:40 | 7 hours 50 minutes    | 1 day      |
| ao_cachechecker                    | 2020-01-26 13:25:05 | 8 hours 33 minutes    | 12 hours   |
| wp_version_check                   | 2020-01-26 16:49:06 | 11 hours 57 minutes   | 12 hours   |
| wp_update_plugins                  | 2020-01-26 16:49:06 | 11 hours 57 minutes   | 12 hours   |
| wp_update_themes                   | 2020-01-26 16:49:06 | 11 hours 57 minutes   | 12 hours   |
| recovery_mode_clean_expired_keys   | 2020-01-26 18:28:38 | 13 hours 37 minutes   | 1 day      |
| wpp_cache_event                    | 2020-01-27 00:00:00 | 19 hours 8 minutes    | 1 day      |
| wp_scheduled_auto_draft_delete     | 2020-01-27 02:30:53 | 21 hours 39 minutes   | 1 day      |
| delete_expired_transients          | 2020-01-27 03:41:10 | 22 hours 50 minutes   | 1 day      |
| wpseo-reindex-links                | 2020-01-27 04:49:06 | 23 hours 57 minutes   | 1 day      |
| wp_scheduled_delete                | 2020-01-27 04:49:06 | 23 hours 57 minutes   | 1 day      |
+------------------------------------+---------------------+-----------------------+------------+

You can check for duplicate cron jobs in WordPress too

wp doctor check cron-duplicates --allow-root

You should have no duplicate cron jobs for your site

+-----------------+---------+---------------------------------------------------------------+
| name            | status  | message                                                       |
+-----------------+---------+---------------------------------------------------------------+
| cron-duplicates | success | All cron job counts are within normal operating expectations. |
+-----------------+---------+---------------------------------------------------------------+

If you do get an error that there are duplicate cronjobs you can use this command to find the duplicates

wp cron event list --field=hook --allow-root | sort | uniq -d

On this particular installation the theme was adding an extra wp_update_themes cronjob

wp_update_themes

If you are on a production site then the blog_public option should be 1 or Google will not index the site leading to SEO issues and your site disappearing from Google.

wp doctor check option-blog-public --allow-root

You will get a notice that if the blog_public option is set to 0 which is fine if you are on a staging site but not on production!

Error: 1 check reports 'error'.
+--------------------+--------+--------------------------------------------+
| name               | status | message                                    |
+--------------------+--------+--------------------------------------------+
| option-blog-public | error  | Site is private but expected to be public. |
+--------------------+--------+--------------------------------------------+

You can make the site index-able by Google and other search engines again by changing the blog_public option back to 1.

wp option update blog_public 1 --skip-plugins --skip-themes --allow-root

Check the number of active plugins

wp doctor check plugin-active-count --allow-root

The threshold is 80 active plugins

+---------------------+---------+------------------------------------------------------------+
| name                | status  | message                                                    |
+---------------------+---------+------------------------------------------------------------+
| plugin-active-count | success | Number of active plugins (34) is less than threshold (80). |
+---------------------+---------+------------------------------------------------------------+

Deactivated WordPress and WooCommerce plugins pose a security risk so that is why this check is included.

wp doctor check plugin-deactivated --allow-root

You should always be vigilant of keeping too many deactivated plugins on a site.

+--------------------+---------+--------------------------------------------------+
| name               | status  | message                                          |
+--------------------+---------+--------------------------------------------------+
| plugin-deactivated | success | Less than 40 percent of plugins are deactivated. |
+--------------------+---------+--------------------------------------------------+

Check how many plugins have updates available

wp doctor check plugin-update --allow-root

Make sure to test your updates on a staging site first 🙂

+---------------+---------+-----------------------------------+
| name          | status  | message                           |
+---------------+---------+-----------------------------------+
| plugin-update | warning | 5 plugins have updates available. |
+---------------+---------+-----------------------------------+

Check if any theme updates are available.

wp doctor check theme-update --allow-root

Outdated themes can pose a security threat. Note also that some premium themes do not have the updater mechanism working correctly so be sure to check the premium them's homepage to be certain there is not a new release available.

+--------------+---------+------------------------+
| name         | status  | message                |
+--------------+---------+------------------------+
| theme-update | success | Themes are up to date. |
+--------------+---------+------------------------+

WordPress has internal caches it uses to speed up sites , this command checks if any plugin or theme code is flushing the cache.

wp doctor check cache-flush --allow-root

This is something to investigate further and make sure that the cache is only flushed under necessary conditions and not excessively.

+-------------+---------+-----------------------------------+
| name        | status  | message                           |
+-------------+---------+-----------------------------------+
| cache-flush | warning | Use of wp_cache_flush() detected. |
+-------------+---------+-----------------------------------+

You can check which code may be calling the cache_flush function with grep.

grep -ril cache_flush wp-content/plugins
grep -ril cache_flush wp-content/mu-plugins
grep -ril cache_flush wp-content/themes/$(wp theme list --status=active --field=name --allow-root)

Checking for PHP files in the upload folder is important too

wp doctor check php-in-upload

You will want to make sure any PHP files in the wp-content/uploads folder are supposed to be there!

+---------------+---------+-------------------------------------------+
| name          | status  | message                                   |
+---------------+---------+-------------------------------------------+
| php-in-upload | warning | PHP files detected in the Uploads folder. |
+---------------+---------+-------------------------------------------+
This find command will help find the PHP files in your wp-content/uploads folder.
cd wp-content/uploads
find . -type f -iname "*.php"

Here is my custom doctor command list for running multiple checks at once.

The file-eval and php-in-upload checks can be quite time consuming if the uploads folder is huge, here is a list that tends to go much faster

wp doctor check autoload-options-size constant-savequeries-falsy constant-wp-debug-falsy core-update core-verify-checksums cron-count cron-duplicates option-blog-public plugin-active-count plugin-deactivated plugin-update theme-update cache-flush language-update

Here is the output showing the report from the above doctor command.

Error: 2 checks report 'error'.
+----------------------------+---------+--------------------------------------------------------------------+
| name                       | status  | message                                                            |
+----------------------------+---------+--------------------------------------------------------------------+
| core-verify-checksums      | error   | WordPress doesn't verify against its checksums.                    |
| cache-flush                | warning | Use of wp_cache_flush() detected.                                  |
| autoload-options-size      | warning | Autoloaded options size (1.37mb) exceeds threshold (900kb)         |
| constant-savequeries-falsy | success | Constant 'SAVEQUERIES' is undefined.                               |
| constant-wp-debug-falsy    | success | Constant 'WP_DEBUG' is defined falsy.                              |
| core-update                | success | WordPress is at the latest version.                                |
| cron-count                 | success | Total number of cron jobs is within normal operating expectations. |
| cron-duplicates            | success | All cron job counts are within normal operating expectations.      |
| option-blog-public         | error   | Site is private but expected to be public.                         |
| plugin-active-count        | success | Number of active plugins (51) is less than threshold (80).         |
| plugin-deactivated         | success | Less than 40 percent of plugins are deactivated.                   |
| plugin-update              | warning | 4 plugins have updates available.                                  |
| theme-update               | warning | 1 theme has an update available.                                   |
| language-update            | success | Languages are up to date.                                          |
+----------------------------+---------+--------------------------------------------------------------------+

Automation with Notifications

You can also create your own custom doctor check automation scripts with YAML (these are from the github page).
Here is a sample doctor.yml file specifying to check the W3 Total Cache plugin is installed

plugin-w3-total-cache:
  check: Plugin_Status
  options:
    name: w3-total-cache
    status: uninstalled

You can now specify the custom doctor.yml file using the --config parameter:

wp doctor check --fields=name,status --all --config=doctor.yml --allow-root

If your custom doctor check fails you will get an error

+-----------------------+--------+
| name                  | status |
+-----------------------+--------+
| plugin-w3-total-cache | error  |
+-----------------------+--------+

You can do something similar for finding out if somebody turned the blog_public option off and get alerted via email.

Sources

Customize Doctor Config
wp cron event command
Find duplicate entries in text file