Using Composer for easy WordPress Deployments

Learn how you can use Composer with WordPress to easily deploy it and manage it on multiple servers. Install & manage your WordPress themes and Plugins via Composer.

Using Composer for easy WordPress Deployments

There are many ways to manage a WordPress application. Fortunately, it is possible to use Composer with WordPress.

In this article, we will show how to use Composer with WordPress so that you can easily maintain it, manage it and deploy it in different server environments.

That means that you can install WordPress core, themes, plugins, etc., and update and delete them when needed via Composer.

To achieve this, we will use the following tools:


Using Composer with WordPress

Our first goal is to download the WordPress core, the plugins and themes as versioned Composer dependencies.

So to use Composer with WordPress, we will first install Composer and then create a composer.json file in the root directory of our project:

{
    "repositories":[
        {
            "type":"composer",
            "url":"https://wpackagist.org"
        }
    ]
}

Since Composer uses Packagist by default as a package repository, we will need to tell Composer that we will need WPackagist instead.

Now we would be able to install public WordPress plugins and themes as Composer dependencies, for example, like this:

{
    "require": {
        "wpackagist-plugin/akismet":"^4.1",
        "wpackagist-theme/twentytwenty":"*"
    }
}

Next, let's install the WordPress core via Composer. We will be using John P Bloch's mirror of WordPress Core to achieve that:

{
	"require": {
	        "johnpbloch/wordpress": ">=5.4"
	},
	"extra": {
	        "installer-paths": {
	            "wp-content/plugins/{$name}/": [
	                "type:wordpress-plugin"
	            ],
	            "wp-content/themes/{$name}/": [
	                "type:wordpress-theme"
	            ]
	        },
	        "wordpress-install-dir": "wordpress"
	},
	"repositories": [
	        {
	            "type": "composer",
	            "url": "https://wpackagist.org"
	        }
	]
}       

In the require section, we added the dependency. Next, in the extra section, we told Composer where to look for themes and plugins. Lastly, we defined the WordPress installation directory to be wordpress.

Now we can run the following command:

composer install --prefer-dist

Composer will now install WordPress within the wordpress directory in the root of our project.

To be able to manage WordPress with Composer fully, we need to use a different directory for wp-content instead of the default one, wordpress/wp-content.

Let's create a new directory in the project's root called wp-content.

Let's go ahead and create the standard wp-config.php file and then add the following code:

$domain = 'mydomain.test';

define('WP_SITEURL', "{$domain}/wordpress");
define('WP_HOME',"http:{$domain}");

$httpHost =  isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : $domain;

define( 'WP_CONTENT_DIR', dirname( __FILE__ ) . '/wp-content' );
define( 'WP_CONTENT_URL', 'http://' . $httpHost . '/wp-content' );

/** Absolute path to the WordPress directory. */
if ( !defined('ABSPATH') ) {
    define('ABSPATH', dirname(__FILE__) . '/wordpress');
}

/** Sets up WordPress vars and included files. */
require_once(ABSPATH . 'wp-settings.php');

Next, let's create an index.php file within our project's root directory:

<?php 
define('WP_USE_THEMES', true);
require( dirname( __FILE__ ) . '/wordpress/wp-blog-header.php' );

We will also create a .htaccess file in the root directory with the following contents:

# BEGIN WordPress

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>

# END WordPress

Since the wp-config.php file contains sensitive data; we will not commit it to our repository by creating a .gitignore file:

/wp-config.php
/wordpress/
/wp-content/
/vendor/

The wordpress, wp-content and vendor directories also need to be ignored so that we will add them to the .gitignore file as well.

Now we end up with a very simple project structure:

/
β”œβ”€β”€ .htaccess
β”œβ”€β”€ composer.json
β”œβ”€β”€ composer.lock
β”œβ”€β”€ wp-config.php
β”œβ”€β”€ wp-content/
β”œβ”€β”€ wordpress/
β”œβ”€β”€ vendor/
└── .gitignore

Note: Make sure your Site address (URL) is yourwebsite.com , and your WordPress address (URL) are: yourwebsite.com/wordpress. This is basically like giving WordPress its directory with the second method.


Using composer to install WordPress plugins and themes from private repositories

You may want to install plugins or themes hosted in private repositories on Github, Bitbucket or elsewhere, but not on WPackagist.
That is possible, but there are two things you need to do:

  1. provide your credentials in an auth.json file
  2. within the composer.json, in the repositories section, you need to tell Composer where to look for the repository:
{
  "type": "vcs",
  "url": "https://bitbucket.org/your-company/your-theme.git"
}

Using Envoy to deploy WordPress with Composer

Now that we have our WordPress project with Composer let's see how to deploy it.

As mentioned previously, we will use Laravel Envoy to write a deployment script for WordPress.

The reason why we are using Envoy is because of its simplicity. You can also decide to use a different tool like Deployer.

Let's go ahead and download Envoy.

We can deploy our WordPress application to different servers: development, staging, and production with the deployment script.

Because the project structure is so lightweight, it will be easy to write the deployment steps.

Here is what our deployment script for WordPress will look do:

  1. Create a new release with a timestamp in a releases directory on your server
  2. Clone the repository
  3. Install all the dependencies
  4. Copy wp-config, .htaccess and other files specific to the server environment (e.g. production)
  5. Create a symlink of the uploads directory to the new release
  6. Create a symlink of the new release to the domain's document root directory on the server
  7. Clean up old releases from the server.

First, let's go to our server and set up a directory called current as the domain document root directory on the server.

If you are unfamiliar with that, you can follow a tutorial on how to do it.

You can create the following directory structure on your server: ~/sites/yoursite

Now, let's install Envoy with the following command:

composer global require laravel/envoy

Next, let's write the deployment script for WP. Let's create a new file in our project root directory called Envoy.blade.php.

In Envoy, we will use the following directives: @servers, @setup, @task and @story. It's very straightforward; here is what they mean:

  • @servers - this is where you define all your servers with their corresponding IPs.
  • @setup - a section where you can define variables or configurations.
  • @task - a single action that should be executed on the specified server.
  • @story - a sequence of tasks that must be executed on the specified server.

Envoy works because it will ssh to the specified servers and execute the defined script by following the directives.

Inside the directives, you can use PHP to define which UNIX commands must be executed on the servers.

The first step will be to define multiple server environments:

@servers(['local' => '127.0.0.1', 'staging' => 'w.x.y.z' 'production' => ['a.b.c.d']])

Next, let's define some variables for our setup steps. These are the things we can configure, depending on our needs.

In our case, we will assume we have a private repository on Bitbucket for the project.

The deployment script will clone the master branch and set up the release directory structure, as discussed above.

@setup

    // the repository to clone
    $repo = '[email protected]:your-company/your-wp-composer-project.git';

    // the branch to clone
    $branch = 'master';

    // set up timezones
    date_default_timezone_set('Europe/Berlin');
    
    // we want the releases to be timestamps to ensure uniqueness
    $date = date('YmdHis');

    // the application directory on your server
    $appDir = '~/sites/yoursite';

    // this is where the releases will be stored
    $buildsDir = $appDir . '/releases';

    // this is where the deployment will be
    $deploymentDir = $buildsDir . '/' . $date;

    // and this is the document root directory
    $serve = $appDir . '/current';
    
@endsetup

Next, let's create a task actually to create the directory for the new release:

@task('dir')
    echo "Preparing new deployment directory..."
    
    cd {{ $buildsDir }}
    mkdir {{ $date }}
    
    echo "Preparing new deployment directory complete."
@endtask

As you can see, it's using Blade syntax for the UNIX commands, with the variables we defined in @setup.

The following task will clone the repository and the specified branch:

@task('git')
    echo "Cloning repository..."
    
    cd {{ $deploymentDir }}
    git clone --depth 1 -b {{ $branch }} "{{ $repo }}" {{ $deploymentDir }}

    echo "Cloning repository complete."
@endtask

Of course, you will need to ensure your server can access the git repository.

The next task will install the dependencies and copy the wp-config.php file:

@task('install')
    echo "Installing dependencies...";

    composer install --prefer-dist
    cp ../../wp-config.php ./wp-config.php
    
    echo "Installing dependencies complete."
@endtask

The next task will create the symlinks to the new release and to the uploads directory:

@task('live')
    echo "Creating symlinks for the live version..."
    
    cd {{ $deploymentDir }}
    ln -nfs {{ $deploymentDir }} {{ $serve }}
    ln -nfs {{ $appDir }}/uploads {{ $serve }}/wp-content/
    
    echo "Creating symlinks completed."
@endtask

The last task will perform cleanup and delete old releases:

@task('deployment_cleanup')
    echo "Cleaning up old deployments..."
	
    cd {{ $buildsDir }}
    ls -t | tail -n +4 | xargs rm -rf
    
	echo "Cleaned up old deployments."
@endtask

We can configure the number of old releases we want to keep on the server. In our case, it is 4.

Now let's write a @story directive where we can group the tasks we just wrote:

@story('deploy-staging', ['on' => 'staging'])
    dir
    git
    install
    live
    deployment_cleanup
@endstory
@story('deploy-production', ['on' => 'production'])
    dir
    git
    install
    live
    deployment_cleanup
@endstory

As you can see, we used the names of the tasks inside the @story to define the order of execution.

Finally, we would be able to run the following commands to deploy our WordPress to the server:

envoy run deploy-staging

and

envoy run deploy-production

and so on, for each environment we want.

So now we can manage our WP installation entirely via Composer.


Keeping WordPress in sync with Composer

As you already know, updating plugins via the WordPress Admin panel is possible. So if the WordPress administrator updates a plugin manually, it will make the site out of sync with the Composer file, which will beat the purpose of using Composer and it might cause the website to stop functioning correctly.

So, how to avoid this situation?

It will be best to tell administrators not to update plugins on their own, especially not in production.

Once you have that settled, you can write a simple task in Envoy to update a given plugin:

@task('update-plugin')
cd {{  $deploymentDir  }}
wp plugin update {{ $plugin }} --version={{ $pluginVersion }}
@endtask

@story('update')
update-plugin
@endstory

Here we are using WP-CLI to perform the update ( but you can also do it via the admin panel if you prefer )

You can execute it by typing:

envoy run update --plugin=bbpress --version=2.6.4

Here we assume we want to update the plugin bbpress to version 2.6.4

Once this is executed, you can also update your composer.json:

composer require wpackagist-plugin/bbpress 2.6.4

And finally, let's deploy our change to the server:

envoy run deploy-production

Other ways to use Composer with WordPress

We have shown how to manage WordPress with Composer and deploy it with your customizable deployment script.

There are, of course, other ways to manage WordPress sites with Composer. One of the popular ways to do so is with Bedrock.

Bedrock is a WordPress boilerplate project with a predefined directory structure.

It looks like this:

β”œβ”€β”€ composer.json
β”œβ”€β”€ config
β”‚   β”œβ”€β”€ application.php
β”‚   └── environments
β”‚       β”œβ”€β”€ development.php
β”‚       β”œβ”€β”€ staging.php
β”‚       └── production.php
β”œβ”€β”€ vendor
└── web
    β”œβ”€β”€ app
    β”‚   β”œβ”€β”€ mu-plugins
    β”‚   β”œβ”€β”€ plugins
    β”‚   β”œβ”€β”€ themes
    β”‚   └── uploads
    β”œβ”€β”€ wp-config.php
    β”œβ”€β”€ index.php
    └── wp

Bedrock will help you move WordPress to a directory called wp and set up different environments: development, staging, and production.

It also does a good job setting up config values in a .env file with the help of PHP dotenv.

The wp-config.php reads the .env file, which is not located in the web directory, and that improves the security of the website.

wp-content is renamed to app

Once you have it set up, you can add plugins as Composer dependencies, similar to what we showed earlier.

You can write a deployment script with Envoy, Deployer, or another tool.


Conclusion - managing and deploying WordPress Sites with Composer

In this article, we showed two ways (manually and with Bedrock) of managing WordPress with Composer and how to easily deploy WordPress to multiple environments when used with Composer.

First, we defined a composer.json file, which enabled us to install the WordPress Core and the themes and the plugins as dependencies.

This made the project very lightweight and easy to deploy with a script.

While WordPress is not initially thought to be used as a Composer dependency, it is a great advantage in modern development workflows. It ensures relatively easy deployments with zero downtime and quick rollbacks.

If you have any experience using WordPress with Composer (and deploying it), we would like to hear about it in the comments.


This post is sponsored by PluralSight

PluralSight