Using Composer to Manage Plugins and Deploy to WP Engine

Damon Cook Avatar


Recently we went over how to do branched deploys to WP Engine with GitHub Actions. Today, I would like to dive a bit deeper into managing plugin dependencies with Composer when deploying to WP Engine.

We’ll assume you have some basic familiarity with Composer and WPackagist, but if you don’t, here is an excellent resource to get you started: Managing your WordPress site with Git and Composer.

Composer deployment demo source code

Check out the full source code for the final demo project on GitHub.

Here is what we’ll be covering:

Overview of project organization

Here is an overview of how the site is currently organized in our WordPress install’s wp-content/.

Current WordPress wp-content/Available on WPackagist?.gitignore?
wp-content/plugins/advanced-custom-fields-proNo, grab from URLYes
(custom plugin)
(custom theme)
Comparing the current WordPress folder structure vs. what we want to version control

We’ll reference the table above to create our .gitignore file. Note: we’re not putting other standard WordPress directories within the wp-content/ directory under version control, e.g. wp-content/uploads, wp-content/upgrade, wp-content/mu-plugins, because we want to keep things lean.

.gitignore (full source)

#--------------------------- # WordPress general #--------------------------- /index.php #--------------------------- # WordPress themes #--------------------------- /themes/* !/themes/custom-theme #--------------------------- # WordPress plugins #--------------------------- /plugins/* !/plugins/custom-plugin #--------------------------- # WP MU plugins: these are # managed by the platform. #--------------------------- /mu-plugins/ #--------------------------- # WP uploads directory #--------------------------- /uploads/ #--------------------------- # WP upgrade files #--------------------------- /upgrade/ #--------------------------- # Composer #--------------------------- /vendor .env .env.* !.env.example
Code language: plaintext (plaintext)

In the snippet above, we’ve added the items we’ll want to ignore that are associated with our Composer setup (last few lines). Let’s dig in on that next.

Set up Composer

First, we’ll want to pull in the pragmatic composer/installers package. We’ll use this Composer package to set our custom paths where we want our WordPress plugins installed by Composer.

composer.jsonwith composer/installers package

{ "name": "wpe/demo-project", "type": "project", "require": { "composer/installers": "~1.0" }, "extra": { "installer-paths": { "plugins/{$name}": [ "type:wordpress-plugin" ] } } }
Code language: JSON / JSON with Comments (json)

In the code snippet above, we require composer/installers, set installer-paths for any Composer dependencies with the "type:wordpress-plugin" to be installed in the plugins/ directory, and we pass the {$name} of the plugin. We won’t see much progress if we run composer install at this point, but composer/installers should be installed.

WPackagist set up in Composer

Next, we’ll want to set up our WPackagist integration to pull in our WordPress project’s plugins.

composer.jsonwith WPackagist plugins required

{ "name": "wpe/demo-project", "type": "project", "repositories": [ { "type": "composer", "url": "" } ], "require": { "composer/installers": "~1.0", "wpackagist-plugin/duplicate-post": "^4.5", "wpackagist-plugin/svg-support": "^2.5.1", "wpackagist-plugin/wordpress-seo": "^19.10" }, "extra": { "installer-paths": { "plugins/{$name}": [ "type:wordpress-plugin" ] } } }
Code language: JSON / JSON with Comments (json)

To pull in plugins from WPackagist, we have to tell Composer to reference WPackagist as a package source, which is accomplished with the repositories key. Then all we have to do is require all of our WPackagist plugins.

ACF PRO set up in Composer

ACF PRO requires a license key, but that is no sweat, thanks to Fränz Friederes’ private-composer-installer Composer package. This package allows us to reference a URL and pass along a variable for our secret license key. The emphasis here is secret. We don’t want to store our license key anywhere that is publicly available. We’ll keep our license key in a .env file. The .env file is a standard way to store environmental variables, which should exist outside the code.

composer.jsonwith newly added ACF PRO integration

{ "name": "wpe/demo-project", "type": "project", "repositories": [ { "type": "package", "package": { "name": "advanced-custom-fields/advanced-custom-fields-pro", "version": "6.0.5", "type": "wordpress-plugin", "dist": { "type": "zip", "url": "{%PLUGIN_ACF_KEY}&t={%VERSION}" } } }, { "type": "composer", "url": "" } ], "require": { "composer/installers": "~1.0", "ffraenz/private-composer-installer": "^5.0", "wpackagist-plugin/duplicate-post": "^4.5", "wpackagist-plugin/svg-support": "^2.5.1", "wpackagist-plugin/wordpress-seo": "^19.10", "advanced-custom-fields/advanced-custom-fields-pro": "*" }, "extra": { "installer-paths": { "plugins/{$name}": [ "type:wordpress-plugin" ] }, "private-composer-installer": { "dotenv-path": ".", "dotenv-name": ".env" } } }
Code language: JSON / JSON with Comments (json)

As you can see, we’ve added the ACF PRO download URL as a reference point in our repositories key. We’ve also added the ffraenz/private-composer-installer and advanced-custom-fields/advanced-custom-fields-pro as required packages. Last, we tell the private-composer-installer where we want our .env file to reside in relation to our composer.json file within our project.

You can check out the full demo project’s composer.json here.

Create .env file with ACF PRO key

All that is left to do is create our .env file and place our ACF PRO license key.

An example .env file with ACF PRO license key secret

Code language: plaintext (plaintext)

With all of our Composer dependencies established, we should be able to run composer install and have everything pulled into our project.

Set up deployment actions

Now that we have all our Composer dependencies set up. We’ll integrate our deployment actions. This will allow us to deploy any custom updates to our custom plugin (wp-content/plugins/custom-plugin) and theme (wp-content/themes/custom-theme) alongside our latest WPackagist or ACF Pro updates.

We’ll be using WP Engine’s official Deploy WordPress to WP Engine GitHub Action for this part. We’ve already covered all the steps to set this workflow up and highly recommend you have your SSH key established on WP Engine and within your GitHub repo.

Establish include and exclude rsync options

The key difference for this new set up is that we want to exclude some files and folders from the rsync process. Below is our final YAML workflow file.


name: Deploy to WP Engine production environment on: push: branches: - main jobs: build: runs-on: ubuntu-latest steps: - uses: actions/[email protected] - name: GitHub Action Deploy to WP Engine uses: wpengine/[email protected] with: # Deploy vars WPE_SSHG_KEY_PRIVATE: ${{ secrets.WPE_SSHG_KEY_PRIVATE }} WPE_ENV: qorp # Deploy Options REMOTE_PATH: "wp-content/" FLAGS: -azvr --inplace --delete --include-from config/include.txt --exclude=".*" --exclude-from config/exclude.txt PHP_LINT: TRUE SCRIPT: wp-content/config/
Code language: YAML (yaml)

The notable differences for this set up are highlighted above (the last four lines).

REMOTE_PATHstringOptional path to specify a directory destination to deploy to. Ex. "wp-content/themes/custom-theme/" . Defaults to WordPress root directory on WP Engine.
FLAGSstringSet optional rsync flags such as --delete or --exclude-from. The example is excluding paths specified in a .deployignore file in the root of the repo. This action defaults to a non-destructive deploy using the flags in the example above.

Caution: Setting custom rsync flags replaces the default flags provided by this action. Consider also adding the -azvr flags as needed.
-a preserves symbolic links, timestamps, user permissions and ownership.
-z is for compression
-v is for verbose output
-r is for recursive directory scanning
PHP_LINTbooleanSet to TRUE to execute a PHP lint on your branch pre-deployment. Default is FALSE.
SCRIPTstringRemote bash file to execute post-deploy. This can include WP_CLI commands for example. Path is relative to the WP root and file executes on remote. This file can be included in your repo, or be a persistent file that lives on your server.
Deployment options for Deploy WordPress to WP Engine GitHub Action

You’ll notice that we’re passing the FLAGS parameter, and it is referencing a config/include.txt file and a config/exclude.txt file. These are key to telling our workflow what files and folders we do and don’t want to deploy.

Let’s create these:


# Including plugins/themes that we check into git so that the version in github is deployed /plugins/custom-plugin /themes/custom-theme
Code language: plaintext (plaintext)


# Excluding these items from being deleted each sync plugins/* themes/* mu-plugins/ uploads/ blogs.dir/ upgrade/* backup-db/* advanced-cache.php wp-cache-config.php cache/* cache/supercache/* index.php mysql.sql .env .env.* vendor
Code language: plaintext (plaintext)

Create post-deploy script

After our GitHub Action performs all the syncing we have the option to run a script. This is how we’ll handle running Composer on the final production server. This way we’ll keep our repository small and only version control what we need to and let Composer run on the final server.


cd wp-content && composer install --optimize-autoloader --no-dev --no-progress
Code language: Bash (bash)

Bonus: PHP linting

The WP Engine GitHub action allows us to lint our code with PHP_LINT: TRUE and checks for syntax errors. It will not test for fatal errors, which is essential to note.


Utilizing Composer with WPackagist to manage your WordPress plugin dependencies can help keep teams organized and facilitate consistent workflows.

Also, keep in mind that there are many different ways to organize and deploy code. Let us know how you’re maintaining your ideal workflow—tag me on Twitter @dcook.