WordPress can be installed and used very quickly. But you can also over-engineer it and deploy it via Git to have some benefits. In this article, you can find information about why this may be a good idea and what pitfalls may wait for you.

Since different requirements lead to different solutions, I won’t share any actual code used to deploy the WordPress to a server. However, I will describe the logic used to deploy it.

Our goal

When we in our scenario thought about what we want to achieve, it sounded quite simple: having the same WordPress code on three machines, which will be used by a load balancer to be able to answer incoming requests.

Status quo was that we used an NFS (Network File System) to store the code of WordPress on a single point, and access it from each of the three machines. That makes this NFS a single point of failure, which is the main reason, we wanted to change that. (Aside of NFS is not as reliable as we have hoped in our scenario.)

Another advantage of deploying via Git is that we can simply disable write permissions for a large part of the WordPress instance, making it pretty secure, since even if a plugin would allow an attacker to write malicious code onto the server, if the directories of WordPress are read-only, the attacker simply cannot write anything anywhere.

In our case, we would also like to use a deployment via the Ansible deploy helper, which allows us to easily revert to an earlier state automatically if a deployment fails or the website is no more accessible after a deployment. An even bigger advantage in using such a system is that the files are first transferred in another directory and only after all files are transferred, the old directory is replaced with the new version, having always a consistent state on your server. Otherwise, it can happen that not all files are available for a short time and some users may not be able to access the website due to such an error.

Initializing the repository

First of all, we need the WordPress code in our repository. Luckily, the WordPress project is no more solely bound to SVN for their code, so we can just clone the official Git repository from GitHub: https://github.com/WordPress/WordPress

Make sure to use this repository, as it stores the code in its final form, which is also published as ZIP for download, as the “wordpress-develop” repository is used for active development.

WordPress configuration via .env

Deploying the code itself is one thing, in order to get it working, you’ll also need to setup the proper configuration. Since the WordPress repository does not include a wp-config.php itself, you need to create your own.

You could enter your credentials directly into this file, but since you need to add it to your Git repository, you should not do it. Credentials don’t belong into Git repository, as there is no protection whatsoever, which makes it a big security threat for your WordPress installation.

That’s why you should use a .env file, which could then be deployed to the server via your Continuous Delivery (CD) inside your Continuous Integration (CI), e.g. via GitHub or GitLab. In my case, this file looks like this:

WORDPRESS_DB_NAME=wordpress_database
WORDPRESS_DB_USER=wordpress_user
WORDPRESS_DB_PASSWORD=*snip*
WORDPRESS_DB_HOST=localhost
WORDPRESS_AUTH_KEY='*snip*'
WORDPRESS_SECURE_AUTH_KEY='*snip*'
WORDPRESS_LOGGED_IN_KEY='*snip*'
WORDPRESS_NONCE_KEY='*snip*'
WORDPRESS_AUTH_SALT='*snip*'
WORDPRESS_SECURE_AUTH_SALT='*snip*'
WORDPRESS_LOGGED_IN_SALT='*snip*'
WORDPRESS_NONCE_SALT='*snip*'
WORDPRESS_DISALLOW_FILE_MODS=true
WORDPRESS_DOMAIN_CURRENT_SITE=example.com
WORDPRESS_SENTRY_DSN=*snip*
WORDPRESS_MEMCACHED_HOST=localhost
WORDPRESS_MEMCACHED_PORT=11211
WORDPRESS_ACF_PRO_LICENSE='*snip*'Code language: JavaScript (javascript)

You can define whatever variables you want, which can then be used in your wp-config.php. Deploy the .env file in the same directory as the wp-config.php, or maybe even one directory above. Just make sure that PHP has access to it. Also, make sure to set proper permissions to the file to make it unreadable for other users.

Then, on the beginning of your wp-config.php, you can access it via this single line of code:

$env = \parse_ini_file( '.env' );Code language: PHP (php)

Now, all variables defined in your .env file are stored in the variable $env. You can then access it like this:

define( 'DB_NAME', $env['WORDPRESS_DB_NAME'] );
define( 'DB_USER', $env['WORDPRESS_DB_USER'] );
define( 'DB_PASSWORD', $env['WORDPRESS_DB_PASSWORD'] );
define( 'DB_HOST', $env['WORDPRESS_DB_HOST'] );Code language: PHP (php)

Since this is regular PHP, you can also check for the existence of a variable and use a default value otherwise:

define( 'WP_SENTRY_DSN', $env['WORDPRESS_SENTRY_DSN'] ?? '' );Code language: PHP (php)

If you now deploy the WordPress code as well as your configuration like this, you should get a working WordPress instance, if the database is already prepared. Since the database is not deployed via Git, you need to make sure that it’s imported in MySQL before.

Where do plugins/themes come from

Pretty sure you want to extend your WordPress with plugins and/or themes. That’s where the really interesting part starts, since you need to check where to get them from.

Composer

For plugins/themes from the official WordPress repository on wp.org, you can use Composer with the project of WordPress Packagist. Every plugin and theme from the WordPress repository can be included and then deployed this way. Via Composer’s installer-paths extra configuration, you can directly deploy them in the correct directory.

Some developers even allow you to deploy their pro plugins via Composer, most notably Advanced Custom Fields Pro.

Git repositories

For custom plugins (or also for pro plugins, which have no other option), you can use Git repositories to deploy them. However, you would need a CI script to clone these repositories to the correct directory during deployment and before the code is pushed to the server.

Git submodules

Another method similar to Git repositories is using Git submodules. This allows you to directly set a reference to another Git repository in your main WordPress Git repository. You would still need to make sure code is properly cloned from the submodules in your CI, but usually this is done automatically for you.

In both Git cases, you need to make sure to get the latest code you want to use. In our case: the latest tag, as every release has such a tag (if yours has not: start creating them for each release). You can use this command to get the latest tag for each submodule:

git submodule foreach --recursive 'git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)'Code language: Bash (bash)

Manually

If you have code outside of Git (please, don’t), that you need to use in your WordPress instance, you can add it within you CI, too. However, this would be a manual effort to automate receiving and storing it inside your CI script during deployment.

Generate assets and get dependencies

One major problem with using Git for plugins, themes, etc. is that usually assets like CSS or JavaScript need to be processed/minified and dependencies have to be pulled before they are properly working in a production environment. That’s why your deployment script must contain a step for generating such assets, depending on your requirements. In our case, we started by iterating over each of our Git submodules and generated the assets via Composer (PHP dependencies) and npm (node package manager, processing SCSS/minifying JavaScript) until we got perfectly working code.

Extra deployment for plugins/themes

We ended up using a different approach, since we want to deploy the WordPress instance itself without any dependencies of plugins/themes. Thus, we only deploy the WordPress Core code and create symbolic links to the actual path where plugins/themes are deployed to. This will then look like this in the directory structure:

.
|-- plugin-releases
|   |-- rh-plugin-name
|   |   |-- current
|   |   `-- releases
|   |       `-- [
]
|   `-- rh-different-plugin
|       |-- current
|       `-- releases
|           `-- [
]
|-- wp-content
    `-- plugins
        |-- rh-plugin-name -> ../../../plugin-releases/rh-plugin-name/current
        `-- rh-different-plugin -> ../../../plugin-releases/rh-different-plugin/currentCode language: JavaScript (javascript)

The actual plugin code is in /plugin-releases and the plugin directories in wp-content/plugins only link to these releases. Why is this? Because we use the Ansible deploy helper, which always replaces the whole directory of the WordPress core on its deployment, which means any changes we make in any subfolder of WordPress will practically be reversed on the next deployment. Loading plugins/themes in another directory, which won’t be affected this way, we can make sure, that already deployed plugins/themes will still be available after a WordPress deployment.

Of course, you should make sure that the plugins/themes are available before you deploy the whole WordPress instance. Also, this only applies to plugins/themes deployed via Git. There’s no need to have such a structure for plugins deployed via Composer. If you add your plugins/themes as Git submodule (e.g. in a separate directory, which is then excluded from the actual deployment), you can iterate over each of the submodules to generate the symbolic links automatically during deployment.

Install languages

As we want to have our WordPress to be mostly read-only, and especially disabled file modifications via DISALLOW_FILE_MODS, updates won’t be possible via WordPress directly, as well as updating and even installation of new languages. That’s why we defined the used localizations in a CI variable and then use a separate deployment step to load WordPress in a docker container, copy all plugins/themes from Composer into it and install the language for core, plugins and themes via WP-CLI. And since we can, we also use WP-CLI to create PHP files out of the translation files. We can then use the wp-content/language directory in a later step to deploy it onto the server. As we’ve also copied all plugins and themes, their translations are also available and can be used.

Shared resources

Since the PHP code of WordPress is not all you need, you also have to manage shared resources, most obviously the directory in wp-content/uploads (or any custom path where you or a plugin stores uploaded data). So, in our case, we cannot get rid of the entire NFS, since all our three servers need to access the uploads, but at least we don’t need to run PHP of off it. I’m pretty sure you can also synchronize shared resources otherwise, but this will also come with duplicating the amount of storage needed. Since in our case this would be storage capacity duplicated in the hundreds of gigabyte, we’re better off this way.

Usually, for your deployment, you also want to use a symbolic link here from the uploads directory of WordPress to your actual storage space of the uploads.

Deployment structure

As an overview, this is what our deployment structure for the WordPress instance looks like:

  1. Install Composer dependencies (which means: plugins/themes from the WordPress repository)
  2. Install locale packages via WP-CLI (in a WordPress docker container)
  3. Upload the complete code via Ansible

GitLab-specific problems

Since we use GitLab in our deployment, we came along some GitLab-specific problems, which sometimes took longer than necessary to fix. GitLab’s documentation is not particularly helpful in some of these cases, sometimes also misleading or even completely wrong.

Job tokens

To even access other repositories within your deployment, you have to add your WordPress instance repository to the “CI/CD job token allowlist” in each of your git repositories, that you want to access and deploy, in Settings > CI/CD > Job token permissions. Otherwise, you can do whatever you want, you won’t have access to even pull the repository.

Absolute submodules

For GitLab to have properly access to submodules during deployment, you can’t use absolute paths/URLs for the source of the submodules. So if you have your WordPress Git repository at gitlab.com/username/wordpress.git and a plugin in gitlab.com/username/my-plugin.git, in order to access the plugin, the URL of the submodule must be url = ./plugin.git. At least for a self-hosted GitLab instance.

Conclusion

Having WordPress deployed via Git gives you the possibility to have a more secure instance, which is identical on multiple servers and can even help you build a platform for high-availability and without the need of having a maintenance mode enabled for updates.

Reposts

Leave a Reply

Your email address will not be published. Required fields are marked *